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 WSS4JApache WSS4J2026/10/15

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(密钥库门面)。核心对象与职责:

对象所在包职责
WSSecHeaderdom.message在 SOAP 报文中插入并持有 wsse:Security 头
WSSecSignaturedom.messageXML 签名构造器(prepare → 选引用 → compute)
WSSecEncryptdom.messageXML 加密构造器(会话密钥加密数据 + 公钥加密会话密钥)
WSSecTimestamp / WSSecUsernameTokendom.message时间戳 / 用户名令牌构造器
Crypto(实现 Merlin)common.crypto密钥库/信任库加载与证书检索
WSSecurityEnginedom.engine验证侧入口,处理报文头全部安全元素
RequestDatadom.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_SHA256http://www.w3.org/2001/04/xmldsig-more#rsa-sha256默认选择
签名算法(旧)WSConstants.RSA_SHA1http://www.w3.org/2000/09/xmldsig#rsa-sha1仅对接老系统,新项目勿用
摘要算法MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256http://www.w3.org/2001/04/xmlenc#sha256与签名算法强度匹配
规范化WSConstants.C14N_EXCL_OMIT_COMMENTShttp://www.w3.org/2001/10/xml-exc-c14n#WithCommentsWS-Security 默认

💡 技术拓展:签名算法对比——

方案强度互操作面建议
RSA-SHA256128 位安全强度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-GCMWSConstants.AES_128_GCM认证加密,密文自带完整性校验新项目首选(本文采用)
AES-256-GCMWSConstants.AES_256_GCM同上,密钥更长高敏数据、合规要求 256 位时
AES-128-CBCWSConstants.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.UT1UsernameToken 已校验
WSConstants.SIGN2XML 签名已验证
WSConstants.ENCR4报文段已解密
WSConstants.TS8时间戳已校验
WSConstants.BST4096直附证书(BinarySecurityToken)已接收处理
WSConstants.UT_NOPASSWORD8192无口令 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 实测均可稳定复现,可直接作为线上告警的分级依据。

七、参考链接

  1. Apache WSS4J 官方主页:https://ws.apache.org/wss4j/
  2. WSS4J 3.0.6 javadoc(WSSecSignature/WSSecEncrypt 包索引):https://ws.apache.org/wss4j/apidocs/index.html
  3. Apache Santuario javadoc(JCEMapper/Init):https://santuario.apache.org/javadocs/3.0/
  4. W3C XML Signature 规范:https://www.w3.org/TR/xmldsig-core/
  5. W3C XML Encryption 规范:https://www.w3.org/TR/xmlenc-core/
  6. 本专栏前置篇:《Apache WSS4J 3.0.6 快速入门与集成示例》(最小闭环与集成坑)