«
大型网站的用户登录系统如何设计?

时间:2022-7   


在软件系统设计中,如何让应用能够在各种环境中安全高效的访问是个复杂的问题,这个问题的背后是一系列软件设计时需要考虑的架构安全问题:架构安全性 | 凤凰架构

在漫长的架构演进历史中,业界对这些问题已经有很成熟的解决方案。在架构安全这块,最好的是遵循技术标准与最佳实践,尽可能不重复造轮子或“创新”。下面这个思维导图就是针对这些问题的常见的技术标准及方案:

在研究分布式系统的认证和授权问题前,让我们回到单体架构的时代,看看在单体架构上这些问题是如何被解决的。

单体系统

认证

认证主要解决 你是谁 的问题,从方式上来看有以下三种:认证 | 凤凰架构

在单体系统时代,认证方式一般是在通信信道上开启 HTTPS,在通信协议上利用 HTTP Basic/Digest/Bearer/HOBA/OCRA 等方式并在通信内容上结合表单或 TOTP 等的认证组合方式。这样可以从通信的不同阶段获得相应的安全保证。

如果想对基于 HTTP 协议的认证方式做进一步的了解,可以参考这两篇文章:

  1. 认证 | 凤凰架构
  2. 细说API - 认证、授权和凭证 - Thoughtworks洞见

单点登录(SSO

认证的一个常见应用场景是单点登录。单点登录主要解决了一个一次登录访问多个独立应用的问题。在单点登录方案出现之前,每个应用都需要独立登录维持各自的会话。相关的技术方案已经很成熟,主要有以下:

授权

授权主要解决 你能做什么 的问题,从方案上来说有以下几种:

如果想对授权做进一步的了解,可以参考这篇文章:

  1. 授权 | 凤凰架构

凭证

凭证是为了解决在认证授权后如何承载认证授权信息的问题。在单体应用时代,主流的解决方案是基于 HTTP 协议的 Cookie-Session 机制为代表的服务端状态存储技术。

由于 HTTP 协议本身是无状态的,要维持一个会话(Session),而不是每次访问都重新认证授权,需要客户端也就是浏览器通过 Cookie 来存储服务器端返回的一个凭证信息,这个凭证信息一般是一串随机的字符串,用来代表用户此次的会话标识。每次请求浏览器都会在 HTTP Header 中携带这个 Cookie 信息,应用拿到这个会话标识后从内存或缓存(Cache)中查询出用户的信息,这样就定位到了具体的用户,实现了会话的维持。

这套古老的方案存在以下先天优势:凭证 | 凤凰架构

一切都很美好,直到我们来到了分布式系统时代。

分布式系统

分布式系统与单体系统的一大区别就是状态管理。分布式系统通过把单体系统中有状态的部分转移到中间件中去管理,从而很容易做到水平扩容,提高系统峰值处理能力。在架构认证和授权部分,分布式和单体并没有什么不同,唯独有变化的在持有状态的凭证部分。

我们知道单体应用在服务端管理用户会话信息,客户端只持有会话标识。如果服务端要将此用户会话状态转移出去有两种处理思路:

为什么在分布式系统中共享状态就这么困难?这是因为分布式系统中有一个不可能三角的理论:CAP。这个理论简单的理解就是因为在分布式系统中,因为网络无法做到绝对的可靠(分区容错性:Partition Tolerance),只能在一致性(Consistency)和可靠性(Availability)间选择一个。
比如上述的三种服务端方案其实都是牺牲了 CAP 的某个方面。比如第一种中心化存储方案我们放弃了中心化存储的分区容错性,一旦其网络分区,整个集群都会不可用。第二种会话复制方案我们牺牲了可用性,当节点在同步会话数据时,整个服务会短暂的不可用。第三种会话粘滞方案我们牺牲了一致性,一旦某个节点宕机,整个集群的数据会因该节点的数据丢失而达到不一致的状态。

JWT

如果你对 JWT 不了解,可以先看这两篇:

  1. JWT | 凤凰架构
  2. The Hard Parts of JWT Security Nobody Talks About

由于 JWT 的 Payload 并未做过多限制,所以很容易产生滥用的问题,并且带来很多误解。 比如下面的一些问题:

相信看了上述的一些问题,你对 JWT 的“简单、安全”有了新的理解。这还没完,JWT 还有以下一些 Cookie-Session 没有的问题:

JWT 解决了 Cookie-Session 方案在分布式系统中因 CAP 的限制而带来的问题,但同时也带来了一些新的问题。所以并不能说 JWT 就是 Cookie-Session 在分布式系统中的完美替代。

那么 JWT 的最佳使用场景到底是什么?这篇 Stop using JWT for sessions 给出了以下的结论:JWT 更适合作分布式系统中的一次性令牌使用。分布式系统继续使用 Cookie-Session 做会话管理,但可以在认证鉴权后生成 JWT 做分布式系统内部服务调用间的一次性令牌。

让我们通过一个例子来理解下在分布式系统下的认证授权场景。

一个例子

  1. 用户通过 HTTPS 访问我们的应用。当请求发送至微服务网关层(Gateway),网关检测 HTTP Header 中的 Cookie 发现没有 SESSIONID 这个键值对,重定向至 SSO 登录页面。

  2. 用户通过 SSO 登录我们的应用。

  3. 用户信息存放至 AD/LDAP 等系统中。管理员提前给用户配置好角色权限。

  4. SSO 集成方案我们选择 OIDC。OIDC 集成了 AD/LDAP,当用户提供正确的用户名和密码后,SSO 重定向至网关。

  5. 网关生成了 SESSIONID 键值对并通过 HTTP Set-Cookie 响应给用户浏览器设置了此 Cookie。

    • 浏览器重新发起带  SESSIONID  Cookie 的请求。网关经过查询其缓存或中间件(如将会话信息存放至 Redis)中的 Session 信息确认了用户的身份信息。之后网关请求 Auth 服务利用其私钥签名生成 JWT 凭证,JWT Payload 中可以存放一部分用户信息和角色信息,这些信息可以从中间件中或 AD/LDAP 中查询出。

    • 网关之后将此 JWT 凭证通过反向代理转发至内部的 BFF 服务,之后请求到达内部的领域微服务。

    • 各领域微服务接受到请求后,先从 HTTP Header 中拿出 JWT 凭证。

  6. 在执行真正的业务逻辑前,先利用之前定时从 Auth 服务中同步获取的公钥。

  7. Auth 服务通过一个类似 https:///.well-known/jwks.json 的 API 提供 JWT 公钥的分发。关于 .well-known 前缀,可阅读 RFC 5785 做进一步了解。在 jwks.json 文件中,我们可以找到 JWK 或 JSON Web Key,这是我们用来验证签名的公钥。

  8. 校验 JWT 这块逻辑属于微服务共有的部分,一般可以开发一个 SDK 包来做这个通用的工作。为了提高性能,可使用缓存技术,定时从 Auth 中同步公钥。

    • 获取到公钥后验证成功后拿出 JWT Payload 即可获取到用户信息和角色权限。

全部流程就是这样,我们得到了以下的一些好处:

架构总是在演进,也许分布式系统中很多问题我们还没完全解决,就来到了云原生时代。

云原生系统

如果你对云原生应用开发还不了解的话,可以先看看我这篇 K8S 云原生应用开发小记。云原生系统其实并不是什么后分布式系统时代。它们两者都是为了解决不同场景的问题而出现的解决方案。

在认证授权这块,云原生系统的优势在于可以通过 服务网格(Service Mesh) 做一些业务系统中通用的切面工作,比如我们在分布式系统中遇到的校验 JWT 的 SDK 其实就可以放入服务网格中的边车(Sidecar)去实现,让业务应用更专注特定领域的业务。

由于这篇文章并不主要讨论云原生,对这部分感兴趣的可以参考以下两篇文章做进一步了解:

  1. Service Mesh架构下的认证与授权
  2. Authentication sidecar

总结

由于篇幅及能力限制,这篇文章我只能从高层次梳理在不同架构演进中认证、授权及凭证这些和架构安全相关的技术的发展过程。由于这些技术涉及了大量的技术标准及实践,很难在一篇文章中对这些技术做详尽的分享,更无法去分享如何实现。但有了这些理论支持和最佳实践,希望能让你在实现的过程中多了一个指引。如果你想进一步了解,可参考文章中的参考文章链接。

最后,技术总是在不断的发展,但并不是新技术总比老技术“先进”。正如文章中对 Cookie-Session 与 JWT 的分析对比,技术方案总是充满了各种  Trade-off 。而作为一个工程师,我们能做的就是认清这些技术的历史背景及局限性,选择最适合项目需求的技术方案。