微服务单点登录的实现方案

会话的基础概念

HTTP 无状态协议

Web 应用采用 Browser/Server 架构,HTTP 作为通信协议。HTTP 是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求 / 响应对之间没有任何联系。

但这也同时意味着,任何用户都能通过浏览器访问服务器资源。如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然 HTTP 协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。

会话机制的概述

浏览器第一次请求服务器时,服务器会创建一个会话(Session),并将会话的 ID 作为响应的一部分发送给浏览器。浏览器接收到会话 ID 后,将其存储起来(通常存储在 Cookie 中),并在后续的第二次、第三次等请求中自动带上这个会话 ID。服务器从请求中取得会话 ID,就能识别出这些请求是否来自同一个用户。这样一来,尽管 HTTP 本身是无状态的,但通过会话 ID,服务器就能将后续请求与第一次请求关联起来,从而实现对用户状态的跟踪。

服务器在内存中保存会话的两种方式:

  • 请求参数
  • Cookie

将会话 ID 作为每一个请求的参数,服务器接收请求自然能解析参数获得会话 ID ,并借此判断是否来自同一会话,很明显,这种方式不靠谱。那就浏览器自己来维护这个会话 ID 吧 ,每次发送 HTTP 请求时浏览器自动发送会话 ID ,Cookie 机制正好用来做这件事。Cookie 是浏览器用来存储少量数据的一种机制,数据以 key /value 形式存储,浏览器发送 HTTP 请求时自动携带 Cookie 信息。Tomcat 会话机制当然也实现了 Cookie,访问 Tomcat 服务器时,浏览器中可以看到一个名为 JSESSIONID 的 Cookie,这就是 Tomcat 会话机制维护的会话 ID ,Cookie 的请求响应过程如下图。

登录状态的概述

有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为 “已授权” 或者 “已登录” 等等之类的状态,既然是会话的状态,自然要保存在会话对象中,Tomcat 在会话对象中设置登录状态如下:

1
2
HttpSession session = request.getSession();
session.setAttribute("isLogin", true);

用户再次访问时,Tomcat 在会话对象中查看登录状态

1
2
HttpSession session = request.getSession();
session.getAttribute("isLogin");

实现了登录状态的浏览器请求服务器模型如下图所示:

每次请求受保护资源时,服务器都会检查会话对象中的登录状态,只有 isLogin=true 的会话才能访问,登录机制因此而实现。

多系统登录的复杂性

系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样:

Web 系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论 Web 系统内部多么复杂,对用户而言,都是一个统一的整体;也就是说,用户访问 Web 系统的整个应用群与访问单个系统一样,登录 / 注销只要一次就够了。

虽然单系统的登录解决方案很完美,但对于多系统应用群已经不再适用了,为什么呢?回顾一下 Cookie,通常 Cookie 用来在浏览器与服务器之间维护会话状态。但 Cookie 是有限制的,这个限制就是 Cookie 的域(通常对应网站的域名),浏览器发送 HTTP 请求时会自动携带与该域名匹配的 Cookie ,而不是自动携带所有 Cookie。

既然这样,为什么不将 Web 应用群中所有子系统的域名统一在一个顶级域名下,例如 baidu.com,然后将它们的 Cookie 域(Domain)设置为 *.baidu.com,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享 Cookie 方式。然而,可行不代表好,基于 Cookie 的登录态共享方案存在众多局限,比如:

  • (1)应用群的域名需要统一

    • 核心原因:浏览器的 Cookie 受 Domain / Path 等属性限制,天然存在跨域问题。
    • 解决方案:
      • 通过统一主域名(如 *.baidu.com)来共享 Cookie;
      • 或在入口层使用 Nginx / API 网关 做统一接入,避免前端直接跨域访问。
  • (2)应用群各系统的会话机制需要统一

    • 问题说明:
      • 不同 Web 技术栈默认使用的 Session Cookie 名称和机制不同(例如:Tomcat 使用 JSESSIONID);
      • 因此,直接共享 Web 容器 Session Cookie 的方式无法支持跨语言、跨平台系统(如 Java、PHP、Python)之间的登录态共享。
    • 解决方案:
      • 不再依赖 Web 容器自身的 Session 机制;
      • 统一使用自定义的 Cookie Key(如 UUID);
      • UUID → Token / 用户登录态信息 存储到 Redis 等集中式存储中;
      • 应用群内各系统通过该 UUID 从 Redis 中获取真实的认证信息,从而实现统一登录态。
  • (3)Cookie 本身存在安全风险

    • 问题说明:
      • Cookie 会被浏览器自动携带,容易受到 CSRF(跨站请求伪造)攻击。
    • 解决方案:
      • Cookie 中只存储无敏感意义的标识信息(如 UUID);
      • 真实的认证信息和权限数据仅保存在服务端(如 Redis);
      • 配合 HttpOnlySecureSameSite 等属性提升整体安全性。

因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录(SSO)。

总结

基于 Cookie 的登录态共享方案,需要统一域名、统一会话标识机制,并避免在 Cookie 中存储敏感信息,通常通过「Cookie + Redis」的方式实现跨语言系统的单点登录。

单点登录的基础概念

单点登录的概述

单点登录(Single Sign-On,SSO)是一种身份认证机制,指用户只需登录一次,就可以在多个相互关联的系统或应用中无需重复登录即可访问受保护资源,包括单点登录和单点注销两部分。简而言之:单点登录就是通过统一认证中心,让用户在多个系统之间实现 “一次登录,全站通行”。

  • 单点登录的核心思想

    • SSO 的核心在于统一身份认证:
      • 用户的账号和密码只在统一认证中心校验一次
      • 登录成功后,由认证中心颁发登录凭证(如 Token / Cookie)
    • 其他系统通过校验该凭证来确认用户身份,而不再要求用户再次登录
  • 单点登录的优点

    • 提升用户体验,避免频繁输入账号密码
    • 减少密码暴露次数,提高安全性
    • 统一账号管理,降低系统维护成本
  • 单点登录的典型应用场景

    • 企业内部系统(OA、CRM、ERP 共用一套账号)
    • 互联网平台(登录一次即可访问多个子系统)
    • 微服务架构中的多业务系统

单点登录的工作原理

相比于单系统登录,单点登录(SSO)需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,认证中心验证用户的用户名密码没问题后,会创建授权令牌,在接下来的请求跳转过程中,授权令牌作为参数携带给各个子系统,子系统拿到授权令牌,即得到了授权,可以借此创建局部会话,局部会话的登录方式与单系统的登录方式相同,即基于 Web 容器(比如 Tomcat)的 Session 来存储用户的登录信息或者 Token。这个过程,也就是单点登录的工作原理,用下图说明:

下面对上图简要描述:

  • 用户访问系统 1 的受保护资源,系统发现用户未登录,跳转至 SSO 中心,并将自己的地址作为参数
  • SSO 认证中心发现用户未登录,将用户引导至登录页面
  • 用户输入用户名密码提交登录申请
  • SSO 认证中心校验用户信息,创建用户与 SSO 认证中心之间的会话,称为全局会话,同时创建授权令牌
  • SSO 认证中心带着令牌跳转回最初的请求地址(系统 1)
  • 系统 1 拿到令牌后,去 SSO 认证中心校验令牌是否有效
  • SSO 认证中心校验令牌,返回令牌有效,注册系统 1
  • 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  • 用户访问系统 2 的受保护资源
  • 系统 2 发现用户未登录,跳转至 SSO 认证中心,并将自己的地址作为参数
  • SSO 认证中心发现用户已登录,跳转到系统 2 的地址,并附上令牌
  • 系统 2 拿到令牌后,去 SSO 认证中心校验令牌是否有效
  • SSO 认证中心校验令牌,返回有效,注册系统 2
  • 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与 SSO 认证中心及各个子系统建立会话,用户与 SSO 认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话。局部会话建立之后,用户访问子系统受保护资源将不再通过 SSO 认证中心,全局会话与局部会话有如下约束关系:

  • 局部会话存在,全局会话一定存在
  • 全局会话存在,局部会话不一定存在
  • 全局会话销毁,局部会话必须销毁

全局会话与局部会话的说明

  • 全局会话通常使用 Redis 集中式缓存来存储用户的登录信息或者 Token。
  • 局部会话通常使用 Web 容器(比如 Tomcat)的 Session 来存储用户的登录信息或者 Token。

单点注销的工作原理

单点注销是指在一个子系统中注销,所有子系统的会话都将被注销,用下面的图来说明。

SSO 认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作

  • 用户向系统 1 发起注销请求
  • 系统根据用户与系统 1 建立的会话 ID 拿到令牌,向 SSO 认证中心发起注销请求
  • SSO 认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  • SSO 认证中心向所有注册系统发起注销请求
  • 各注册系统接收 SSO 认证中心的注销请求,销毁局部会话
  • SSO 认证中心引导用户至登录页面

单点登录的部署架构

单点登录涉及 SSO 认证中心与多个子系统,子系统与 SSO 认证中心需要通信以交换令牌、校验令牌及发起注销请求,因而子系统必须集成 SSO 的客户端;SSO 认证中心则是 SSO 服务端,整个单点登录过程实质是 SSO 客户端与 SSO 服务端通信的过程,用下图描述:

  • SSO Client(客户端)

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

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

SSO 服务端与 SSO 客户端通信方式有多种,包括 HttpClient、WebService、RPC、RESTful API 等。

单点登录的实现方案

在 Spring Cloud 微服务架构中,实现单点登录(SSO)常见有以下几种方案。

基于 Session + 统一认证中心

  • 思路:

    • 独立一个认证中心(Auth Server)
    • 用户登录后,Session 信息存储在 Redis 等集中式缓存
    • 各微服务共享 Session 信息
  • 优点:

    • 实现简单,适合传统项目
    • 改造成本低
  • 缺点:

    • 强依赖 Session,不够 “微服务化”
    • 扩展性和跨端能力较弱
  • 适用场景:

    • 内部系统、流量不大、历史系统改造

基于 JWT(主流方案)

  • 思路:

    • 登录成功后由认证服务(Auth Server)签发 JWT
    • 客户端将 JWT 存储在本地持久化存储中(如浏览器的 LocalStorage 或者 IndexedDB)
    • 客户端每次请求在 HTTP Header 中携带 JWT
    • 网关或微服务对 JWT 进行校验
  • 优点:

    • 无状态,天然支持分布式
    • 性能好,扩展性强
    • 适合前后端分离、移动端
  • 缺点:

    • Token 失效控制较复杂(需要使用短期 Access Token + Refresh Token,或在 Redis 中维护 Token 黑名单)
  • 适用场景:

    • Spring Cloud + Gateway 的主流方案

基于 Gateway 统一认证

  • 思路:

    • 使用 Auth Server 作为统一认证中心
    • 登录成功后签发 JWT / OAuth2 Token
    • 所有请求先经过 Gateway
    • 在网关层完成 Token 校验和权限控制
    • 微服务只处理业务逻辑
  • 优点:

    • 认证和授权逻辑集中,职责清晰
    • 微服务完全解耦,无状态
    • 易扩展,支持第三方登录
  • 缺点:

    • 网关成为流量入口,压力较大
    • 需要配合限流、熔断、缓存等机制
  • 适用场景:

    • 微服务数量多
    • 前后端分离
    • 对安全性和扩展性要求较高的系统

基于 OAuth2 / OpenID Connect(企业级标准)

  • 思路:

    • 使用 OAuth2 作为授权框架
    • OpenID Connect 提供身份认证
    • 可结合 Spring Authorization Server / Keycloak
  • 优点:

    • 标准化、扩展性强
    • 支持第三方登录(微信、钉钉、GitHub)
    • 安全性高
  • 缺点:

    • 实现复杂,学习成本高
  • 适用场景:

    • 中大型系统
    • 需要对接第三方身份体系

基于 CAS / LDAP(传统企业方案)

  • 思路:

    • 使用 CAS 或 LDAP 统一身份认证
    • Spring Cloud 作为业务系统接入
  • 优点:

    • 成熟稳定
    • 适合企业内部账号体系
  • 缺点:

    • 与微服务结合较重
    • 灵活性一般
  • 适用场景:

    • 政企、银行、事业单位

Token 的存储方式

在单点登录(SSO)体系中,通常会使用两类 Token,包括 Access Token 和 Refresh Token,两者在用途、生命周期和安全级别上存在本质区别。

对比项 Access TokenRefresh Token
有效期短(分钟级)长(天 / 周)
使用频率高频低频
安全级别较低极高
一旦泄露影响有限风险极大
使用场景访问受保护资源换取新的 Access Token

总结

  • Access Token:用于访问受保护的资源,生命周期较短,随每次请求携带,一旦泄露影响相对可控。
  • Refresh Token:用于在 Access Token 过期后换取新的 Access Token,生命周期较长,不直接访问业务资源,安全级别要求更高。

Access Token

Access Token 建议存储在浏览器的 LocalStorage 和 IndexedDB。

  • (1) 存储在浏览器的 LocalStorage

    • 特点:
      • API 使用简单
      • 浏览器关闭、重启都不会丢
      • 前后端分离项目中使用最广泛
    • 优点:
      • 实现简单
      • 性能较好
      • 支持所有主流浏览器
    • 缺点:
      • 容易被 XSS(跨站脚本攻击)读取
    • 适用场景:
      • 企业内部系统
      • 风险可控的 B 端项目
  • (2) 存储在浏览器的 IndexedDB

    • 特点:
      • 浏览器原生数据库
      • 浏览器关闭、重启都不会丢
      • 不容易被简单 XSS(跨站脚本攻击)直接读取
    • 优点:
      • 安全性高
      • 适合存敏感数据
    • 缺点:
      • API 复杂
      • 使用成本高
    • 适用场景:
      • 金融、政企
      • 对安全要求极高的系统

提示

  • 用户登录成功后由认证服务签发 JWT,客户端将 JWT 存储在本地(如浏览器的 LocalStorage 或 IndexedDB),后续请求通过 HTTP Header 携带 JWT,网关或微服务对 Token 进行校验。
  • 由于 JWT 存储在浏览器的 LocalStorage 是不安全的,因此实际项目中通常使用短期 Access Token + Refresh Token,并结合 CSP、防 XSS(跨站脚本攻击)手段。

Refresh Token

Refresh Token 建议存储在浏览器的 Cookie(HttpOnly)。

  • 存储在 Cookie(HttpOnly)
    • 关键点:
      • Cookie 必须设置 ExpiresMax-Age 属性
      • 否则默认是会话 Cookie,浏览器关闭即失效
    • 优点:
      • 天然支持持久化
      • 可设置 HttpOnly 属性防止 JavaScript 读取
    • 缺点:
      • 跨域配置复杂
      • 存在 CSRF(跨站请求伪造)攻击风险
      • 不符合微服务 Header 标准用法
    • 适用场景:
      • 传统 Web 系统
      • Refresh Token 存储(常见)

Cookie(HttpOnly)会不会由浏览器自动携带给服务端?

  • Cookie(HttpOnly)在客户端发送请求时会由浏览器自动携带给服务端。
  • Cookie 的 HttpOnly 属性只是禁止 JavaScript 访问 Cookie,不影响浏览器在满足 Domain、Path、SameSite 等条件下自动携带给服务端。

为什么 Refresh Token 更适合存储在 Cookie(HttpOnly)?

  • Refresh Token 的有效期较长,一旦泄露风险极高,因此不适合存放在浏览器的 LocalStorage 和 IndexedDB 等可被 JavaScript 读取的位置。
  • 使用 Cookie(HttpOnly )可以防止 XSS(跨站脚本攻击)窃取 Refresh Token,同时浏览器又能自动携带 Refresh Token,用于刷新 Access Token,是安全性和可用性的最佳平衡。

参考资料