登录一次,畅行所有系统:单点登录 SSO 的原理、协议、架构与安全设计
单点登录(Single Sign-On,SSO)不是“把多个系统的登录页面做成一个”,而是建立一个统一身份提供者,让用户在多个相互信任的应用之间复用认证结果。本文从浏览器跳转、Cookie、Token、OAuth 2.0、OIDC、SAML、Session、注销、续期和安全治理等方面,系统拆解 SSO 的工作机制与工程实现。
B%E5%BD%95SSO%E6%8A%80%E6%9C%AF%E8%AE%BE%E8%AE%A1-%E5%8A%A8%E6%BC%AB%E5%B0%81%E9%9D%A2.png&pos_id=img-w11I6P5d-1785853449631)
1. 先说结论
单点登录的核心目标是:
用户只在统一身份中心认证一次
↓
多个业务系统通过信任协议确认用户身份
↓
用户无需重复输入密码即可访问其它系统
SSO 不等于“一套 Cookie 共享所有系统”。现代系统通常使用:
- OAuth 2.0:授权框架,解决“应用如何获得访问授权”;
- OpenID Connect:建立在 OAuth 2.0 之上的身份认证协议,解决“用户是谁”;
- SAML 2.0:企业和传统组织常用的基于 XML 断言的联邦认证协议;
- CAS:经典 Web 单点登录协议,概念清晰但现代 API 场景相对有限;
- JWT:一种 Token 格式,不是完整的登录协议。
2. 为什么需要单点登录
假设一个企业有 OA、CRM、财务、代码平台和数据平台。如果每个系统独立登录,会产生:
- 用户需要记忆多套账号密码;
- 密码策略、二次认证和离职禁用难以统一;
- 每个系统都要维护登录、找回密码、风控和审计;
- 用户在系统之间切换时体验差;
- 一个账号被禁用后,其他系统可能仍然保留有效会话;
- 业务系统直接保存密码,攻击面扩大。
SSO 将认证责任集中到 Identity Provider(IdP),业务系统作为 Service Provider(SP)或 Relying Party(RP)消费认证结果。
3. 角色与概念
| 概念 | 含义 |
|---|---|
| User | 需要访问业务系统的人或服务主体 |
| IdP | Identity Provider,统一身份提供者,负责认证用户 |
| SP | Service Provider,提供业务能力并信任 IdP |
| RP | Relying Party,在 OIDC 中依赖 IdP 完成身份认证 |
| Client | OAuth/OIDC 中代表业务应用的客户端 |
| Authorization Server | OAuth 授权服务器,签发授权码和 Token |
| Resource Server | 持有受保护资源、校验 Access Token 的服务 |
| Session | IdP 或业务系统维护的登录状态 |
| Cookie | 浏览器自动携带的站点状态凭证 |
| Access Token | 访问资源的授权凭证 |
| ID Token | OIDC 中描述用户身份的 JWT |
| Refresh Token | 用于换取新 Access Token 的长期凭证 |
| Assertion | SAML 中由 IdP 签名的身份断言 |
4. 总体架构
4.1 认证中心负责什么
- 登录页面、密码校验、MFA、验证码和风控;
- 用户目录、组织、角色和账号生命周期;
- OAuth/OIDC/SAML/CAS 协议端点;
- 授权码、Access Token、ID Token、Refresh Token 签发;
- 公钥发布、密钥轮换和 Token 撤销;
- 登录审计、设备管理和全局注销。
4.2 业务应用负责什么
- 发起登录请求;
- 校验回调中的 state、nonce、code;
- 建立自己的应用 Session 或维护本地 Token;
- 根据 claims 映射本地用户、角色和租户;
- 在访问资源时校验 Access Token 的签名、issuer、audience、scope 和过期时间。
5. 最常见的 OIDC Authorization Code 流程
5.1 正常登录链路
5.2 为什么使用 Authorization Code
浏览器不直接拿到用户密码,也不直接在 URL 中携带 Access Token。授权码短时有效且只能被指定客户端兑换,配合 PKCE 可以降低授权码被拦截后重放的风险。
5.3 核心参数
client_id 客户端标识
redirect_uri 认证完成后的回调地址
response_type 通常为 code
scope openid profile email 等权限范围
state 防 CSRF,必须与浏览器会话绑定
nonce 防 ID Token 重放,必须在 ID Token 中校验
code_challenge PKCE 中的挑战值
6. OAuth 2.0 与 OIDC 的区别
6.1 OAuth 2.0 解决什么
OAuth 2.0 解决的是授权委托:
用户允许应用访问某个资源
它并不标准化“用户是谁”。Access Token 主要面向 Resource Server,不应被业务应用直接当作身份信息来源。
6.2 OIDC 增加了什么
OIDC 在 OAuth 2.0 上增加:
openidscope;- ID Token;
- UserInfo Endpoint;
- 标准 claims,如
sub、iss、aud、nonce、auth_time; - Discovery 和 JWKS。
因此,登录认证优先使用 OIDC,而不是仅使用 OAuth 2.0 自定义“获取用户信息”接口。
7. Token 设计
7.1 Access Token
Access Token 表示访问资源的授权,常见字段包括:
{
"iss": "https://id.example.com",
"sub": "user-123",
"aud": "order-api",
"scope": "orders:read orders:write",
"exp": 1780000000,
"iat": 1779996400,
"jti": "token-id"
}
资源服务必须校验:签名、issuer、audience、有效期、scope、必要的租户和权限 claims。
7.2 ID Token
ID Token 是给客户端的身份结果,主要回答“认证了哪个用户、认证如何发生”。它不是通用 API 访问令牌,不应该拿着 ID Token 调任意资源服务。
7.3 Refresh Token
Refresh Token 寿命较长,风险更高。推荐:
- 只存储在安全的后端或受保护的客户端存储中;
- 使用 Refresh Token Rotation;
- 绑定客户端、设备或会话;
- 检测重用并撤销令牌族;
- 支持主动撤销和全局注销。
7.4 JWT 不是万能方案
JWT 优点是无状态校验、跨服务传递方便;缺点是签发后默认不容易立即撤销,Payload 可被读取,过大时会增加网络成本。JWT 应该只放必要 claims,不要放密码、敏感隐私或不断变化的权限。
8. Cookie Session 与 Token 的选择
| 方案 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 服务端 Session | 易撤销、敏感数据不出服务端 | 需要存储和共享 Session | Web 后台、传统业务 |
| JWT Access Token | 跨服务方便、无需中心查询 | 撤销难、泄漏影响大 | API、微服务 |
| BFF + HttpOnly Cookie | 浏览器不接触 Token,安全性好 | 增加 BFF 层 | Web SPA |
| 浏览器直接持有 Token | 架构简单 | XSS/存储风险高 | 受控场景,需谨慎 |
推荐 Web 应用使用 BFF:浏览器只持有 HttpOnly、Secure、SameSite Cookie,BFF 在后端保存或转发 Token。
9. SAML、CAS、OIDC 对比
| 维度 | OIDC | SAML 2.0 | CAS |
|---|---|---|---|
| 数据格式 | JSON/JWT | XML Assertion | Ticket |
| 主要场景 | Web、移动端、API、现代应用 | 企业 SaaS、传统组织、AD | 经典 Web 单点登录 |
| 开发体验 | 相对简单 | XML、签名和配置复杂 | 概念简单 |
| API 友好性 | 好 | 较弱 | 较弱 |
| 身份与授权 | 身份 + OAuth 授权 | 身份断言 + 属性 | 登录票据 |
| 常见问题 | Token 泄漏、配置错误 | XML 签名/解析错误 | 跨域和现代客户端适配 |
如果新系统由自己设计,通常优先 OIDC;需要接入传统企业 IdP 时,SAML 仍然非常常见。
10. 单点登录与单点退出
10.1 登录态的两个层次
IdP Session:用户在身份中心已经登录
App Session:用户在某个业务系统已经登录
用户访问第二个应用时,IdP 发现已有 IdP Session,可以直接签发新的授权码,用户无需再次输入密码。但这不代表两个应用共享同一个业务 Session。
10.2 注销难点
注销有三层:
- 本地注销:删除当前应用 Session;
- IdP 注销:删除统一身份中心 Session;
- 全局注销:通知其他应用结束会话并撤销 Token。
全局注销可以通过前端通道、后端通道、事件总线或短 Access Token + Refresh Token 撤销实现。工程上必须接受分布式系统的短暂不一致,并记录注销状态。
11. 安全设计
11.1 必须防御的攻击
- CSRF:使用不可预测且与会话绑定的 state;
- 授权码注入:校验 state、PKCE、client 和 redirect_uri;
- Open Redirect:redirect_uri 必须精确白名单,不允许任意 URL;
- Token 泄漏:避免 URL 暴露、日志打印和前端长期存储;
- XSS:HttpOnly Cookie、CSP、输出编码和依赖治理;
- 重放攻击:短过期时间、nonce、jti、设备绑定和 Rotation;
- 权限提升:严格校验 audience、scope、tenant 和角色来源;
- IdP 仿冒:TLS、issuer 校验、公钥来源固定和域名保护;
- 登录 CSRF:不能只校验“用户已经登录”,还要校验请求上下文;
- Session Fixation:登录成功后重新生成 Session ID。
11.2 Cookie 安全属性
Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
HttpOnly:禁止 JavaScript 读取;Secure:只通过 HTTPS 发送;SameSite:降低跨站请求携带风险;Domain:尽量不要设置过宽;Path:限制发送范围;Max-Age/Expires:明确生命周期。
12. 统一身份中心的数据模型
User
├── Credential
├── Device
├── Organization
├── Role/Permission
└── Consent
OAuthClient
├── redirect_uris
├── allowed_scopes
├── client_auth_method
└── secret/public_key
AuthSession
├── user_id
├── device_id
├── authentication_time
├── amr/acr
└── status
TokenFamily
├── access_tokens
├── refresh_tokens
├── rotation_counter
└── revoked_at
权限、身份和会话要分开建模。用户的角色变化不一定立即改变已签发 JWT,因此关键操作必须回源检查或使用短期 Token。
13. 源码设计
sso/
identity/ user.py organization.py credential.py directory.py
protocol/ oauth.py oidc.py saml.py cas.py discovery.py jwks.py
auth/ login.py password.py mfa.py risk.py consent.py
session/ session.py cookie.py device.py logout.py
token/ access.py id_token.py refresh.py revoke.py rotate.py
client/ registry.py redirect.py policy.py
security/ csrf.py nonce.py pkce.py signatures.py key_rotation.py
audit/ events.py login_log.py risk_log.py
api/ authorize.py token.py userinfo.py introspect.py
13.1 Authorization Endpoint
def authorize(request):
client = clients.get(request.client_id)
validate_redirect_uri(client, request.redirect_uri)
validate_response_type(request.response_type)
validate_scope(client, request.scope)
if not session.is_authenticated(request.browser):
return redirect_to_login(request)
auth_code = code_store.create(
client_id=client.id,
user_id=session.user_id,
redirect_uri=request.redirect_uri,
code_challenge=request.code_challenge,
nonce=request.nonce,
expires_in=60,
one_time=True,
)
return redirect(request.redirect_uri, code=auth_code, state=request.state)
13.2 Token Endpoint
def token(request):
code = code_store.consume_once(request.code)
validate_client(code.client_id, request.client_auth)
validate_redirect_uri(code.redirect_uri, request.redirect_uri)
validate_pkce(code.code_challenge, request.code_verifier)
id_token = signer.sign_id_token(code.user_id, nonce=code.nonce)
access_token = signer.sign_access_token(code.user_id, code.client_id)
refresh_token = refresh_store.issue_rotating_family(code.user_id)
return Tokens(id_token, access_token, refresh_token)
13.3 资源服务校验
def authenticate_bearer(token, required_scope):
claims = jwt.verify(token, jwks, issuer=IDP, audience=RESOURCE_ID)
require(claims.exp > now())
require(required_scope in claims.scope)
return Principal(sub=claims.sub, tenant=claims.tenant)
14. 登录失败、续期和异常场景
14.1 登录失败
- 密码错误:限速、风险评分、必要时验证码;
- MFA 失败:记录设备和 IP,避免无限尝试;
- 账号禁用:不透露过多账号存在性信息;
- 回调失败:校验 state 和 code,不重复兑换;
- IdP 不可用:已登录的短期本地 Session 可按策略继续,不能伪造新身份。
14.2 Access Token 过期
客户端使用 Refresh Token 获取新 Access Token;如果 Refresh Token 也过期或被撤销,重新走授权流程。并发刷新要避免多个请求同时旋转导致 Token 家族误判重用。
14.3 用户权限变化
短期 Token 不能保证权限立即收敛。高风险权限变更后应:
- 撤销该用户 Token Family;
- 删除或缩短业务 Session;
- 发布权限变更事件;
- 资源服务在关键操作时回源或检查版本号。
15. 前后端分离与移动端
15.1 SPA
推荐 Authorization Code + PKCE,避免 Implicit Flow。更安全的企业方案是 BFF,浏览器只管理 Cookie,BFF 负责 Token 交换和 API 访问。
15.2 移动端
使用系统浏览器或安全认证代理完成授权,不要在 App 内嵌 WebView 中收集密码。使用 PKCE,配合 Universal Links/App Links 防止回调被恶意应用抢占。
15.3 服务间认证
服务间通常不需要用户交互登录,可使用 Client Credentials、mTLS、私钥 JWT 或工作负载身份。不要把用户登录流程硬套到后台服务。
16. 可观测性和审计
每次认证应关联:
request_id
user_id / subject
client_id
session_id
device_id
ip / user-agent
amr / acr
scope
issuer / audience
result / failure_reason
timestamp
日志中禁止记录密码、完整 Token、client secret 和 Refresh Token。需要支持:登录成功率、MFA 挑战率、授权码失败率、Token 刷新失败率、重放检测、异常设备和全局注销状态。
17. 一次完整 SSO 请求
用户访问“财务系统”:
- 浏览器访问财务系统,发现没有本地 Session;
- 财务系统生成 state、nonce 和 PKCE verifier;
- 浏览器跳转到 IdP;
- IdP 检查自己的 SSO Cookie;
- 如果没有,展示登录和 MFA;
- 认证成功后创建 IdP Session;
- IdP 生成一次性 authorization code;
- 浏览器带 code 和 state 回调财务系统;
- 财务后端校验 state、code、redirect_uri 和 PKCE;
- 财务后端调用 Token Endpoint;
- IdP 签发 ID Token、Access Token 和可选 Refresh Token;
- 财务系统校验 ID Token 的签名、issuer、audience、nonce 和过期时间;
- 财务系统映射本地用户和权限,创建本地 Session;
- 后续请求携带本地 HttpOnly Cookie;
- 用户访问 CRM 时,CRM 再次跳转 IdP;
- IdP 已有 Session,直接为 CRM 签发新的 code;
- 用户无需再次输入密码。
18. 选型建议
| 场景 | 推荐 |
|---|---|
| 新建 Web/API 平台 | OIDC Authorization Code + PKCE |
| 企业接入传统 IdP | SAML 2.0 或由网关转换为 OIDC |
| 内部经典 Web 系统 | CAS 或 OIDC |
| SPA | OIDC + PKCE,优先 BFF |
| 移动端 | 系统浏览器 + PKCE |
| 服务间 | Client Credentials/mTLS/工作负载身份 |
| 关键权限操作 | 短 Token + 回源校验 + MFA |
19. 最容易踩的坑
- 把 JWT 当成登录协议;
- 不校验
state; - 允许任意
redirect_uri; - 把 Access Token 放在 URL;
- 把 ID Token 当 API Token;
- 把 Refresh Token 存在 localStorage;
- 不校验
iss、aud、nonce; - 只依赖 JWT 过期,不设计撤销;
- 用户离职后只禁用账号,不撤销已有会话;
- 多系统共享过宽的 Domain Cookie;
- 日志打印完整 Token;
- 误以为“IdP 登录了”就代表所有业务权限都合法;
- 不区分认证(Authentication)和授权(Authorization);
- 不考虑时钟偏差、密钥轮换和公钥缓存;
- 前端跳转成功就认为后端认证成功。
20. 总结
SSO 的本质不是共享登录页面,而是建立跨应用的信任关系:
统一身份认证
+ 标准协议
+ 短期凭证
+ 严格回调校验
+ 权限与会话治理
+ 可审计与可撤销
= 可生产化的单点登录系统
对新系统而言,OIDC Authorization Code + PKCE 是常见起点;对传统企业系统,SAML 仍有价值;对浏览器安全,BFF + HttpOnly Cookie 通常比把 Token 暴露给前端更稳妥。无论使用何种协议,都必须将 state、nonce、PKCE、redirect_uri、issuer、audience、scope、过期、撤销和审计 当作完整系统的一部分,而不是可选配置。
参考资料
- OAuth 2.0 RFC 6749
- OAuth 2.0 Security Best Current Practice
- OpenID Connect Core 1.0
- SAML 2.0 Technical Overview
- OWASP OAuth 2.0 Cheat Sheet
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/huaiixinsi/article/details/163482615




