Apache WSS4J 3.0.6 签名与加密 API 详解
详解 Apache WSS4J 3.0.6 的 WSSecSignature 与 WSSecEncrypt 两套核心 API:prepare-compute 签名流水线、KeyIdentifier 六种寻址、RSA-SHA256 与 AES-128-GCM 算法链、JCEMapper 算法映射实测、验证侧 RequestData 配置与篡改报错的 code 语义,全部代码与回显实测通过。
Apache WSS4J 3.0.6 签名与加密 API 详解
目标:吃透 WSS4J 3.0.6 的签名与加密两套核心 API——WSSecSignature 的 prepare-compute 流水线、WSSecEncrypt 的会话密钥与密钥传输模型、算法与密钥寻址的选型依据、验证侧 RequestData 配置,以及两类实测失败场景的报错语义。全部代码、命令与回显均已在 Ubuntu 22.04、OpenJDK 17.0.20.1 实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- JDK:OpenJDK 17.0.20.1;Apache Maven 3.6.3
- 组件版本:Apache WSS4J 3.0.6(wss4j-ws-security-dom + wss4j-ws-security-common)、Apache Santuario xmlsec 3.0.6、BouncyCastle bcprov-jdk18on 1.82
- 密钥库(本文共用,口令统一
q1w2e3r4):client.jks(JKS,别名 client,签名方私钥)、server.bcfks(BCFKS,别名 server,接收方私钥)、trust.jks(JKS,含 client/server 两张可信证书) - 验证工程:
/root/wss4j-demo
密钥库生成与工程搭建见前置篇《Ubuntu 22.04 安装 Apache WSS4J 指南》。本文所有代码块摘自实测工程 WssDemo.java,XML 片段为真实运行输出(每次运行 Nonce/CipherValue 不同,属正常)。
二、总览:两条流水线与核心对象
WSS4J 3.0.6 的 DOM 层(org.apache.wss4j.dom)围绕「构造器流水线」组织:签名走 WSSecSignature,加密走 WSSecEncrypt,两者共享 WSSecHeader(持有 wsse:Security 头的引用)与 Crypto(密钥库门面)。核心对象与职责:
| 对象 | 所在包 | 职责 |
|---|---|---|
WSSecHeader | dom.message | 在 SOAP 报文中插入并持有 wsse:Security 头 |
WSSecSignature | dom.message | XML 签名构造器(prepare → 选引用 → compute) |
WSSecEncrypt | dom.message | XML 加密构造器(会话密钥加密数据 + 公钥加密会话密钥) |
WSSecTimestamp / WSSecUsernameToken | dom.message | 时间戳 / 用户名令牌构造器 |
Crypto(实现 Merlin) | common.crypto | 密钥库/信任库加载与证书检索 |
WSSecurityEngine | dom.engine | 验证侧入口,处理报文头全部安全元素 |
RequestData | dom.handler | 验证侧配置(Crypto、回调、TTL、重放缓存) |
构造器实例化方式统一为「先建 WSSecHeader,再把它传给各构造器」:
WSSecHeader secHeader = new WSSecHeader(doc);
secHeader.insertSecurityHeader(); // 在 soap:Header 里插入 <wsse:Security>
WSSecSignature sign = new WSSecSignature(secHeader);
💡 说明:WSS4J 3.0.6 的 DOM 层直接操作
org.w3c.dom.Document,不要求先封装成 SAAJ 对象,也不依赖应用服务器,可嵌进任何持有 DOM 的处理链(网关过滤器、单元测试、独立进程)。
三、签名详解:WSSecSignature
3.1 密钥库加载:CryptoFactory 与 Merlin
Crypto 是密钥库的门面接口,默认实现 Merlin 通过属性驱动。实测的工厂方法(签名方加载含私钥的 JKS):
static Crypto crypto(String file, String type, String provider) throws WSSecurityException {
Properties p = new Properties();
p.setProperty("org.apache.wss4j.crypto.provider", "org.apache.wss4j.common.crypto.Merlin");
p.setProperty("org.apache.wss4j.crypto.merlin.keystore.type", type);
p.setProperty("org.apache.wss4j.crypto.merlin.keystore.file", file);
p.setProperty("org.apache.wss4j.crypto.merlin.keystore.password", "q1w2e3r4");
if (provider != null) {
p.setProperty("org.apache.wss4j.crypto.merlin.keystore.provider", provider);
}
return CryptoFactory.getInstance(p);
}
属性语义逐项说明:
crypto.provider:实现类,默认已是 Merlin,显式写出利于排障。keystore.type/file/password:密钥库类型、路径、读取口令。keystore.provider:密钥库类型的 JCE 提供者。BCFKS 必须传BC(BouncyCastle),JKS 传null即可;实测同一份代码加载 BCFKS 无需改任何其他逻辑。
按别名检索证书用 CryptoType(实测取服务端证书):
Crypto crypto = crypto("/root/keystores/server.bcfks", "BCFKS", "BC");
CryptoType ct = new CryptoType(CryptoType.TYPE.ALIAS);
ct.setAlias("server");
X509Certificate cert = crypto.getX509Certificates(ct)[0];
运行回显:
server 证书主体 = CN=server.demows.example,O=Demo,L=Beijing,C=CN | 签名算法 = SHA256withRSA
3.2 签名四步流水线
WSSecSignature 的标准用法是四步(每一步实测代码):
WSSecSignature sign = new WSSecSignature(secHeader);
sign.setUserInfo("client", "q1w2e3r4"); // 密钥别名 + 私钥口令
sign.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE); // 密钥寻址方式
sign.setSignatureAlgorithm(WSConstants.RSA_SHA256); // 签名算法
sign.setDigestAlgo(MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); // 摘要算法
sign.setSigCanonicalization(WSConstants.C14N_EXCL_OMIT_COMMENTS); // 规范化
sign.prepare(clientCrypto()); // 第 1 步:解析私钥,生成签名上下文与 BST
List<WSEncryptionPart> parts = new ArrayList<>();
parts.add(new WSEncryptionPart("Body", SOAP_NS, "Content")); // 第 2 步:声明要签哪些报文段
List<Reference> refs = sign.addReferencesToSign(parts); // 第 3 步:生成 <SignedInfo><Reference>
sign.computeSignature(refs); // 第 4 步:计算 <SignatureValue>
sign.prependBSTElementToHeader(); // 第 5 步:把 BST 放进 Security 头
逐项说明关键参数:
setUserInfo(alias, keyPassword):签名用私钥的别名与私钥口令(JKS 里 keypass 与 storepass 同为q1w2e3r4)。addReferencesToSign的WSEncryptionPart(name, ns, type):"Content"表示只签元素内容(Body 内部),"Element"表示连标签一起签。实测组合场景同时签 Timestamp 与 Body:
parts.add(new WSEncryptionPart("Timestamp", WSConstants.WSU_NS, "Element"));
parts.add(new WSEncryptionPart("Body", SOAP_NS, "Content"));
getSignatureValue():签名后可取回签名值做展示,RSA 2048 实测恒为 256 字节。
签名完成后报文头实测片段(节选,CipherValue 类长值已截断展示):
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<wsse:Security soap:mustUnderstand="1" xmlns:wsse="...">
<wsse:BinarySecurityToken ...>
<!-- Base64 编码的 client 证书 -->
</wsse:BinarySecurityToken>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#WithComments"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#...">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>5l0b9vOF3VY1cgNPw1kUVIM2mIMyZyzkzHBrUQJPepw=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
bWpjHTtI4CGPj1MhVf9dTmFRKrC57BRr0F3m2FW1HvpWoZp...(共 256 字节)
</ds:SignatureValue>
</ds:Signature>
</wsse:Security>
</soap:Header>
验证侧实测结果:
签名值字节数 = 256
验证通过,处理到的安全动作 = [SIGN, BST]
💡 技术拓展:密钥寻址方式对比——
setKeyIdentifierType常用六种(实测 3.0.6 全部编译期可用的常量):BST_DIRECT_REFERENCE(证书直附报文,1)、ISSUER_SERIAL(只传颁发者+序列号,2)、X509_KEY_IDENTIFIER(只传公钥指纹标识,3)、X509_ISSUER_SERIAL(同 2 的别名形态)、SKI_KEY_IDENTIFIER(X.509 v3 的 SKI 扩展,256)、THUMBPRINT(SHA-1 指纹,8)。数字为实测常量值。
寻址方式 报文开销 前提 典型场景 BST_DIRECT_REFERENCE 大(携带整张证书) 无 点对点、无共享信任库时最省事 ISSUER_SERIAL 小 验证方预置对方证书 网关集群共享信任库 THUMBPRINT 小 验证方按指纹索引证书 固定合作方、证书轮换少 本文采用 BST_DIRECT_REFERENCE(签名)+ ISSUER_SERIAL(加密) 组合:签名侧直附证书让验证零预置;加密侧收方证书是「我方选对方公钥」的场景,用颁发者+序列号定位即可,报文更精简。
3.3 签名算法链选型
实测可用的算法组合(WSS4J 3.0.6 + JDK 17):
| 环节 | 常量 | URI(实测) | 建议 |
|---|---|---|---|
| 签名算法 | WSConstants.RSA_SHA256 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 | 默认选择 |
| 签名算法(旧) | WSConstants.RSA_SHA1 | http://www.w3.org/2000/09/xmldsig#rsa-sha1 | 仅对接老系统,新项目勿用 |
| 摘要算法 | MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256 | http://www.w3.org/2001/04/xmlenc#sha256 | 与签名算法强度匹配 |
| 规范化 | WSConstants.C14N_EXCL_OMIT_COMMENTS | http://www.w3.org/2001/10/xml-exc-c14n#WithComments | WS-Security 默认 |
💡 技术拓展:签名算法对比——
方案 强度 互操作面 建议 RSA-SHA256 128 位安全强度 WS-Security 1.1 主流实现均支持 新项目首选(本文采用) RSA-SHA1 <80 位(可碰撞) 兼容老系统 仅存量对接,规划下线 ECDSA-SHA256 同级且密钥更短 对端需支持 ECDSA 移动端/证书链受限时评估 本文采用 RSA-SHA256 + SHA-256 摘要 + 独占 C14N,理由:SHA-1 已可构造碰撞,安全评审基本必挡;ECDSA 对接面在老对端上易踩不支持。
四、加密详解:WSSecEncrypt
4.1 两层密钥模型
XML Encryption 的标准模型是「双层密钥」:真正的报文数据用一次性会话密钥(对称)加密;会话密钥再用接收方公钥(非对称)加密后随报文传输。WSSecEncrypt 把这两层封装在一次 build() 里:
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(128);
SecretKey sessionKey = kg.generateKey(); // 第一层:一次性会话密钥
WSSecEncrypt enc = new WSSecEncrypt(secHeader);
enc.setUserInfo("server"); // 第二层:接收方密钥别名(用其公钥加密会话密钥)
enc.setKeyIdentifierType(WSConstants.ISSUER_SERIAL);
enc.setSymmetricEncAlgorithm(WSConstants.AES_128_GCM);
Document encDoc = enc.build(serverCrypto(), sessionKey); // 加密 Body,返回新文档
逐项说明:
setUserInfo("server"):只传别名,不带口令——加密侧只用公钥;私钥口令在解密侧通过回调提供。setSymmetricEncAlgorithm(WSConstants.AES_128_GCM):会话密钥的数据加密算法,URI 实测为http://www.w3.org/2009/xmlenc11#aes128-gcm。build(crypto, sessionKey):返回新的 Document(原 doc 不再可用),Body 内容被替换为<xenc:EncryptedData>。- 密钥传输算法默认即 RSA-OAEP-MGF1P(
http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p),可用enc.setKeyEncAlgo(WSConstants.KEYTRANSPORT_RSAOAEP)显式声明。
加密后报文实测片段(节选):
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<wsse:Security soap:mustUnderstand="1">
<xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
<ds:KeyInfo>
<wsse:SecurityTokenReference>
<ds:X509Data><ds:X509IssuerSerial>
<ds:X509IssuerName>CN=server.demows.example,O=Demo,L=Beijing,C=CN</ds:X509IssuerName>
<ds:X509SerialNumber>...</ds:X509SerialNumber>
</ds:X509IssuerSerial></ds:X509Data>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>f2Dk+9LvB...(RSA 加密后的会话密钥)</xenc:CipherValue>
</xenc:CipherData>
<xenc:ReferenceList>
<xenc:DataReference URI="#..."/>
</xenc:ReferenceList>
</xenc:EncryptedKey>
</wsse:Security>
</soap:Header>
<soap:Body>
<xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
<xenc:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#aes128-gcm"/>
<xenc:CipherData>
<xenc:CipherValue>KqE8iDD...(AES-GCM 加密后的 Body)</xenc:CipherValue>
</xenc:CipherData>
</xenc:EncryptedData>
</soap:Body>
加密侧完整性实测回显:
密文侧 Body 明文可见性 = 不可见(正常)
解密后 Body 内容 = order-1001|item=SSD-2T|qty=3|amount=1299.00
解密后与原文一致 = true
4.2 解密侧:回调提供私钥口令
解密发生在 WSSecurityEngine.processSecurityHeader 内:引擎发现 <xenc:EncryptedKey> 后,用 decCrypto 找到 server 私钥,并通过 WSPasswordCallback(usage=DECRYPT,实测常量值 1)向应用索要私钥口令。实测回调骨架:
reqData.setDecCrypto(crypto("/root/keystores/server.bcfks", "BCFKS", "BC"));
reqData.setCallbackHandler(callbacks -> {
for (Callback cb : callbacks) {
WSPasswordCallback pc = (WSPasswordCallback) cb;
if (pc.getUsage() == WSPasswordCallback.DECRYPT) { // 解密 EncryptedKey
pc.setPassword("q1w2e3r4");
} else if (pc.getUsage() == WSPasswordCallback.USERNAME_TOKEN) { // 校验 UT
pc.setPassword("q1w2e3r4");
} else {
throw new UnsupportedCallbackException(cb);
}
}
});
实测解密验证结果:
验证通过,处理到的安全动作 = [ENCR]
4.3 算法映射:JCEMapper 与 Init.init()
密钥传输的 URI 到 JCE 名称的转换由 Santuario 的 JCEMapper 完成,且依赖入口处的 Init.init()。实测探针(同一 JVM 前后对比):
beforeInit oaep map = null
afterInit oaep map = RSA/ECB/OAEPPadding
未初始化时 WSSecEncrypt.build() 直接抛(实测原文):
org.apache.wss4j.common.ext.WSSecurityException: unsupported key transport encryption algorithm: No such algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"
结论:任何用到 JCEMapper.translateURItoJCEID 的路径(加密密钥传输、数据加密算法解析)都必须先 Init.init()。把它固定为进程入口第一行是唯一稳妥做法(幂等可重复调用)。
💡 技术拓展:数据加密算法对比——
方案 常量 特点 建议 AES-128-GCM WSConstants.AES_128_GCM认证加密,密文自带完整性校验 新项目首选(本文采用) AES-256-GCM WSConstants.AES_256_GCM同上,密钥更长 高敏数据、合规要求 256 位时 AES-128-CBC WSConstants.AES_128老实现默认,无内建完整性 仅对接存量系统 本文采用 AES-128-GCM,理由:CBC 在 WS-Security 语境下的填充预言机风险已有大量公开研究,GCM 一并解决了机密性与完整性;密钥传输沿用 RSA-OAEP-MGF1P(RSA-1.5 因 Bleichenbacher 攻击史不再用于新系统)。
五、验证侧:WSSecurityEngine 与 RequestData
5.1 引擎入口与配置
验证侧一个入口处理报文头里全部安全元素(签名、加密、UT、时间戳),配置集中在 RequestData:
WSSecurityEngine engine = new WSSecurityEngine();
RequestData reqData = new RequestData();
reqData.setSigVerCrypto(trustCrypto()); // 验签用的信任库
reqData.setDecCrypto(serverCrypto()); // 解密用的密钥库(含私钥)
reqData.setCallbackHandler(utCallback("q1w2e3r4")); // 口令回调(UT 比对/私钥解密)
reqData.setNonceReplayCache(new MemoryReplayCache()); // UT Nonce 重放缓存
reqData.setTimestampReplayCache(new MemoryReplayCache()); // 时间戳重放缓存
reqData.setTimeStampTTL(300); // 时间戳有效窗口(秒)
WSHandlerResult result = engine.processSecurityHeader(doc, reqData);
逐项说明:
sigVerCrypto与decCrypto分开配置:验签只需要「可信证书表」,解密需要「自己私钥」——两者常常来自不同密钥库。timeStampTTL=300:报文时间戳Expires超过当前时间 300 秒即拒绝(实测默认即 300)。- 重放缓存:带 Nonce 的 UsernameToken 与时间戳都可被重放,实测接入内存缓存后同一 Nonce 二次提交会被拒。
5.2 结果解读:动作码与 TAG_ACTION
WSHandlerResult.getResults() 返回每个安全元素的处理结果,动作码经 WSSecurityEngineResult.TAG_ACTION 取出(实测)。3.0.6 常用动作码:
| 常量 | 实测值 | 含义 |
|---|---|---|
WSConstants.UT | 1 | UsernameToken 已校验 |
WSConstants.SIGN | 2 | XML 签名已验证 |
WSConstants.ENCR | 4 | 报文段已解密 |
WSConstants.TS | 8 | 时间戳已校验 |
WSConstants.BST | 4096 | 直附证书(BinarySecurityToken)已接收处理 |
WSConstants.UT_NOPASSWORD | 8192 | 无口令 UsernameToken(仅身份标识) |
实测组合场景(时间戳 + 摘要 UT + 签名 Body 与 Timestamp + 加密 Body)的验证输出:
验证通过,处理到的安全动作 = [TS, UT, SIGN, BST, ENCR]
组合报文字节数(序列化后) = 7563
密文侧 Body 明文可见性 = 不可见(正常)
解密后 Body 内容 = order-1001|item=SSD-2T|qty=3|amount=1299.00
解密后与原文一致 = true
六、失败场景实测与报错语义
两类典型失败均在实测中复现,报错原文与 code 如下(验签环节的异常类型为 org.apache.wss4j.common.ext.WSSecurityException)。
场景一:签名后篡改 Body(把 order-1001 改写为 order-9999)再验证,实测拒绝:
[场景1] 拒绝成功: code=FAILED_CHECK message=The signature or decryption was invalid
定位:摘要比对失败(Reference validation)。这是签名「防篡改」的直接证据——Body 任何一字节变化都会导致 DigestValue 对不上。
场景二:时间戳过期(把 wsu:Created/wsu:Expires 重写为 30 分钟前)再验证,实测拒绝:
[场景2] 拒绝成功: code=MESSAGE_EXPIRED message=Invalid timestamp: The message timestamp has expired
定位:引擎按 timeStampTTL 判定时间窗失效。code 的实际意义:MESSAGE_EXPIRED 专门标识时效类拒绝,与签名类失败可编程区分(wse.getErrorCode() 判断分支)。
💡 技术拓展:失败分支的工程化处理建议——按
getErrorCode()分流:FAILED_CHECK(签名/解密无效)通常意味着报文被篡改或密钥不匹配,应直接断连并告警;MESSAGE_EXPIRED多为时钟漂移,优先核对两端 NTP 而非重放报文;FAILED_AUTHENTICATION(UT 口令错)宜限速防爆破。三类 code 实测均可稳定复现,可直接作为线上告警的分级依据。
七、参考链接
- Apache WSS4J 官方主页:https://ws.apache.org/wss4j/
- WSS4J 3.0.6 javadoc(WSSecSignature/WSSecEncrypt 包索引):https://ws.apache.org/wss4j/apidocs/index.html
- Apache Santuario javadoc(JCEMapper/Init):https://santuario.apache.org/javadocs/3.0/
- W3C XML Signature 规范:https://www.w3.org/TR/xmldsig-core/
- W3C XML Encryption 规范:https://www.w3.org/TR/xmlenc-core/
- 本专栏前置篇:《Apache WSS4J 3.0.6 快速入门与集成示例》(最小闭环与集成坑)