基于CQRS的Todo应用DDD权限校验分层与实现位置咨询
嘿,针对你的CQRS Todo应用权限校验问题,我结合DDD和CQRS的最佳实践来给你拆解建议:
一、DDD分层中的权限职责归属
你的权限规则本质是业务规则(用户仅能操作同组Todo),所以不同分层的职责要明确:
- 领域层:这是核心规则的归宿。你应该在
Todo实体中封装权限校验逻辑,比如添加CanBeAccessedBy(User user)或CanBeUpdatedBy(User user)这样的方法,确保无论哪个上层调用领域实体,都必须遵守这个规则。比如当尝试更新一个Todo时,实体自身可以判断操作的用户是否属于同组,直接抛出业务异常如果权限不足。这样能保证业务规则的一致性,不会因为上层调用的不同而被绕过。 - 应用层:作为协调层,应用层负责触发权限校验。比如在命令/查询处理器中,先获取当前用户的组信息,再调用领域层的校验方法,或者做前置的条件过滤(比如只处理用户同组的请求)。它是领域层和基础设施层的桥梁,但不应该承载核心业务规则。
- 基础设施层:这层只做辅助性的权限优化,比如在查询数据库时直接带上
GroupId = 当前用户GroupId的过滤条件,提升性能。但绝对不能用这个替代领域层的校验——因为如果有其他方式绕过数据库查询(比如直接操作内存中的实体),权限规则就会失效。
二、CQRS架构下的具体放置建议
CQRS把读写操作分开,所以要分别针对命令端(Create/Update)和查询端(Read)来安排:
命令端(Create/Update操作)
- 优先放在命令处理器中:命令处理器是业务操作的入口,在执行命令前做权限校验是最合理的。比如处理
CreateTodoCommand时,先拿到当前用户的GroupId,要么强制命令中的TodoGroupId与用户组一致(甚至可以直接从用户信息中获取GroupId,不让客户端传入,避免篡改),要么调用Todo.CanBeCreatedBy(user)做校验。如果权限不通过,直接返回错误,不进入领域逻辑。 - 实体层做兜底校验:即使命令处理器漏了校验,实体自身也要保证规则被执行。比如
Todo的构造函数可以接收用户的GroupId,确保创建的Todo属于用户所在组;更新方法也可以检查操作用户的组是否与Todo组匹配,防止非法修改。 - 不建议放在控制器(动作)中:控制器只是API的入口,负责请求参数解析和响应返回。如果把权限校验放在这里,会导致业务逻辑分散到入口层,而且如果有其他命令触发方式(比如消息队列),就会绕过控制器的校验,出现权限漏洞。控制器只适合做基础的参数合法性校验(比如必填项是否为空)。
查询端(Read操作)
- 放在查询处理器中:这是最适合的位置。处理查询请求时,查询处理器直接在查询逻辑中加入用户组的过滤条件。比如处理
GetTodoListQuery时,执行SQL:SELECT * FROM Todo WHERE GroupId = @UserGroupId;处理GetTodoByIdQuery时,SQL要同时校验TodoId和GroupId,确保返回的Todo属于用户所在组。 - 性能优先,兼顾规则:查询端通常更注重性能,所以直接在数据库层面过滤是最优解,但要确保查询处理器是读操作的唯一入口,避免其他绕过方式。如果需要更严格的校验,也可以在查询结果返回前,用领域层的
CanBeAccessedBy方法再次确认,但这可能会带来额外的性能开销,需要权衡。
举个贴合你场景的例子
比如用户1(GroupId=1)请求获取Todo3(GroupId=2):
- 查询处理器拿到用户的GroupId=1,执行SQL
SELECT * FROM Todo WHERE TodoId=3 AND GroupId=1,结果为空,返回“权限不足”的响应。
再比如用户3(GroupId=2)创建Todo: - 命令处理器拿到用户的GroupId=2,直接将命令中的Todo
GroupId设为2(不让客户端传入,避免篡改),调用Todo.Create(title, desc, 2),实体构造函数确保GroupId有效,完成创建。
内容的提问来源于stack exchange,提问作者Furkan
相关产品推荐
相关产品推荐

