抛砖引玉

在文章开始前,先看看一个常见的情况 👇

在集团内进行开发时,通常会遇到不同组之间的合作,如果是同一个组的前后端,因为交互请求都是在同一个 「域」 内发生的,所以一般不会存在跨域问题。但如果未做处理,直接从 a.alibaba.com 请求 b.alibaba.com 的接口,就会出现跨域的问题,这是因为浏览器对于不同域请求的限制问题,其实跨域的问题很好解,只要设置了正确的请求头即可,具体的可以参考我的这篇文章 👉《一次跨域问题的分析》

但这是访问不需要登录的接口,那如果是从 a.alibaba.com 访问 b.alibaba.com 下的一个需要登录的接口呢?又该如何解决呢?

下文以 A 站点指代 a.alibaba.com,B 站点指代 b.alibaba.com

单系统登录

对于一个 web 应用来说,通信协议通常是 HTTP 协议,该协议是无状态的,也就是说,在请求与请求之间是不会产生关联的。这也就意味着,任何用户都能通过浏览器访问服务器资源,且不会打扰到其他用户。如下图所示 👇

如果想要保护某些资源,比如一些珍贵的学习资料,那就必须限制浏览器的请求,对于服务端来说就是要知道发出这个请求的人是谁,也即让请求变得有「状态」,只不过既然 HTTP 协议无状态,那就让浏览器和服务器之间共同维持一个状态吧,而这就是最常见的——会话机制。

在会话机制中,最重要的就是 Cookie 和 Session 了,Session 好理解,服务端保存的用来维护某一个用户的状态,浏览器只需用某种方式记录下这个会话的 ID 然后之后每次请求携带即可,想必有小伙伴会发出疑问了,既然是给浏览器携带参数,那么直接在请求参数里携带不是最简单的吗?

的确,将会话 id 作为每一个请求的参数,服务器接收请求自然能解析参数获得会话 id,并借此判断是否来自同一会话,这个思路当然是可以的,只是这种做法的缺点也十分明显,就是请求的 URL 会变得非常长,隐秘性也很差。

而 Cookie 是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,并且浏览器发送 http 请求时自动附带 Cookie 信息。此时,有 Cookie 参与的登录请求的流程就变成了下面这样 👇

Cookie 和 Session 的使用原理基本如此,至于这么设置 Cookie,怎么通过 Cookie 校验 Session 就不是本文要说的内容了。有兴趣的可以查阅相资料。

多系统登录

不知道你有没有留意过,如果你在浏览器中登录了百度网盘之后,再打开百度贴吧时就会发现此时你已经登录成功了,这种情况就是本节要说的多系统登录了。

随着单系统的蓬勃发展,web 系统由单系统发展成多系统组成的应用群,换一种说法就是 「生态」

随着集团的规模不断增加,系统越来越多,复杂性随之巨增,正常情况下,这种复杂性应该由系统内部承担,而不是用户。因为对于一个好的系统应该是,无论 web 系统内部多么复杂,对用户而言,都应该是一个统一的整体,也就是说,用户访问 web 系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。

如下图所示 👇

单系统登录解决方案的核心是 Cookie,Cookie 携带会话 id 在浏览器与服务器之间维护会话状态。但 Cookie 是有限制的,这个限制就是 Cookie 的域(通常对应网站的域名),浏览器发送 http 请求时会自动携带与该域匹配的 Cookie,而不是所有 Cookie,因此,你在请求淘宝的时候是绝对不会携带上只能在百度域下生效的 Cookie 的。

SSO

单点登录全称 Single Sign On(以下简称 SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括**「单点登录」「单点注销」**两部分。

登录

不同于单系统登录,单点登录需要引入一个独立的登录中心,每个系统可能并不会提供登录入口,所有的登录操作都是通过独立的登录中心实现的。由于这一流程较多,这里以时序图的方式来讲解。

一个单点登录的时序,简化后如上图所示。文字流程如下:

  1. 浏览器访问 A 站点时由于未登录,跳转至 SSO 登录中心
  2. 完成在 SSO 登录中心的登录后,登录中心创建一个全局会话
  3. SSO 登录中心返回一个 tikect 给 A 站点,并在 SSO 登录中心记录下 A 站点
  4. 下次访问 A 站点时携带包含了这个 ticket 的 Cookie,A 站点收到请求并创建针对 A 站点的局部会话,给用户返回已登录的 A 站点页面

此时如果用户想要访问 B 站点,那么流程如下所示:

  1. 浏览器访问 B 站点显示未登录,跳转至 SSO 登录中心
  2. SSO 登录中心发现用户已经在登录中心完成登录
  3. SSO 登录中心返回一个 tikect 给 B 站点
  4. B 站点拿到 ticket 后再请求一次 SSO 站点,验证无误后写入 ticket 到 Cookie 中,此时 SSO 登录中心记录下 B 站点
  5. 下次访问 B 站点时携带包含了这个 ticket 的 Cookie,B 站点收到请求并创建针对 B 站点的局部会话,给用户返回已登录的 B 站点页面

注销

注销相较于登录就简单了许多,假设我在 A 站点注销了,那么 SSO 中心接收到注销请求后,直接销毁保存在 SSO 系统的全局会话,然后向所有注册系统发出注销请求,各系统在接受到注销请求后,分别销毁自己的局部会话即可。篇幅原因,这里就不着重笔墨来写了。感兴趣的同学可以自己画一下时序图。

实现原理

SSO 采用的是 Client/Server 模式,为了实现 SSO,A、B 站点都需要接入 sso-client 包,SSO 登录中心需要实现 sso-server 包。这两个包的主要功能如下。

sso-client

  • 拦截子系统未登录用户请求,跳转至 sso 认证中心
  • 接收并存储 sso 认证中心发送的令牌
  • 与 sso-server 通信,校验令牌的有效性
  • 建立局部会话
  • 拦截用户注销请求,向 sso 认证中心发送注销请求
  • 接收 sso 认证中心发出的注销请求,销毁局部会话

sso-server

  • 验证用户的登录信息
  • 创建全局会话
  • 创建授权令牌
  • 与 sso-client 通信发送令牌
  • 校验 sso-client 令牌有效性
  • 系统注册
  • 接收 sso-client 注销请求,注销所有会话

在了解了 sso-client 和 sso-server 的主要功能后,编码实现就容易的多了,互联网上已经有很多相关的资料了,这里就不展开说了。

登录态保护

在了解了 SSO 之后,我们知道,在 A 站点登录后,下次再请求 A 站点就会携带诸如「A_USER_COOKIE」的一个 Cookie 值。在 B 站点登录后,下次再请求 B 站点就会携带诸如「B_USER_COOKIE」的一个 Cookie 值。

结合着 SSO 的原理,我们再回到本文一开始的问题,如果想要从 A 站点跨域请求 B 站点一个需要登录的接口,不可避免的一定要重定向到 SSO 站点。因为从 A 站点发出到 B 站点的请求携带的是来自 A 站点的 Cookie,B 站点是无法直接解析的。(这里有点绕,理解一下)

为了解决这个问题,可以从前后端两个方式去着手,提供一下思路。

  1. 前端方向,捕捉重定向的错误单独处理,只是如果重定向过程中有可能会出现跨域问题。
  2. 后端方向,通过某种途径,可以让 B 站点的后端解析来自 A 站点中包含的已经登录过 SSO 的 Cookie

所谓根域即不同应用共享的域名部分,比如 a.alibaba.com 和 b.alibaba.com,根域就是 alibaba.com。根域 token 是各个子域名应用共用的 Cookie,每个子域名应用的请求都可以接收到这个 Cookie 参数,但是每个应用是否能用这个 Cookie 来建立登录态,则需要满足不同的条件。

这个条件是由分发此根域 Token 的 SSO 中心规定。

根域 token 的使用时序

时序如上图所示,这样的好处是,就算我在 A 站点携带的是 A 站点的 Cookie,也可以去访问 B 站点一个需要登录的接口。因为 A 站点的 Cookie 中有一个全局的根域 token,B 站点在将请求发送到 SSO 校验时只要有这个根域 token 即可返回对应的用户信息了。

根域 token 的优势

根域 token 的消费端在应用侧,由 SDK 封装这部分逻辑,根据根域 token 建立登录态。对于原先已经接入了 sso-client 的 B 应用只需升级支持根域 token 的版本即可。

这样做的好处是,部分没有接 SDK 的应用也可以通过该 token 完成登录校验。比如 A 站点的后端应用没有更新 sso-client 也无妨,因为 sso-server 升级后会将根域 token 下发。A 应用只需将根域 token 携带给升级后的 B 应用即可。

这样在 A、B 两个站点的前后端开发者之前真正需要做出改变的就只有 B 站点的服务端开发人员了,极大的减少了沟通带来的低效率与撕逼。(中间件的升级独立与 A、B 站点的开发之外)

根域 token 的问题

从上述表述发现,根域 token(即共享 Cookie)的确是一个可行的解决办法,但这种方案有很多限制:

  1. 应用群域名统一,基本限制了必需是同一集团下的域
  2. 共享 Cookie 无法跨语言,即服务端技术栈需统一
  3. Cookie 不安全

所以,对于大多数情况,共享 Cookie 都无法解决统一登录的问题。

只是,眼尖的小伙伴应该意识到了上述三个问题,对于集团内网来说应该都不是问题。为什么这么说?我们逐一分析下。

第一点,都是集团内网的网站,因此所有的站点都是“*.alibaba.com”,域名统一这一点不存在限制。其二,集团内技术栈统一。其三,大多数系统都是内网使用,几乎不存在 Cookie 不安全的情况。

只不过浏览器针对一个域名(根域及子域)下的 Cookie 数量有数量限制,超过则会按规则逐出部分 Cookie,DevExpress网站给出了一些浏览器对于 Cookie 的限制,如下图所示 👇

通俗的说就是,对于 chrome 浏览器而言,每个域名的上限是180个 Cookie,而这 180 个域名是针对**「根域名」**的,也即:a.alibaba-inc.com, b.alibaba-inc.com 和*.alibaba-inc.com 共享这 180 个限制。

在得知了这个限制之后,我们也就理解了为什么共享 Cookie 的方案即使是在集团内也有诸多的限制了。

这里提一下,对于超出数量的 Cookie 的逐出规则,我在查阅资料的过程中发现一些博客写的是 LRA(least-recently accessed),最近最少使用算法。的确,IETF 标准的RFC-6265对 Cookie 逐出策略的规范确实是 LRA 算法。

IETF 是国际互联网工程任务组的简称,IETF 的主要任务是负责互联网相关技术标准的研发和制定,是国际互联网业界具有一定权威的网络相关技术研究团体。

不过我们都知道,规矩制定的再好也得看实现规矩的人是什么样的。我们来看看浏览器的霸主,chrome 是如何实现的。下图是从 chromium 项目源码中截取的部分片段,地址如下 👇

https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/cookies/cookie_monster.cc?spm=ata.21736010.0.0.75512bb3YUmXb0&file=cookie_monster.cc

简单翻译一下源码中的注释就是,chromium 在源码中把 Cookie 分为了 6 个优先级,再移除 Cookie 的时候先按照优先级进行排序,然后再依次 LRA 算法删除。

// 1.  Low-priority non-secure cookies.
// 2.  Low-priority secure cookies.
// 3.  Medium-priority non-secure cookies.
// 4.  High-priority non-secure cookies.
// 5.  Medium-priority secure cookies.
// 6.  High-priority secure cookies.

我们以www.taobao.com为例,打开控制台-应用程序-Cookie,下图中的最后一栏就是Cookie的优先级。

OAuth 和 SSO 之间的关系

想到统一登录,相信很多人都会想到手机上使用的微信登录、QQ 登录等登入第三方网站的案例。

但事实上,上述这些案例涉及到的是一个名为 OAuth 的协议。只是为用户资源的授权提供了一个安全的、开放而又简易的标准。OAuth 2.0 为客户端开发者开发 Web 应用,桌面端应用程序,移动应用等提供特定的授权流程。这一点和 SSO 有很大的区别。

通俗的讲,OAuth 是为解决不同公司的不同产品实现登录的一种简便授权方案,通常这些授权服务都是由大客户网站提供的,如腾讯,支付宝,淘宝等。而使用这些服务的客户可能是大客户网站,也可能是小客户网站。使用 OAuth 授权的好处是,在为用户提供某些服务时,可减少或避免因用户懒于注册而导致的用户流失问题。

SSO 通常处理的是同一个公司的不同应用间的访问登录问题。如企业应用有很多业务子系统,只需登录一个系统,就可以实现不同子系统间的跳转,而避免了登录操作。

OAuth 与 SSO 的应用场景不同,虽然可以使用 OAuth 实现 SSO,但并不建议这么做。不过,如果 SSO 和 OAuth 结合起来的话,理论上是可以打通各个公司的各个不同应用间的登录,但现实往往是残酷的。

毕竟,这是一个各家都在尽力打造**「生态」**护城河的互联网时代。