SAML签名验证必须先对SignedInfo节点做排他性规范化(exclusive canonicalization),再用公钥验签;直接解Base64后验整个XML字节必然失败,因签名仅覆盖规范化后的SignedInfo子树。

Signature 验证不是解 Base64 后直接验字节 —— 你必须先对 SignedInfo 做排他性规范化(exclusive canonicalization),再用公钥验签。否则哪怕 XML 结构完全正确,sig.verify() 也一定失败。
为什么直接验 contentBytes 总是 false?
你当前的 verifySignature 方法传入的是整个响应 XML 的字节,但 SAML 签名只覆盖 SignedInfo 节点(含其子节点),且该节点在签名前已被按特定规则标准化(比如去掉无关空白、归一化命名空间声明)。直接传原始 XML 字符串或未规范化的节点内容,哈希值必然不匹配。
-
SignedInfo是签名的“输入原文”,不是整个Response - 规范化算法由
ds:CanonicalizationMethod@Algorithm指定,常见为http://www.w3.org/2001/10/xml-exc-c14n# - Java 原生
Node.getC14NMethod()不支持自动识别该算法,需手动调用org.jcp.xml.dsig.internal.dom.DOMCanonicalizer或用xmlsec库
用 xmlsec 库最稳(推荐)
Apache xmlsec 是 Java 生态验证 SAML 签名的事实标准,它自动处理:CanonicalizationMethod 解析、Reference URI 解析(如 #_response123456)、Transforms(如 enveloped-signature 剔除签名自身)、以及 SignatureMethod 映射(rsa-sha256 / rsa-sha1 自动适配)。
import org.apache.xml.security.signature.XMLSignature; import org.apache.xml.security.transforms.Transforms; import org.w3c.dom.Document; import org.w3c.dom.Element;// 已加载 Document doc Element sigElement = (Element) doc.getElementsByTagNameNS("https://www.php.cn/link/573a77b45da4e86a0fc93e5f76cc99ce#", "Signature").item(0); XMLSignature signature = new XMLSignature(sigElement, ""); boolean isValid = signature.checkSignatureValue(publicKey); // 自动做 c14n + verify
- 别自己解析
ds:SignatureValue和ds:SignedInfo——xmlsec内部已封装全部逻辑 - 确保 classpath 有
xmlsec-3.0.3.jar(适配 Java 11+)和xmlsec-java-3.0.3.jar - 若用 Keycloak 发出的响应,注意其
SignatureMethod很可能是rsa-sha256,旧版xmlsec(如 2.x)默认不支持,必须升到 3.x
手撸验证时,三个关键点不能漏
如果因合规或轻量要求必须不用第三方库,务必确认以下三步都严格对齐 SAML 响应中的 ds:Signature 描述:
-
取对节点:用 XPath 定位到
ds:SignedInfo元素,不是ds:Signature根节点 -
做对规范化:调用
signedInfoNode.getC14NMethod().canonicalizeSubtree(signedInfoNode),且参数必须设为true(保留注释)或false(剔除注释),取决于CanonicalizationMethod@Algorithm是否带#WithComments -
验对算法:从
ds:SignatureMethod@Algorithm提取实际算法(如http://www.w3.org/2001/04/xmldsig-more#rsa-sha256),映射为 Java 支持的字符串(SHA256withRSA),而非硬写RSA-SHA1
SAML 签名验证真正难的不是代码,而是对 ds:Reference 中 URI、Transforms、DigestMethod 这三者的联动理解 —— 它们共同定义了“到底哪段字节被签了”,漏掉任意一个环节,就等于在验一份假数据。










