跨微服务的操作授权

4
现代的多用户Web应用对用户操作施加了很多限制,也就是说,这些操作需要权限。例如,用户只能更改自己的个人数据,只有组成员才能在该组中发布内容。在经典的单体应用程序中,可以通过连接多个数据库表来轻松执行此类限制,并根据查询结果采取相应行动。然而,在微服务中,这些限制应该在哪里和如何处理变得不太清楚。
为了举例说明,请考虑一个Facebook克隆版。整个应用程序由以下几部分组成:
- 一个前端,使用JS和其他Web技术编写 - 由多个微服务组成的后端 - 用于从后端检索和提交数据的API,即网关
至于业务逻辑,有两个众所周知的实体:
- 事件(如音乐会、生日聚会等) - 帖子(存在于墙壁、页面、事件等上的文本条目)
假设这两个实体由单独的服务EventService和PostService管理。然后考虑以下约束条件:
“活动帖子可以由两种类型的用户删除:帖子的作者和活动的主办方。”
在单体应用程序中,这个约束概念上非常容易处理。收到删除帖子的请求,提供帖子id和用户id,
1. 获取帖子所属的事件。 2. 检查用户是否是帖子的作者。 3. 如果是,则删除该帖子。如果不是,则获取活动的主办方。 4. 检查用户是否在主办方之中。 5. 如果是,则删除该帖子。
然而,在微服务策略下,我很难想象如何跨服务划分此类操作的职责。
备选方案1
一个简单的方法是将此类逻辑放入网关中。这样,可以基本上执行与上述相同的过程,但调用服务而不是直接调用数据库。大致草图:
// Given postId and userId
// Synchronous solution for presentational purposes

const post = postClient('GET', `/posts/${postId}`);
const hosts = eventClient('GET', `/events/${post.parentId}/hosts`);
const isHost = hosts.find(host => host.id == userId);

if (isHost) {
    postClient('DELETE', `/posts/${postId}`);
}

然而,我对此解决方案并不满意。一旦我开始在网关中放置这样的逻辑,就会变得非常诱人,因为这是一种快速简单的完成任务的方式。所有业务逻辑最终都将聚集在网关中,服务将变成“愚蠢”的CRUD端点。这将违背拥有具有明确定义的责任范围的独立服务的目的。此外,随着操作变得更加复杂,它可能会变得缓慢,因为它可能会导致对服务的高数量调用。
本质上,我将重新发明单块架构,用缓慢和有限的网络调用替换数据库查询。
备选方案2
另一个选择是允许服务之间的无限通信,在执行删除操作之前,PostService可以仅仅询问EventService用户是否是所讨论事件的主持人。然而,我担心大量的微服务相互通信可能会在更长时间内引入许多耦合。专家似乎普遍建议反对直接的服务间通信。
备选方案3
通过一个可靠的事件发布和订阅系统,服务可以保持更新关于其他服务中发生的事情的最新状态。例如,每当在EventService中将用户提升为主持人时,都将发布一个事件(例如,“events.participant-status-changed,{userId:14323,eventId:12321,status:'host'}”)。PostService可以订阅事件并在收到删除帖子请求时记住这个事实。
然而,我对这一点也不太满意。它将创建一个非常复杂且容易出错的系统,其中一个未处理的(但可能罕见的)事件可能会导致服务不同步。此外,有风险导致逻辑放错地方。例如,尽管概念上这是事件实体的属性,但此问题中的约束将由PostService处理。
但我应该强调,当使用微服务实现应用程序时,我非常乐观地认为事件的有用性。我只是不确定它们是否是解决这类问题的答案。
你会如何解决这种假想但相当现实的困难?

1
好帖子。我很想读这篇帖子中有趣的回答。 - Anil
2个回答

1
一个活动的帖子可以被两种用户删除:帖子的作者和活动的主持人。
因此,我首先要做的是确定删除帖子的权力所在——使用指导原则的思想,即应该有一个单一的写作者负责维护任何给定的不变量。
在这种情况下,Post服务似乎是合理的选择。
我假设管道包括某种机制来检测用户是否是他们所说的身份——已认证的身份是服务的输入。
对于作者删除帖子的情况,在验证规则是否满足方面应该是微不足道的,因为我们有创建和删除帖子的权限在同一个地方。这里不需要协作。
因此,棘手的部分是确定经过身份验证的身份是否属于活动主持人。可以推断出,确定事件主持人的权威属于Event服务。
现在,让我们来进行一次现实检验:如果您在没有锁定信息的情况下查询事件服务以查找事件的主持人,则所有者可能会与删除命令的处理同时更改。换句话说,ChangeOwner命令和DeletePost命令之间可能存在数据竞争。 Udi Dahan观察到:
微秒级别的时间差异不应该对核心业务行为产生影响。
特别是,行为的正确性不应取决于消息传输系统恰好传递命令的顺序。
您的事件溯源方法最接近这个想法。
然而,我对此并不完全满意。它将创建一个非常复杂且容易出错的系统,其中未处理的(但潜在罕见的)事件可能导致服务失去同步。此外,有风险逻辑会出现在错误的位置。例如,本问题中的约束将由PostService处理,尽管从概念上讲它是事件实体的属性。
几乎是这样 - 关键思想是:如果邮政服务是邮寄生命周期的权威,那么它必须被允许宣布它已经改变了主意。您在设计中构建了这样一个概念:权威根据其可用信息做出最佳决策,并在新信息将使早期选择无效时应用更正(有时称为补偿事件)。
因此,当删除命令到达时,您会检查它是否来自主机。如果是,您可以立即标记该帖子。如果不是来自主机,则您会记住有人想要删除该帖子,如果稍后更新事件通知您同一人是主机,则可以应用该标记。
相同的方法也适用于相反的情况-删除来自主机,因此帖子已被标记。糟糕!我们刚刚发现骗子不是主机。好吧,那就再次显示该帖子。

在实践中,你会如何将这些事件实际地联系起来?例如,几毫秒前发生了一次删除。如果在此之后不久收到一个“HostChanged”事件,你会检查旧主机执行的每个可能在记录事件日期之后发生的删除并将其还原吗? - plalx
@VoiceOfUnreason:“所以当删除命令到达时,您需要检查它是否来自主机。”如果我理解正确的话,这涉及邮件服务直接与事件服务通信,以检索调用操作的用户的角色(即是否为主机)? - Hannes Petri

0

跟进:

在过去的一周里,我一直在密切思考这个问题,也许我发现了我的推理方式中的一个缺陷。我曾经认为帖子实体仅驻留在帖子服务中,但也许这是一个令人不安的过度简化。

如果每个服务(即有界上下文)都有自己的帖子概念,并因此安排存储呢?事件服务保留了在事件中发布的帖子的表格,墙服务记录了墙上的帖子等等。

这些实体将非常薄,主要由GUID、发布者身份和内容组成。它们还可以包含仅在该上下文中使用的特殊属性。例如,事件可能允许固定帖子,但其他服务则不允许。

(Nota bene:下面的术语“事件”同时具有完全不同的含义,即使用例如Apache Kafka之类的消息在进程之间发送描述发生的事情。)

每次提交帖子到服务时,都会向事件总线发布一个事件。例如,当用户在活动中发布帖子时,事件服务将创建一个帖子实体并发出events.post-posted {id: ..., authorId: ..., contents: ...}。同样,墙也会发布wall.post-posted {id: ..., authorId: ..., receiverId: ..., contents: ...}
反过来,帖子服务会监听所有这些事件。每次将帖子发布到另一个服务时,在帖子服务中创建一个相应的帖子实体,并共享原始帖子的ID。这是“智能”帖子实体,具有应用程序中所有帖子通用的所有功能。它可以处理发送通知、安排线程、发现滥用、记录喜欢等功能。
这意味着每个服务在处理其帖子实体时都更加自由,因为它们不再参考驻留在单个服务中的单个信息源。这使得网关可以根据情况从多种方式中选择检索帖子数据。例如,为了告诉UI一个帖子已被固定,它需要与事件服务交互,但为了获取文本内容,它可能需要与帖子服务交互。也许墙服务中的帖子实体有特殊选项来处理发布在人们墙上的生日祝福。
回到最初的问题:这消除了服务在处理事件删除时进行通信的需要。它不再通过帖子服务删除,在接收到删除帖子请求时,由事件服务负责。由于它拥有关于帖子作者和事件主机的信息,它可以自行做出决策。
对此想法的批评:
虽然我觉得我在正确的轨道上,但我有两个主要顾虑。
第一个是这显然不能完全回答如何使用微服务实现Web应用程序时处理权限的原始问题。也许这只是一个假设场景的答案,而其他问题的稍加变化根本无法通过这种方法缓解。

我的第二个担忧是我正在陷入被动侵略事件陷阱。我描述的是一系列事件,但也许我真的在发布命令?毕竟,发布event.post-posted事件的原因是触发在帖子服务中创建事件。另一方面,如果不监听这些事件,应用程序不会崩溃;事件只会有非常干燥的帖子。


你对这种方法有什么想法?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接