解决跨域认证难题,掌握现代微服务架构的身份基石。
作为一名开发者,你是否曾好奇:用户在淘宝(taobao.com)登录后,跳转到天猫(tmall.com)为何能直接购物,无需重复登录?这背后是现代分布式系统中至关重要的单点登录(Single Sign-On, SSO) 技术。
本文将深入剖析SSO的核心思想,并通过清晰的流程图、代码示例和关键安全考量为你彻底讲明白。
01 问题与核心思想:为什么需要“中央认证官”?
1.1 痛点:同源策略的壁垒
在多系统并存的现代企业中(如OA、CRM、财务系统),每个系统域名不同(oa.com, crm.com)。浏览器的同源策略如同一道安全墙,阻止了它们直接共享登录状态(如LocalStorage或Cookie)。让用户在每个系统间重复登录,体验极差。
1.2 解决方案:引入统一的认证中心 (CAS)
解决方案是设立一个所有子系统都信任的、独立的中央认证服务,通常拥有一个独立的顶级域名(如 sso.com 或 passport.com)。这套标准协议通常被称为CAS。
生活化比喻:
- 认证中心 (CAS) = 景区总售票处
- 全局Session = 在总售票处购买的通票
- 子系统 (oa.com, crm.com) = 景区内的各个景点
- Ticket (票据) = 总售票处开的一次性验票凭证,凭此进入特定景点。
下面的序列图清晰地展示了用户首次登录和免登访问的完整交互流程,是理解SSO的关键。
02 实战推演:深入SSO的“握手”细节
场景一:首次登录子系统A (app.com)
第1步:发现未登录,重定向至CAS
用户访问 app.com,前端路由守卫检查发现无本地Token。
// 前端路由守卫 (Vue Router示例)router.beforeEach((to, from, next) => { // 检查是否存在当前系统的本地Token const localToken = localStorage.getItem('app_token'); if (localToken) { // 已登录,放行 next(); } else { // 未登录,重定向至CAS,并携带当前URL作为回调地址 const redirectUri = encodeURIComponent_(window.location.href); _window.location.href = `https://sso.com/login?service=${redirectUri}`; }});第2-5步:全局登录与凭证下发
用户在 sso.com 登录成功后,CAS会在其域名下设置一个表示全局会话的Cookie,并生成一个一次性的Service Ticket (ST),重定向回子系统A。
### 认证中心响应:种下全局Cookie并重定向HTTP/1.1 302 FoundSet-Cookie: SESSION=abcd1234; Domain=sso.com; Path=/; HttpOnly; SecureLocation: https://app.com/callback?ticket=ST-123456789abcdef第6步(关键):前端处理Ticket,换取局部Token
浏览器带着Ticket回到 app.com/callback?ticket=ST-123456...,前端需要拦截并处理。
// 在应用入口或路由守卫中处理回调const urlParams = new URLSearchParams_(window.location.search);const ticket = urlParams.get('ticket');if (ticket) { // 用Ticket向后端换取本地Token fetch('/api/sso/verify-ticket', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticket }) }) .then(response => response.json()) .then(data => { if (data.success) { // 1. 将局部Token存入本地存储 localStorage.setItem('app_token', data.token); // 2. !!! 安全关键:清除URL中的Ticket参数 !!! const cleanUrl = _window.location.pathname; window.history.replaceState(null, '', cleanUrl); // 3. 跳转到首页或目标页 _window.location.href = '/'; } else { // 验证失败,处理错误 console.error('Ticket验证失败'); } });}第7步:后端验证Ticket
子系统A的后端需要与CAS后端通信,验证Ticket的有效性。
// 子系统A后端 API:/api/sso/verify-ticketapp.post('/api/sso/verify-ticket', async (req, res) => { const { ticket } = req.body; // 向CAS后端发起服务端请求,验证Ticket const casValidateUrl = `https://sso.com/serviceValidate?ticket=${ticket}&service=${APP_A_URL}`; try { const response = await fetch(casValidateUrl); const casData = await response.text(); // CAS通常返回XML格式 // 解析CAS响应,确认Ticket有效并获取用户身份 if (/* 验证成功 */) { const user = /* 从CAS响应中解析出的用户信息 */; // 1. 为用户在子系统A创建局部会话(如生成Session或JWT) const localToken = generateLocalTokenForUser(user); // 2. 将局部Token返回给前端 res.json({ success: true, token: localToken }); } else { res.status(401).json({ success: false, message: 'Invalid ticket' }); } } catch (error) { res.status(500).json({ success: false, message: 'Validation error' }); }});03 概念辨析:SSO vs. OAuth 2.0 vs. OIDC
这是一个常见的面试题,切勿混淆。
协议 | 核心目标 (解决什么问题) | 典型场景 |
SSO (CAS) | 认证 (Authentication) - “你是谁?” | 公司内部系统免密登录(OA、CRM、财务) |
OAuth 2.0
| 授权 (Authorization) - “你能干什么?” | 微信扫码登录第三方App,授权获取头像昵称(但不泄露密码) |
OIDC | 认证 (Authentication),基于OAuth 2.0 | 在OAuth2流程上,标准化返回用户身份信息(ID Token),是现代SSO的流行实现方式 |
简单说:SSO是一种解决方案/目标,而OAuth 2.0和OIDC是实现该目标的协议标准。现代系统常用OIDC来实现更安全、标准的SSO。
04 安全闭环:实现“单点登出”
单点登录必须配套单点登出,否则用户在一个系统退出后,其他系统仍可访问,存在巨大安全风险。
正确流程如下:
- 用户在子系统A点击“退出”。
- 子系统A前端:清除本地Token (localStorage.removeItem('app_token'))。
- 子系统A前端:重定向至CAS的登出端点。
_window.location.href = 'https://sso.com/logout' - 认证中心 (CAS):销毁全局Session(清除 sso.com 的Cookie)。向所有在此次全局会话下登录过的子系统的后端(通过之前记录的Service Registry)异步发送登出通知(通过WebHook或后端间API调用)。
- 各子系统后端:收到通知后,销毁各自的局部会话(如使本地Token或Session失效)。
- CAS重定向:最后,CAS将用户重定向回一个确认页面或登录页。
总结与关键洞察
- 核心是信任代理:SSO的核心是建立一个所有子系统都信任的中央认证机构(CAS)。
- 双凭证机制:理解全局会话(CAS的Cookie)和局部会话(各子系统的Token)的分离是关键。
- 前端职责重大:前端在路由守卫、Ticket拦截与清理、重定向逻辑中扮演着不可或缺的角色。
- 安全无小事:Ticket是一次性的,必须及时清理URL。单点登出是保障安全的必要环节。
掌握SSO,不仅意味着你能搞定登录需求,更代表你对Web安全、网络协议和分布式系统设计有了更深的理解。下次与后端联调SSO时,你就能自信地沟通整个流程的每一处细节。
