SAML断言(SAML Assertion)是安全断言标记语言里最核心的身份描述单元。它由身份提供商(IdP)生成,以XML形式封装用户的认证状态与属性,并交由服务提供商(SP)消费,从而完成一次跨域的单点登录。断言并不是登录页面,而是一张“数字身份证”,里面写明了谁、在什么时候、以什么方式通过了认证。
一、SAML断言到底是什么
从技术视角看,SAML断言是一段符合SAML 2.0规范的XML文档。它通常由<Assertion>根节点包裹,内部包含签发者、主体、认证语句、属性语句以及条件限制。身份提供商在用户登录成功后动态构造这段XML,并使用私钥对其签名,确保内容不可伪造。
断言中包含几个关键元素:<Subject>描述被认证的用户,通常通过NameID标识;<Conditions>设定断言的有效时间和允许访问的受众;<AuthnStatement>记录认证发生的时间和使用的认证上下文类;<AttributeStatement>则携带角色、邮箱等扩展属性。服务提供商拿到断言后,先验证签名,再检查条件,最后从主体和属性中提取登录信息。
1.1 为什么用XML而不是JSON
SAML诞生于Web服务安全标准体系成熟的时期,当时XML签名与加密规范已经完备,能够保障端到端的完整性和机密性。JSON后来才流行,且其签名标准JWS在不同语言中的实现一致性不如XML Signature。因此SAML选择XML,是为了直接复用已有的XML Security生态。
另外,SAML交互多发生在浏览器重定向或POST绑定中,整个断言会被Base64编码后作为表单字段传输。XML的结构化特性让校验工具链非常稳定,企业级网关也更容易做深度报文审计。虽然XML冗长,但在安全协议里可读性反而降低了误配风险。
二、SAML断言的典型XML结构
下面是一段简化但语法正确的SAML断言示例,展示了核心节点与签名位置。真实场景中签名值会放在<Signature>内,此处为了突出结构做了省略。
<?xml version="1.0" encoding="UTF-8"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_example_assertion_001"
Version="2.0"
IssueInstant="2023-10-01T12:00:00Z">
<saml:Issuer>https://idp.ipipp.com</saml:Issuer>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
user@ipipp.com
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData Recipient="https://sp.ipipp.com/acs"
NotOnOrAfter="2023-10-01T12:15:00Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2023-10-01T12:00:00Z"
NotOnOrAfter="2023-10-01T12:15:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.ipipp.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2023-10-01T11:55:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:password
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
上面的代码中没有写出<Signature>节点,但实际部署时,IdP必须在断言上计算XML Signature,将签名节点插入到<Issuer>之后。SP端校验时,会按照规范对断言原文做规范化处理,再用IdP证书中的公钥验证,任何中间篡改都会造成校验失败。
需要注意的是,Base64编码只是传输编码,不是加密。如果走的是HTTP重定向绑定,断言明文会出现在URL参数里,因此必须配合HTTPS。如果是POST绑定,断言放在隐藏表单域中提交,同样依赖TLS保护。
2.1 用代码生成并签名断言
在Java生态中,常使用OpenSAML库构造断言。下面示例展示如何建立一个最基础的断言对象,并设置有效期与主体。真实签名步骤还需载入证书和私钥,此处省略具体密钥加载。
import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.impl.AssertionBuilder;
import org.joda.time.DateTime;
public class SamlAssertionDemo {
public Assertion buildBasicAssertion() {
AssertionBuilder builder = new AssertionBuilder();
Assertion assertion = builder.buildObject();
assertion.setID("_demo_001");
assertion.setVersion(org.opensaml.saml.common.SAMLVersion.VERSION_20);
assertion.setIssueInstant(new DateTime());
// 后续设置Issuer、Subject、Conditions等
return assertion;
}
}
这段代码只是搭建骨架。生产环境中,你还要用XMLSignatureBuilder对断言签名,并把X509证书嵌入到签名密钥信息里,以便SP获取公钥。很多自研SSO网关出错,就是因为签名算法用了RSA-SHA1而被现代SP拒绝,应改用RSA-SHA256。
另外,断言的ID必须全局唯一且以字母或下划线开头,否则部分解析库会报格式错误。时间字段建议使用UTC,避免时区错乱导致条件校验不通过。
三、服务提供商如何消费XML断言
SP拿到Base64编码的断言后,先解码得到XML字符串,再解析为DOM对象。第一步永远是验证签名,第二步检查<Conditions>中的时间窗口与受众,第三步读取<Subject>和<AttributeStatement>建立本地会话。
如果断言中携带了NameID,SP通常把它映射为内部用户标识。属性语句里的角色字段则用来做权限控制。整个过程不应信任任何未经验证的节点,例如不能因为断言里写了admin就直接赋权,而应先确认签名合法且签发者受信任。
3.1 常见校验失败原因
实践中,约四成故障来自时间不同步。IdP和SP服务器时钟偏差超过允许偏移量,会让NotBefore或NotOnOrAfter校验失败。解决办法是部署NTP服务,或在SP端配置合理的时钟容差。
另一类是受众限制不匹配。断言里写的是https://sp.ipipp.com,但SP实际配置的entityID是https://sp2.ipipp.com,解析库会认为该断言不是发给自己的而拒绝。这类问题在测试环境拷贝配置时极易出现。
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 签名验证失败 | 证书过期或算法不一致 | 核对IdP证书与SP信任库 |
| 条件校验异常 | 时钟偏移或受众写错 | 检查NTP与entityID配置 |
| 无法提取用户 | NameID格式不支持 | 确认SP支持的NameID策略 |
通过理解SAML断言的XML构成与验证链条,开发者能够在对接企业微信、Okta、Azure AD等身份源时迅速定位问题,也能在自研系统中正确实现基于断言的登录信任模型。
SAML_assertionXML_signatureSSO_authentication修改时间:2026-08-02 10:21:46