核心回答
跨域没法共享 Cookie,就靠中央认证中心:子系统没登录就跳过去,认证中心确认已登录后再跳回来,子系统建立自己的登录态。
这句话先说到这里就够了。
1. 为什么 Cookie 共享解决不了所有 SSO?
假设:
主系统: https://www.example.com 订单系统: https://order.example.com 数据看板: https://dashboard.example.com它们属于同一个父域名:
example.com ├── www.example.com ├── order.example.com └── dashboard.example.com这种情况下,可以考虑:
Set-Cookie: token=xxx; Domain=.example.com浏览器访问:
order.example.com时,就可能自动携带:
token=xxx所以:
同一父域名下,可以通过
Cookie Domain共享登录态。
但如果变成:
www.example.com order.example.net dashboard.example.org就完全不同了。因为:
example.com example.net example.org之间不能通过 Cookie 的Domain属性互相共享 Cookie。
所以:
Cookie Domain=.example.com不可能让:
example.net example.org也收到这个 Cookie。
2. 跨一级域名怎么办?
这时候就不能再想着:
主系统 Cookie ↓ 直接共享 ↓ 子系统而应该变成:
┌──────────────────┐ │ 中央认证中心 │ │ Auth Server │ └────────┬─────────┘ │ 保存全局登录态 │ ┌──────────────┼──────────────┐ ↓ ↓ ↓ 主系统 订单系统 数据看板 example.com example.net example.org核心思想非常简单:
不是让不同域名共享 Cookie,而是
让不同系统共享同一个认证中心。
3. 最经典的 SSO 流程
例如用户第一次访问:
https://order.example.net订单系统发现:
当前没有自己的登录态于是:
订单系统 ↓ 跳转认证中心 ↓ 认证中心检查自己的登录态假设认证中心发现:
用户已经登录那么它就不需要用户重新输入账号密码。然后给订单系统一个一次性的认证结果。
以经典 CAS 为例,这个结果就是:
ticket完整过程:
用户 │ │ 访问订单系统 ▼ 订单系统 │ │ 没有本地 Session ▼ 认证中心 │ │ 检查中央 Session │ │ 已登录 ▼ 生成一次性 Ticket │ │ 重定向回订单系统 ▼ 订单系统后端 │ │ 用 Ticket 服务端请求认证中心 ▼ 认证中心 │ │ 校验 Ticket │ │ 返回用户身份 ▼ 订单系统 │ │ 创建自己的 Session ▼ 用户登录成功这里有一个特别重要的点:
Ticket 不是最终的登录 Token
不要简单理解成:
认证中心 ↓ 把用户 Token 放 URL ↓ 订单系统直接使用经典 CAS 更像:
认证中心 ↓ 给一个一次性 Ticket ↓ 订单系统后端 ↓ 拿 Ticket 找认证中心兑换用户身份 ↓ 创建自己的 Session所以 Ticket 更接近:
“这次认证已经成功了,你拿着这个凭证来找认证中心确认。”
而不是:
“这是用户以后一直使用的登录 Token。”
现代 Web 更常见的是 OAuth 2.0 + OIDC
子系统 ↓ 跳转认证中心 ↓ 认证中心发现用户已登录 ↓ 返回 Authorization Code ↓ 子系统后端拿 Code 换 Token ↓ OIDC 返回 ID Token / 用户身份 ↓ 子系统建立自己的登录态这里的核心变化是:
CAS: Ticket → 换用户信息 OIDC: Authorization Code → 换 ID Token + Access Token认证中心到底怎么知道回哪个子系统?
子系统发起登录时,会带上自己的身份和回调地址:
订单系统 ↓ 认证中心 │ ├── client_id=order └── redirect_uri=https://order.example.com/callbackclient_id告诉认证中心:
“我是哪个子系统。”
redirect_uri告诉认证中心:
“登录成功后回哪里。”
认证中心通常会提前登记:
order → https://order.example.com/callback dashboard → https://dashboard.example.com/callback所以认证中心不是自己猜,而是根据client_id找到对应的回调地址,并校验redirect_uri是否匹配。
最终完整流程
用户访问子系统 │ ▼ 子系统发现没登录 │ │ client_id │ redirect_uri ▼ 认证中心 │ ├── 已登录 ─────────────┐ │ │ └── 未登录 │ ↓ │ 用户登录 │ ↓ │ 建立中央 Session │ └───────┬────────┘ ↓ Ticket / Code ↓ 跳回子系统 ↓ 子系统后端校验 ↓ 获取用户身份 ↓ 建立本地 Session ↓ 自动登录4. 为什么 Ticket 要一次性?
这是这道题很容易继续追问的地方。假设 URL 是:
https://order.example.net/callback?ticket=ST-123456如果这个 Ticket 永久有效,就存在明显风险。攻击者拿到:
ST-123456以后可能再次拿它换取用户身份。所以通常要求:
Ticket ↓ 只能使用一次 ↓ 认证中心校验成功 ↓ 立即失效于是:
第一次使用: Ticket → 校验成功 → 返回用户信息 → Ticket 失效 第二次使用: Ticket → 校验失败这就是典型的防重放思路。
5. 为什么不直接把 Token 放 URL?
面试官很可能继续问:
“那我直接把 Token 放 URL 里不就行了吗?”
例如:
https://order.example.net?token=eyJ...这种方案风险比较大。因为 URL 可能出现在:
浏览器历史记录 Referer 访问日志 代理日志 监控系统 截图 用户复制分享的链接所以:
长期有效 Token ↓ 直接放 URL通常是不合适的。更合理的是:
URL ↓ 短生命周期、一次性的认证凭证 ↓ 后端服务端校验 ↓ 创建本地登录 Session6. 前端项目和后端渲染项目有什么区别?
这是一个非常好的追问。假设订单系统根本不是 React/Vue:
Java Spring MVC而是:
浏览器 ↓ Java 服务 ↓ HTML它一样可以做 SSO。流程甚至更简单:
浏览器 ↓ 访问 Java 系统 ↓ Java 发现没有 Session ↓ 302 Redirect ↓ 认证中心 ↓ 认证中心发现已经登录 ↓ 302 Redirect ↓ Java 系统 callback ↓ Java 后端拿 ticket ↓ 服务端调用认证中心 ↓ 获取用户身份 ↓ 创建 Java Session ↓ Set-Cookie: JSESSIONID=xxx ↓ 返回页面所以:
SSO 本质上不是前端路由问题,而是
认证系统之间如何建立信任和传递认证结果的问题。
这一点非常重要。
7. 如果是现代 SPA 呢?
如果子系统是:
React Vue Angular现代项目更常见的方案不一定是 CAS。还可能使用:
OAuth 2.0 OIDC尤其是:
OIDC = OpenID Connect可以理解成:
OAuth 2.0 + 用户身份认证 ↓ OIDC所以面试时最好不要说:
“不同一级域名必须使用 CAS。”
这个说法太绝对。更准确的是:
跨一级域名不能靠 Cookie 共享解决,需要引入中央认证中心;具体可以使用 CAS、OIDC 等标准协议。
这个回答明显更高级。
8. state 到底干什么?
如果继续追问 OIDC / OAuth:
为什么要 state?不要简单回答:
“state 防 CSRF。”
可以进一步说:
客户端发起认证请求 ↓ 生成随机 state ↓ 保存本地 ↓ 带 state 跳转认证中心 ↓ 认证中心完成认证 ↓ 回调携带 state ↓ 客户端校验两边 state 是否一致例如:
客户端保存: state = abc123跳转:
/auth? client_id=xxx &state=abc123回调:
/callback? code=xxx &state=abc123然后:
if(callbackState!==savedState){reject();}核心目的就是:
确认这次回调确实对应当前客户端发起的那次认证请求,避免攻击者伪造认证流程。
所以可以说它用于防 CSRF,但不要把state简化成“一个普通防 CSRF Token”。
9. 用户退出登录怎么办?
这又是 SSO 的另一半:
登录统一,退出也要考虑统一。
假设:
认证中心 │ ├── 主系统 Session ├── 订单系统 Session └── 数据看板 Session用户在主系统:
点击退出不能只做:
删除主系统 Cookie否则可能出现:
主系统:已退出 ❌ 订单系统:还登录着 ✅ 数据看板:还登录着 ✅这就不是完整的 SSO 退出。
10. 统一登出怎么做?
一种典型架构是:
用户 ↓ 主系统 Logout ↓ 认证中心 ↓ 销毁中央登录态 ↓ 通知各个子系统 ↓ 子系统销毁自己的 Session例如:
认证中心 │ 全局 Session 删除 │ ┌───────────┼───────────┐ ↓ ↓ ↓ 主系统 订单系统 数据看板 ↓ ↓ ↓ Session Session Session 删除 删除 删除通知子系统的具体方式可以有很多:
HTTP 回调 消息队列 事件总线 前端重定向退出 标准协议提供的 Logout 机制不能简单说:
“统一登出就是 MQ 广播。”
因为 MQ 只是一种工程实现方式,不是 SSO 的必然要求。
11. 这道题真正考什么?
其实不是考:
Cookie 怎么设置?而是考你能不能把这个问题拆开:
登录态在哪里保存? ↓ 不同系统之间能不能共享 Cookie? ↓ 不能共享怎么办? ↓ 有没有中央认证中心? ↓ 认证结果怎么安全传递? ↓ 怎么防重放? ↓ 怎么防 CSRF? ↓ 子系统怎么建立自己的 Session? ↓ 用户退出后怎么同步?这才是这道题的核心。
12. 面试官继续追问链
这道题可以一路追到很深:
主系统登录后,子系统怎么自动登录? ↓ 为什么不能直接共享 Cookie? ↓ 什么情况下 Cookie 可以共享? ↓ 不同一级域名怎么办? ↓ CAS 是什么? ↓ CAS Ticket 和 Token 有什么区别? ↓ 为什么 Ticket 必须一次性? ↓ 为什么不能直接把 Token 放 URL? ↓ state 是干什么的? ↓ OAuth 2.0 和 OIDC 什么关系? ↓ SPA 和后端渲染项目分别怎么接? ↓ Session 和 Token 怎么选? ↓ 用户退出后怎么实现统一登出? ↓ 多个子系统怎么通知? ↓ 如果认证中心挂了怎么办? ↓ 如果 Ticket 被截获怎么办? ↓ SSO 和权限控制有什么区别?其中最后两个尤其容易拉开水平。
13. 一个面试时可以直接说的版本
第一段先说这个,不要一上来背 CAS 全流程:
主系统登录后,子系统能不能自动登录,关键看它们能不能共享登录态。同一父域名下可以考虑 Cookie 共享;如果是不同一级域名,就不能直接共享 Cookie,而是让多个系统依赖统一认证中心。子系统发现自己没登录,就跳到认证中心,认证中心确认用户已经登录后返回一次性的认证凭证,子系统后端再向认证中心校验并建立自己的登录态。
如果面试官继续追问 CAS:
经典 CAS 里,这个一次性凭证就是 Ticket。Ticket 通过浏览器带回子系统,但子系统不会直接信任它,而是由后端拿 Ticket 去认证中心校验,成功后创建自己的 Session。Ticket 用一次就失效,这样可以降低重放风险。
如果继续问统一登出:
退出登录也不能只删主系统自己的 Session,而是要先销毁认证中心的全局登录态,再通知各个子系统清理自己的局部 Session,这样才能真正做到单点登录和统一登出。
这三段基本就是这道题的主干答案。