SAML 2.0 与遗留 SSO:企业身份联邦的协议原理、迁移策略与 OIDC 对比
概述
2026 年了,SAML 2.0 这个诞生于 2005 年的标准在 OIDC 的压力下看似日薄西山,但据 Gartner 和 Okta 的统计,全球超过 70% 的企业 SaaS 产品仍然把 SAML SSO 放在 Enterprise 定价方案的第一行。当你做 B2B SaaS,第一个要求"接 SSO"的企业客户——通常是 500 人以上的中大型公司——发给你的可能不是 OIDC metadata URL,而是一个 XML 格式的 Federation Metadata 文件。
SAML(Security Assertion Markup Language)的核心思想比 OAuth/OIDC 更简单:身份提供方(IdP,Identity Provider)签发一个 XML 格式的断言(Assertion),服务提供方(SP,Service Provider)验签后信任其中的身份信息。但也正是这种基于 XML 的"简单",使得 SAML 在工程实现上比基于 JSON 的 OIDC 复杂得多——XML 签名验证、NameID 格式选择、Metadata 同步等环节都潜藏着大量工程陷阱。
本文的目标不是争论 SAML 与 OIDC 孰优劣,而是面向实际工程场景:
- 为需要对接存量企业 SAML IdP 的开发者提供协议详解
- 为正在做身份系统现代化的架构师提供迁移策略参考
- 为安全工程师提供 SAML 协议的安全分析与攻击面梳理
协议模型与核心概念
关键角色映射
SAML 和 OIDC 在概念层是同构的——都解决"IdP 告诉 SP 用户是谁"——但协议细节天差地别。以下是两种协议的角色映射:
| SAML 角色 | OIDC 对应 | 职责 |
|---|---|---|
| IdP(Identity Provider) | OP(OpenID Provider) | 认证用户,签发 SAML Assertion |
| SP(Service Provider) | RP(Relying Party) | 接收 Assertion,建立本地会话 |
| Principal | User | 被认证的用户 |
| Assertion | ID Token | 包含用户身份信息的安全声明 |
| Metadata | Discovery Document | 技术配置交换(端点、证书、能力集) |
SAML 协议栈结构
SAML 2.0 不是一个单一协议,而是一个由多个子规范组成的协议栈:
┌─────────────────────────────────────────────────┐
│ SAML Protocol (核心协议层) │
│ AuthnRequest / Response / Assertion │
├─────────────────────────────────────────────────┤
│ SAML Bindings (绑定层) │
│ HTTP-Redirect / HTTP-POST / HTTP-Artifact │
├─────────────────────────────────────────────────┤
│ SAML Profiles (应用层) │
│ Web SSO Profile / Single Logout Profile │
├─────────────────────────────────────────────────┤
│ XML Security (安全层) │
│ XML Signature / XML Encryption │
├─────────────────────────────────────────────────┤
│ Transport (传输层) │
│ HTTP / HTTPS │
└─────────────────────────────────────────────────┘这种分层设计让 SAML 具有良好的灵活性,但也意味着实现者需要同时处理多个规范,工程复杂度远高于 OIDC 的"全在 HTTP+JSON 层解决"模式。
两种 SSO 流程详解
SP-Initiated SSO(服务提供方发起)
SP-Initiated 是最常见的 SAML SSO 流程,由 SP 发起认证请求:
用户 → SP → IdP → 用户 → SP → 本地会话建立详细交互序列:
participant User as 用户
participant SP as 应用 (SP)
participant IdP as 企业 IdP
User->>SP: 1. 访问受保护资源
SP->>SP: 2. 生成 SAML AuthnRequest
SP->>User: 3. 302 到 IdP SSO URL + AuthnRequest
Note over User,IdP: SAML AuthnRequest 经过浏览器重定向
User->>IdP: 4. POST SAML AuthnRequest
IdP->>User: 5. 认证用户(可能已有会话,跳过交互式登录)
IdP->>IdP: 6. 生成 SAML Response (含 Assertion)
IdP->>User: 7. 自动提交表单到 SP ACS URL
Note over User,SP: SAML Response 经过浏览器 POST
User->>SP: 8. POST SAML Response 到 SP ACS
SP->>SP: 9. 验证签名、断言条件、受众
SP->>User: 10. 建立本地会话, 302 到目标页面AuthnRequest 的典型格式:
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_abc123"
Version="2.0"
IssueInstant="2026-07-07T10:00:00Z"
Destination="https://idp.example.com/sso"
AssertionConsumerServiceURL="https://sp.example.com/acs"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST">
<saml:Issuer>https://sp.example.com</saml:Issuer>
</samlp:AuthnRequest>工程要点:AuthnRequest 经过浏览器 URL 重定向时需要做 Base64 编码 + URL 编码。由于完整的 XML 可能长达数 KB,部分浏览器对 URL 长度有限制(通常 2KB~8KB),实践中往往采用 DEFLATE 压缩后 Base64 编码以减小体积。
IdP-Initiated SSO(身份提供方发起)
用户已登录 IdP,在 IdP 门户点击 SP 的图标,IdP 直接生成 SAML Response POST 到 SP ACS URL。
用户 → IdP(已有会话)→ SP → 本地会话建立关键差异与风险:
- 缺少 AuthnRequest 上下文:SP 是被动接收方,无法传递 RelayState(目标页面),IdP 也不会知道用户在 SP 中想去哪里
- CSRF 保护缺失:IdP-Initiated SSO 不在 SP-Initiated 的 CSRF 保护范围之内——SP 没有发出过 AuthnRequest,因此无法通过 InResponseTo 字段做请求-响应绑定
- Replay 风险:如果 SP 没有额外的防重放措施(如对 Assertion ID 做去重),攻击者可以重放一个截获的 SAML Response 来登录
防御建议:商用 IdP 通常给 Assertion 设置很短的 NotOnOrAfter 有效期(一般 5 分钟以内),提供了有限的防护窗口。但 SP 仍应记录 Assertion ID 并拒绝重复使用,或要求 IdP-Initiated 流程必须携带额外的签名参数。
SAML Assertion 结构与验证
Assertion 的核心字段
<saml:Assertion ID="_def456" IssueInstant="2026-07-07T10:00:30Z" Version="2.0">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<!-- XML 签名 -->
<ds:Signature>...</ds:Signature>
<!-- 有效期 + 受众限制 -->
<saml:Conditions NotBefore="2026-07-07T10:00:00Z"
NotOnOrAfter="2026-07-07T10:05:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<!-- 认证声明:IdP 如何认证用户 -->
<saml:AuthnStatement AuthnInstant="2026-07-07T10:00:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<!-- 属性声明:用户信息 -->
<saml:AttributeStatement>
<saml:Attribute Name="urn:oid:1.2.840.113549.1.9.1.1">
<saml:AttributeValue>user@company.com</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<!-- 主体 -->
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
user@company.com
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
NotOnOrAfter="2026-07-07T10:05:00Z"
Recipient="https://sp.example.com/acs"
InResponseTo="_abc123"/>
</saml:SubjectConfirmation>
</saml:Subject>
</saml:Assertion>SP 验证清单
SP 收到 SAML Response 后,必须执行以下验证步骤:
┌─ 1. 签名验证 ─────────────────────────────────────────────┐
│ 用 IdP 的公钥验证 XML Signature │
│ 注意:签名可能在整个 Response 上,也可能只在 Assertion 上 │
├─ 2. 时间条件验证 ─────────────────────────────────────────┤
│ NotBefore ≤ 当前时间 < NotOnOrAfter │
│ 时钟漂移容忍窗口通常设为 ±30 秒 │
├─ 3. 受众验证 ─────────────────────────────────────────────┤
│ Audience 必须等于 SP 自己的 entity ID │
│ 防止一个 SP 的 Assertion 被重放到另一个 SP │
├─ 4. SubjectConfirmationData 验证 ─────────────────────────┤
│ Recipient 必须等于 ACS URL │
│ InResponseTo(如果存在)必须等于 AuthnRequest 的 ID │
├─ 5. NameID 验证 ─────────────────────────────────────────┤
│ 确认用户标识符的格式和值在预期范围内 │
├─ 6. Assertion ID 去重 ───────────────────────────────────┤
│ 记录已处理的 Assertion ID,拒绝重复使用 │
│ 缓存窗口应覆盖 Assertion 的整个有效期 │
└───────────────────────────────────────────────────────────┘工程陷阱:XML 签名包装攻击(XSW):SAML 的 XML 签名验证逻辑如果实现不当,攻击者可以通过移动 Signature 元素到文档其他位置,让 SP 验证的是无害的伪造节点,而实际处理的却是恶意 Assertion。这是 SAML 特有的攻击面,OIDC 的 JWS 结构不存在此类问题。防御措施:使用经过严格审计的 SAML 库(如 python3-saml、SAML2 Java Toolkit),禁止手写 XML 签名验证逻辑。
NameID 策略:比你想象的麻烦
NameID 是用户在 SP 中的标识符。和 OIDC 的 sub 不一样——sub 在同一个 IdP 下全局唯一——NameID 可以有不同的 Format,而且格式决定了语义:
| NameID Format | 示例值 | 语义 |
|---|---|---|
urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | user@company.com | 邮箱地址 |
urn:oasis:names:tc:SAML:2.0:nameid-format:persistent | JAv2pHy8rNkMp | 不透明持久标识——同一用户每次登录相同,但不同 SP 间不同(pairwise pseudonymous) |
urn:oasis:names:tc:SAML:2.0:nameid-format:transient | t+N3cVxKzLpXq | 临时标识——每次登录都不同(用于隐私场景) |
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified | 任意字符串 | 由 IdP 和 SP 协商决定 |
persistent 格式,两边的 NameID 值会不同——IdP 为每个 SP 生成不同的 pairwise 标识,无法通过 NameID 直接关联。解决方案:
- 使用 emailAddress 格式:依赖邮箱格式的唯一性,但改名、离职后重用都会出问题
- 在 Attributes 中传递通用标识:如 SAML Attribute
urn:oid:0.9.2342.19200300.100.1.1(uid),让多个 SP 用同一个属性做关联 - IdP 端配置固定属性释放策略:在 IdP 配置中为特定 SP 明确指定 NameID 来源字段
工程陷阱:企业客户的 IdP 管理员可以配置 NameID 的 Format 和来源。你会发现客户的 IdP 发来的 NameID 可能不是邮箱,甚至可能是一串你不认识的数字(如 AD 的 objectGUID 的 Base64 编码)。确认"SP 用什么来唯一标识用户"是 SAML 集成的第一件事,也是最容易遗漏的事。
SAML Metadata:互操作性工程的关键
SAML Metadata 是一个 XML 文件,描述了 IdP 或 SP 的技术配置,作用类似于 OIDC 的 Discovery 文档,但 XML 比 JSON 难读得多。
关键 Metadata 片段
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<!-- 签名证书:用于验证 SAML 签名 -->
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIE...</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<!-- SSO 端点 -->
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://idp.example.com/sso"/>
<md:SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://idp.example.com/sso/post"/>
<!-- 支持的 NameID 格式 -->
<md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
</md:IDPSSODescriptor>Metadata 管理工程要点
- 证书过期监控:Metadata 中的签名 X.509 证书会过期——到期前 IdP 管理员应更新 Metadata。证书突然过期导致所有 SAML SSO 登录失败是一个经典的 P0 事故。 SP 必须支持 Metadata 的自动刷新(建议每 24 小时拉取一次)或至少提供证书过期的监控和告警。
- SP 端的 Metadata 配置:确保 Metadata 中的
Location端点 URL 与 SP 实际使用的 URL 一致。一个常见的配置错误是 SP 使用https://app.example.com/acs作为 ACS URL,但 Metadata 发布的却是https://app.example.com/saml/acs,导致 Recipient 验证失败。
- 多 IdP 场景:大型 SaaS 产品通常需要支持多个企业客户各自的 IdP(每个客户有自己的 Azure AD、Okta 实例)。需要维护一个 IdP Metadata 注册表,并为每个客户做独立的签名验证。
SAML vs OIDC:全方位对比
| 维度 | SAML 2.0 | OIDC |
|---|---|---|
| 数据格式 | XML | JSON |
| 传输方式 | HTTP-Redirect + HTTP-POST with HTML form | HTTP Redirect + POST (JSON) |
| 移动端友好度 | 差(依赖浏览器重定向和表单 POST) | 好(原生 App 可直接处理) |
| 会话登出 | SAML Single Logout(复杂,兼容性差) | RP-Initiated Logout(简单,广泛支持) |
| 企业存量 | 巨大(ADFS、Shibboleth、老 Okta 部署) | 快速增长(新系统默认选择) |
| 部署复杂度 | 高(Metadata 交换 + 证书管理 + XML 签名验证) | 中低(Discovery URL 即开即用) |
| 签名粒度 | XML 签名(可选择性签名节点) | JWS(对整个 Token 签名) |
| 加密能力 | 原生支持 XML Encryption(断言级加密) | 依赖 JWE(较少使用) |
| 适用场景 | 企业 B2B SSO、政府/金融/医疗行业 | 移动端、B2C、API 认证、现代 Web 应用 |
遗留 SAML 系统迁移策略
迁移的四种策略
策略一:协议桥接(推荐用于无法更换 IdP 的场景)
在 SAML IdP 和现代应用之间插入一个身份代理层(Identity Broker/Gateway),将 SAML 转换为 OIDC:
遗留 SAML IdP → [SAML-to-OIDC Bridge] → 现代应用(OIDC)优势:不需要修改 IdP 或应用;劣势:引入了新的组件,增加运维复杂度。
开源方案:Keycloak 可以作为 SAML-to-OIDC 桥接代理,或者使用商业 IDaaS 产品。
策略二:并行运行(风险最低)
新应用部署 OIDC,旧应用保持 SAML,在前端统一路由:
用户登录 → [统一认证门户]
├─ OIDC → 新应用
└─ SAML → 旧应用优势:不影响现有系统;劣势:用户需要看到两套登录体验。
策略三:IdP 端双协议(需要 IdP 支持)
如果 IdP 同时支持 SAML 和 OIDC(如 Azure AD、Okta),直接在 IdP 中注册两个应用条目,分别使用不同协议。
优势:应用端不需要处理多协议;劣势:维护两套配置。
策略四:逐步替换(长期方案)
- 先在新系统中实现 SAML 支持
- 旧系统下线时迁移到 OIDC
- 最终关闭 SAML 端点
密码哈希迁移的特殊挑战
用户密码哈希不可逆迁移——SAML 场景下,密码验证在 IdP 侧完成,SP 永远不知道密码本身,因此这方面压力较小。真正的挑战在于:
- MFA 种子不可迁移:如果用户在 IdP 侧绑定了 TOTP/硬件密钥,迁移到新 IdP 后需要重新注册
- Session 连续性:迁移期间需要维护新旧 IdP 的并行会话,确保用户无感切换
- 属性映射:SAML Attributes 和 OIDC Claims 的语义不完全等价,需要映射表
安全分析
已知攻击面
- XML 签名包装(XSW):前文已述,使用标准化 SAML 库防御
- Assertion 重放:通过 Assertion ID 去重防御
- 证书替换攻击:如果 SP 没有正确校验 Metadata 证书指纹,攻击者可以提供自己的证书来伪造 Assertion
- Golden SAML 攻击:如果 IdP 的签名私钥泄露,攻击者可以为任意用户签发有效的 Assertion——这与 OIDC 的 IdP 私钥泄露是等价的威胁模型
- 时钟漂移利用:如果 SP 的时钟偏移很大,过大/过小的时间窗口都会被利用——建议 NTP 同步 + 容忍窗口不超过 ±60 秒
与国密算法的结合
在等保/密评场景下,SAML 的 XML 签名可能需要使用 SM2/SM3 国密算法替代 RSA/SHA-256:
- 国密 XML 签名尚无广泛遵循的国际标准,国内厂商(如吉大正元、北京数字认证)通常采用自定义方案
- GM/T 0034-2014《基于SM2的证书认证系统密码及安全规范》中的证书格式约束会影响 SAML 证书的互操作性
- 实践中,部分企业采用"国密 TLS 传输 + 国际算法签名断言"的折中方案——TLS 层使用国密密码套件满足传输加密合规,SAML 断言仍使用 RSA 签名确保互操作性
总结
- SAML 不会消失:企业 B2B SaaS 必须支持 SAML,这是存量市场的刚性需求
- 实现双协议:OIDC 是新系统的默认选择,但第一个大客户来的时候很可能要 SAML
- Metadata 管理是运维重点:证书过期 = P0 事故,必须自动化监控和刷新
- 不要手写 SAML 解析器:XML 签名验证陷阱多,使用经过审计的成熟库
- 迁移要渐进:优先在新系统实现 OIDC,旧系统通过 SAML Bridge 逐步过渡
参考来源
- OASIS SAML 2.0 规范
- SAML 与 OIDC 对比 — Quant67
- SAML vs OIDC: 2026 Decision Guide — Clerk
- SAML 安全最佳实践 — OWASP
相关实践
- FIDO2 与 WebAuthn:无密码认证协议的原理与架构——现代身份认证协议的最佳实践
- OAuth 2.1 授权框架:从 OAuth 2.0 安全最佳实践到统一规范的重构——OIDC 的上层协议基础
- 国密 SSL/TLS 客户端认证与双向认证协议详解——国密环境下的认证协议实践