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