登录一次,畅行所有系统:单点登录 SSO 的原理、协议、架构与安全设计
单点登录(Single Sign-On,SSO)不是“把多个系统的登录页面做成一个”,而是建立一个统一身份提供者,让用户在多个相互信任的应用之间复用认证结果。本文从浏览器跳转、Cookie、Token、OAuth 2.0、OIDC、SAML、Session、注销、续期和安全治理等方面,系统拆解 SSO 的工作机制与工程实现。
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. 角色与概念
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 通常为 codescope 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 的选择
推荐 Web 应用使用 BFF:浏览器只持有 HttpOnly、Secure、SameSite Cookie,BFF 在后端保存或转发 Token。
9. SAML、CAS、OIDC 对比
如果新系统由自己设计,通常优先 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=1800HttpOnly:禁止 JavaScript 读取; Secure:只通过 HTTPS 发送; SameSite:降低跨站请求携带风险; Domain:尽量不要设置过宽; Path:限制发送范围; Max-Age/Expires:明确生命周期。
12. 统一身份中心的数据模型
User├── Credential├── Device├── Organization├── Role/Permission└── ConsentOAuthClient├── redirect_uris├── allowed_scopes├── client_auth_method└── secret/public_keyAuthSession├── user_id├── device_id├── authentication_time├── amr/acr└── statusTokenFamily├── access_tokens├── refresh_tokens├── rotation_counter└── revoked_at
权限、身份和会话要分开建模。用户的角色变化不一定立即改变已签发 JWT,因此关键操作必须回源检查或使用短期 Token。
13. 源码设计
sso/identity/ user.py organization.py credential.py directory.pyprotocol/ oauth.py oidc.py saml.py cas.py discovery.py jwks.pyauth/ login.py password.py mfa.py risk.py consent.pysession/ session.py cookie.py device.py logout.pytoken/ access.py id_token.py refresh.py revoke.py rotate.pyclient/ registry.py redirect.py policy.pysecurity/ csrf.py nonce.py pkce.py signatures.py key_rotation.pyaudit/ events.py login_log.py risk_log.pyapi/ 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_iduser_id / subjectclient_idsession_iddevice_idip / user-agentamr / acrscopeissuer / audienceresult / failure_reasontimestamp
日志中禁止记录密码、完整 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. 选型建议
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
夜雨聆风