当我没有ID

时间:2019-10-06 17:06:25

标签: authentication command domain-driven-design cqrs

我目前正在开发具有登录页面的CQRS应用程序。在LoginCommand中,从数据库中获取一个用户,如果该用户可以登录(加密的密码与给定的加密密码匹配),则会向该用户分配一个令牌。

在CQRS中,该命令通常接收该命令针对的元素的ID,以便获取其标识的域聚合并在其上执行逻辑。但是,在那种情况下,我从用户那里得到的是电子邮件。尽管这是一个唯一字段,但我不确定尽管是一个唯一字段,但使用该字段获取聚合是否错误。

我也可以想到其他具有相同问题的情况,例如尝试通过不包含帖子ID的给定语义URL来识别Post

由于禁止在Command中执行查询,并且不太可能在登录表单中附加用户ID,因此在这种情况下我必须提取哪些选项?我是否应该在命令(例如控制器)之外查询读取模型?

5 个答案:

答案 0 :(得分:2)

在这种特殊情况下,我认为CQRS对您的设计没有任何影响。也许事件源可能会。

如果您有一些自然键,那么您有几个选择。一方面,它必须是唯一的。您的UserRepository可能还有一种使用电子邮件地址来获取用户ID的方法:

public interface IUserRepository
{
    User Get(Guid id);
    User Find(string email);
}

当我可能返回Find时,我倾向于使用null方法,因为Get表示该实体应该存在,并且在找不到该实体时会抛出异常。

如果您只想按id查找,则需要使用某个商店中的id查找email。根据您的一致性要求,最终的一致性查询/读取存储可能就足够了,但是没有什么可以阻止您访问具有电子邮件到ID映射的100%一致性存储。

答案 1 :(得分:1)

从域的角度来讲,与ID记录关联的User就是surrogate key。它在现实世界中没有对应的表示形式,仅用于帮助您持久保存和检索数据。

因此,如果emailUser记录的唯一字段,则一定要像在命令中使用标识符一样使用它。

这不一定意味着您可以摆脱id字段。

您仍希望在id记录中有一个替代键,例如User字段,因为您可能希望为用户提供更改其电子邮件地址的选项。即使更改了电子邮件地址,您仍需要能够在整个系统中唯一地标识用户,这就是代理密钥派上用场的地方。由于性能原因,您还需要代理密钥。最好总是使用IntegerUUID字段代替String电子邮件地址作为主键,或在参考字段中使用

您还应该区分Command及其对应的Command HandlerCommand只是一个DTO,它封装了外部环境中发生的更改或需要提交给数据库的更改。从这种意义上讲,它们是不可变的,不应以任何方式执行查询或更新自身。

Command Handler(本质上类似于应用程序服务,但是在后台)消耗命令中的数据。您可以在其中查询存储库并检索记录。实际上,这将是进行任何重复或参考密钥验证的必要条件。

答案 2 :(得分:1)

我强烈建议这不是“命令”,而是从读取模型中读取。原因如下:

您只是在检查提供的凭据是否与读取模型中的凭据匹配。此操作不会更改域的状态,因此与命令的使用不一致。

但是这里存在更严重的潜在问题。我不确定您是否正在使用事件源,但是如果您使用事件源,我将非常担心将密码放入事件源中。即使加密。使用当前密码和历史密码对事件存储进行数据泄露可能是一个现实问题。

还有更多...

我想尽可能限制密码在网络上的传输。与对成员数据库的传统凭据检查相比,将其添加到命令中(取决于您的基础结构)会增加额外的传输时间。

我确实知道,但是您可能希望记录某人已登录或登录失败的事实。为此,如果您的域需要此命令,请发出“ RecordSuccessfullLoggin”或“ RecordFailedLoginAttempt”之类的命令。

答案 3 :(得分:1)

首先,我假设当您说您向用户分配令牌时,这意味着您在数据库中写入了该分配。否则,您的登录命令将不是命令。

话虽如此,我认为在您的用户存储库中有一种方法可以检索知道电子邮件的用户。

答案 4 :(得分:0)

这是一个非常有趣的问题,我在 CQRS/DDD/Event Sourcing 上下文中也遇到过这种困境。下面是在已经提到的其他答案之上的一些其他答案。

选项 A:如果您认为通过将用户设置为已登录或类似状态来修改用户状态,则需要一个 ID。

  1. 使用电子邮件作为您的 ID。您可以将电子邮件视为聚合/用户 ID。虽然就像有人提到的那样有限制,因为电子邮件永远无法修改。不过,您始终可以使用所需的电子邮件(即 ID)创建一个克隆用户,因此这更像是一个技术问题,如果业务需要,可以轻松实现。

  2. 有两个 UI 步骤登录并在第一个步骤中检索 ID。或者您可以以标准方式为您的用户使用 int/guid/uuid,并预先通过电子邮件查询读取端以检索此 ID。 有些登录系统(Google 或 Yahoo)也分两步工作。见https://ux.stackexchange.com/questions/91763/google-and-yahoo-require-you-to-enter-your-username-first-then-password-why-is 尽管存在争议,但它有一些优点,例如根据用户的偏好允许不同的登录机制,在 UI 上显示自定义登录,烦人的自动机器人,因为它会减少网络钓鱼附加,因为第二个屏幕对于不同的用户来说看起来“不同”。此外,如果您从注册设备访问(例如:您的徽标等),它允许读取端返回与从新设备访问不同的内容。

选项 B: 另一个选项不是将其视为命令,而是将其视为查询。基本上,您是通过传递电子邮件和密码来查询系统,以获取与该请求匹配的令牌。您可以稍后使用该令牌来实际更改系统,例如附加到命令作为有效负载的一部分,或命令元数据或 http 标头或类似内容。如果您愿意,负责发行令牌的系统可以引发事件,以便您的域能够对这些“外部”事件(例如:userLoggedIn、userLoggedOut、loginAttemptFailed)做出反应,并对该特定用户的聚合执行所需的任何操作.

我个人喜欢 Option A.2 有 2 个步骤的选项,因此流程如下:

  1. 最终用户向阅读端发送查询,通过特定电子邮件进行搜索
  2. 查询端返回或多或少的信息取决于设备是否已注册。但无论如何它都会返回一个 ID
  3. 如果电子邮件存在(或者即使不存在,如果您想避免共享有关哪些电子邮件存在的信息,最终用户也不需要知道这一点。)显示第二个 UI 步骤,用户在其中输入密码和/或任何其他安全凭证(PIN?电话号码?通过电子邮件发送的代码?)。 另外,请注意使用此机制,用户可以轻松拥有多个电子邮件、别名等。
  4. 最终用户通过登录尝试向写入端提交命令,提供凭据并使用正确的 ID。
  5. 写入端(命令处理程序/域)验证凭据并执行其需要执行的任何操作,引发事件 userLoggedIn 或 failedLoginAttempted 或您认为与系统相关的任何事件以捕获或更改为。

总而言之,我的建议是,如果您想将您的 User 视为更改状态的聚合(loggeddIn、loggedOut、locked),您仍然可以使用“标准”CQRS 流程,但您需要一种用于前端的机制end 事先知道 ID 以便发送命令。 这还不算太糟糕,因为这个 ID 可以缓存在浏览器中,即使不访问系统的查询端也可以自动检索,用户不需要关心它。