单点登录(SSO)把多个业务系统的认证集中起来,这本身是件好事。但当这些系统分布在不同的主域名下,比如 company.com、company-cdn.com 和 company-shop.com,Session 的维持就变成了一个实实在在的工程难题。浏览器的同源策略直接把跨域 Cookie 的路堵死了,这意味着你在 A 站登录后,B 站根本读不到 A 站的 Session ID,用户会被反复踢回登录页。这不是配置失误,而是浏览器安全模型和集中式会话管理之间的结构性冲突。

跨域 SSO 的 Session 同步困境到底卡在哪

问题核心在于 Session 的标识符通常存储在 Cookie 里,而 Cookie 默认不跨域。即使你把 SSO 认证中心部署好了,用户在 passport.company.com 登录成功后,浏览器里种下的 Session Cookie 只属于 passport.company.com 这个域。当用户跳转到 shop.company.com,浏览器根本不会把这个 Cookie 带过去,后端拿不到 Session ID,自然认为用户未登录。你可能会想用 document.domain 或者 postMessage 去传递,但这些方案要么受限严重,要么架构上把安全性降得很低。真正的挑战不是“能不能传过去”,而是在不破坏安全隔离的前提下,让多个独立域名的应用共享同一份登录态。

基于认证中心的令牌中转模式

这是目前最成熟的方案,核心思路是不再让各个业务系统直接读取 Session,而是通过一个统一的认证中心来做令牌验证。用户访问 shop.company.com 时,如果没有本地 Session,后端直接 302 重定向到 passport.company.com。认证中心检查自己的 Session Cookie,如果有效,就生成一个一次性的授权码,拼在 URL 上跳回 shop.company.com。shop.company.com 拿到授权码后,用服务端的方式向认证中心换取 Access Token 和用户信息,然后在自己域下建立独立的 Session。整个过程里,跨域传输的不是 Session ID,而是一个短时效、可撤销的授权码。每个业务系统维护自己的会话,认证中心只负责签发令牌,这样即使某个子站被攻破,攻击者也拿不到其他站的会话凭证。

Token 的无状态方案与 Session 的取舍

JWT 这类自包含令牌经常被拿来解决跨域问题。令牌本身包含用户信息和签名,各个系统只要验证签名就行,不需要去中心存储查 Session。但这里有一个容易被忽略的坑:令牌的吊销问题。一旦签发了 JWT,在过期时间之前,它是始终有效的。如果用户修改了密码或者管理员封禁了账号,你怎么让已经发出去的 JWT 立刻失效?要么维护一个全局的黑名单,这又回到了集中存储的老路;要么把令牌有效期设得极短,用 Refresh Token 频繁续期,但这会增加认证中心的调用频率和延迟。所以,对于强安全场景,完全无状态的 JWT 并不合适。更务实的做法是混合架构:用 JWT 做跨域信息传递,但每个业务系统在后端仍然维护一个本地 Session,JWT 只用来在首次接触时建立这个 Session。

iframe 隐身登录的隐蔽陷阱

还有一种做法是在各个业务域名下嵌入一个隐藏的 iframe,指向认证中心的某个页面。认证中心在自己的域下有 Session Cookie,iframe 加载时会把 Cookie 带上。认证中心通过 postMessage 把用户信息推给父页面,父页面拿到后在自己的域下发起登录请求。这个方案看起来绕过了重定向的跳转损耗,用户体验更顺滑。但它极度依赖第三方 Cookie 的支持。Safari 和 Firefox 默认就屏蔽第三方 Cookie,Chrome 也在逐步淘汰。一旦第三方 Cookie 被禁用,iframe 里的认证中心页面连自己的 Session Cookie 都读不到,整个流程直接瘫痪。而且 iframe 通信还引入了点击劫持和 XSS 攻击面的风险,你需要严格设置 X-Frame-Options 或 CSP 的 frame-ancestors 指令,同时校验 postMessage 的来源 origin,任何一个环节疏忽都会造成令牌泄露。

共享 Session 存储的架构考量

如果你的多个业务系统后端都在同一个内网,可以考虑让它们共享同一个 Session 存储,比如共用 Redis 集群。每个系统在用户请求过来时,不再依赖 Cookie 的域名归属,而是通过某种机制拿到 Session ID,然后自己去 Redis 里查。问题的关键又回到了如何跨域传递这个 Session ID。你可以把它放在 URL 参数里,但这会带来严重的泄露风险,URL 会被记录在服务器日志、浏览器历史、Referer 头里。你也可以用 POST 表单自动提交,但每次跨域跳转都要做一次表单提交,实现起来很别扭。更隐蔽的做法是在反向代理层做文章,用 Nginx 或 Envoy 把不同域名的请求在网关层面统一处理,在代理层完成 Session ID 的注入和转发,让后端应用感知不到跨域的存在。但这要求所有流量都经过统一的网关,对网络拓扑有要求。

安全退出时的全局会话销毁

登录难,退出更难。用户在 shop.company.com 点击退出,你不仅要销毁 shop.company.com 的本地 Session,还要通知 passport.company.com 和其他已经登录的子站一起退出。如果只清除本地 Cookie,用户访问其他子站时依然处于登录态,这叫“退不干净”。你需要一个全局的 Session 注销机制。可以在认证中心维护一个用户会话列表,记录该用户已经在哪些系统建立了会话。退出时,认证中心逐个调用各系统的注销接口,或者发布一条注销消息到消息队列,各系统订阅后清除对应的本地 Session。如果调用失败,要有重试和补偿机制。还有一种轻量级的做法是缩短 Session 的有效期,配合滑动过期策略,让各个系统的本地 Session 在短时间内自然失效,但这牺牲了实时性。

跨域 Session 的 CSRF 和 XSS 防护变化

引入跨域 SSO 后,攻击面发生了变化。传统的 CSRF 攻击针对的是表单提交和状态变更接口,但跨域场景下,认证中心的授权码回调接口成了高危目标。攻击者可以构造一个恶意链接,诱骗已登录用户点击,从而在攻击者控制的业务系统上拿到授权码并换取令牌。所以认证中心在签发授权码时,必须校验 redirect_uri 的白名单,并且要求业务系统在换取令牌时提供事先分配好的 client_secret。对于 XSS,跨域 postMessage 通信是新的攻击入口。任何通过 postMessage 接收到的消息,都必须严格验证 event.origin,并且对消息内容做结构校验,绝不能直接把消息里的数据当作可信输入拼接到页面或存储里。

多域名下的 Session 粘滞与负载均衡

如果你的业务系统做了水平扩展,有多台服务器,本地 Session 就会遇到粘滞问题。用户第一次被分配到服务器 A,建立了 Session,下次请求被负载均衡分到服务器 B,Session 就丢了。通常的做法是启用 ip_hash 或 cookie 插入的会话保持。但在跨域 SSO 场景下,用户从认证中心跳回来时,可能被分配到不同的服务器,而授权码只能使用一次,第二次使用会报错。这就要求授权码的校验不能依赖本地状态,必须放在共享存储里,比如 Redis,所有服务器都去同一个地方校验和标记授权码已使用。或者干脆把本地 Session 也放到 Redis,实现无状态的业务服务器,这样就不存在粘滞问题了。

前端 SPA 与跨域 Session 的适配

单页应用让问题更复杂。SPA 通常用 AJAX 与后端交互,页面跳转很少。如果后端返回 302 重定向到认证中心,AJAX 请求会跟随重定向,但由于跨域限制,前端拿不到重定向后的响应,只会收到一个网络错误。你需要在前端封装一层拦截器,当收到 401 状态码时,不是让 AJAX 自己去跟重定向,而是由前端代码把当前页面完整跳转到认证中心。认证中心验证通过后,带着授权码跳回 SPA 的地址。SPA 从 URL 上解析出授权码,再用 AJAX 发给自己的后端换取令牌。这个流程里,授权码短暂暴露在浏览器地址栏里,所以必须保证授权码是一次性的,且有效期极短。另外,SPA 的页面状态可能在跳转过程中丢失,你可以在跳转前把当前状态序列化后存到 sessionStorage,回来后再恢复。

跨域 SSO 的监控与降级

任何分布式系统都要考虑故障隔离。认证中心一旦挂了,所有业务系统的登录都会中断。你需要对认证中心做高可用部署,同时各业务系统要有降级策略。比如在认证中心不可用时,允许用户使用本地账号密码临时登录,等认证中心恢复后再做会话合并。监控上要关注几个核心指标:授权码的发放量和兑换量的比例,如果兑换量远低于发放量,可能是有攻击者在批量尝试授权码;各系统本地 Session 的创建时间分布,能反映用户在跨域跳转过程中的耗时;认证中心与各系统之间的令牌刷新失败率,能提前发现系统间网络或配置问题。日志里要记录每次跨域跳转的完整链路,包括重定向的每一步耗时,方便排查用户反馈的“登录循环”问题。

长期演进中的协议选择

如果系统还在早期,可以考虑直接用 OAuth 2.0 和 OpenID Connect 这类标准协议来构建跨域认证体系。它们已经把授权码流程、令牌刷新、用户信息端点都标准化了,比你从零造轮子要可靠得多。OpenID Connect 在 OAuth 2.0 的基础上增加了 id_token,用 JWT 格式携带用户身份信息,并且定义了标准的 Session Management 和 Logout 规范。虽然实现起来有一定复杂度,但它考虑了各种边缘情况,比如多设备同时登录时的会话管理、前后端分离架构下的令牌处理。如果你的业务系统已经成型,改造成本太高,那就需要在前文提到的方案里做组合选择,核心原则是:跨域传递的永远是短期凭证,长期会话由各系统独立维护,认证中心只做集中鉴权和全局注销的协调者。