WSS4J在Web应用中SOAP请求签名验证失败问题咨询
我正在开发一个需要通过数字签名调用Web服务的任务:已经用JAX-WS生成了Web Service客户端,实现了SOAP Handler,使用Apache WSS4J库在请求发送前对SOAP信封签名,签名用的是加载到密钥库的.pfx文件。
本地Eclipse运行时一切正常,能收到服务器正确响应,但把代码集成到Web应用后,服务器端无法验证签名,返回soap:Server 75275错误。
我的疑问是:WSS4J是否可用于Web服务器环境下的SOAP请求签名?如果不可行,有哪些替代方案或解决方向?
相关代码
SOAP Handler签名逻辑
Document doc = soapEnv.getOwnerDocument(); WSSecSignature builder = new WSSecSignature(); WSSecTimestamp timestamp = new WSSecTimestamp(); WSSecUsernameToken usernametoken = new WSSecUsernameToken(); usernametoken.setPasswordType(WSConstants.PASSWORD_TEXT); usernametoken.setUserInfo(SOAP_USER, SOAP_USER_PASS); usernametoken.addNonce(); usernametoken.addCreated(); WSSecHeader secHeader = new WSSecHeader(); // secHeader.setMustUnderstand(true); secHeader.insertSecurityHeader(doc); usernametoken.build(doc, secHeader); timestamp.build(doc, secHeader); builder.setUserInfo(KEYSTORE_ALIAS, KEYSTORE_PWD); builder.setX509Certificate(cert); builder.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE); builder.setSignatureAlgorithm(WSConstants.RSA_SHA1); builder.setSigCanonicalization(WSConstants.C14N_EXCL_OMIT_COMMENTS); builder.setDigestAlgo(WSConstants.SHA1); builder.setUseSingleCertificate(true); builder.appendBSTElementToHeader(secHeader); // Set message parts to sign List<WSEncryptionPart> parts = new ArrayList<WSEncryptionPart>(); WSEncryptionPart bodyPart1 = new WSEncryptionPart("Body", WSConstants.URI_SOAP11_ENV, "Content"); bodyPart1.setElement(soapBody); parts.add(bodyPart1); builder.setParts(parts); // Set keystore and sign the document Properties properties = new Properties(); properties.setProperty("org.apache.wss4j.crypto.provider", "class org.apache.ws.security.components.crypto.Merlin"); Merlin crypto = (Merlin) CryptoFactory.getInstance(properties); crypto.setKeyStore(store); doc = builder.build(doc, crypto, secHeader); soapMsg.saveChanges();
获取证书和密钥库的代码
private boolean getX509Certificate() { String keyAlias = ""; try { Security.addProvider(new BouncyCastleProvider()); FileInputStream stream = new FileInputStream(KEYSTORE_FILE); //.pfx file this.store = KeyStore.getInstance("PKCS12"); store.load(stream, KEYSTORE_PWD.toCharArray()); Enumeration<String> aliases = store.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); if (store.getCertificate(alias).getType().equals("X.509")) { keyAlias = alias; } } this.cert = (X509Certificate) store.getCertificate(keyAlias); return true; } catch (Exception e) { e.printStackTrace(); } return false; }
异常信息
soap:Server.75275 Response Value:soap:Server.75275
2018-05-11 18:56:17,977 ERROR [WebService](WFA:132301310 - 152335722 Associate:349730687 IE=349730685) Exception javax.xml.ws.soap.SOAPFaultException:
2018-05-11 18:56:17,978 ERROR [WebService](WFA:132301310 - 152335722 Associate:349730687 IE=349730685) [org.apache.axis2.jaxws.marshaller.impl.alt.MethodMarshallerUtils.createSystemException(MethodMarshallerUtils.java:1363)
org.apache.axis2.jaxws.marshaller.impl.alt.MethodMarshallerUtils.demarshalFaultResponse(MethodMarshallerUtils.java:1089)
org.apache.axis2.jaxws.marshaller.impl.alt.DocLitWrappedMinimalMethodMarshaller.demarshalFaultResponse(DocLitWrappedMinimalMethodMarshaller.java:684)
org.apache.axis2.jaxws.client.proxy.JAXWSProxyHandler.getFaultResponse(JAXWSProxyHandler.java:626)
org.apache.axis2.jaxws.client.proxy.JAXWSProxyHandler.createResponse(JAXWSProxyHandler.java:566)
org.apache.axis2.jaxws.client.proxy.JAXWSProxyHandler.invokeSEIMethod(JAXWSProxyHandler.java:432)
org.apache.axis2.jaxws.client.proxy.JAXWSProxyHandler.invoke(JAXWSProxyHandler.java:213)
com.sun.proxy.$Proxy256.synchronousSendAndReceive(Unknown Source)
回答
首先明确:WSS4J完全可以在Web服务器环境下用于SOAP请求签名,你本地正常、Web环境失败的问题大概率是环境差异导致的,而非库本身不支持。下面给你几个排查方向和解决建议:
1. 密钥库文件路径问题
你在本地用FileInputStream(KEYSTORE_FILE)加载.pfx文件,到了Web应用中,这个路径可能不再有效。Web容器下,文件路径是相对于容器的工作目录,而非你本地项目的路径。
解决办法:
- 把.pfx文件放到Web应用的
WEB-INF/resources目录下,用类加载器获取输入流:InputStream stream = getClass().getResourceAsStream("/WEB-INF/resources/your-keystore.pfx"); - 或者配置绝对路径,通过环境变量或配置文件注入,避免硬编码。
2. 安全提供者加载问题
你在代码中手动添加了BouncyCastleProvider(),但Web服务器可能已经有自己的安全提供者配置,或者类加载器隔离导致这个提供者没有被正确加载。
解决办法:
- 检查Web服务器的JRE/lib/security/java.security文件,确认BouncyCastle是否已经被注册为安全提供者。如果没有,可以在那里添加:
(N是序列数,放在现有提供者后面)security.provider.N=org.bouncycastle.jce.provider.BouncyCastleProvider - 或者在代码中用
Security.insertProviderAt(new BouncyCastleProvider(), 1)确保它被优先加载,避免类加载器问题。
3. SOAP消息的Canonicalization(规范化)差异
本地和Web环境下,SOAP消息的规范化处理可能因为JAX-WS实现、容器的XML解析器不同,导致签名的哈希值不一致。你用的是C14N_EXCL_OMIT_COMMENTS,要确保两边的XML解析行为一致。
解决办法:
- 开启WSS4J的调试日志,对比本地和Web环境下生成的SOAP消息(特别是签名部分)是否一致。可以添加日志配置:
log4j.logger.org.apache.wss4j=DEBUG - 检查Web容器使用的XML解析器(比如Xerces vs 内置的JDK解析器),是否和本地Eclipse使用的一致。必要时可以在Web应用中指定使用特定的XML解析器。
4. 类加载器隔离问题
Web容器(比如Tomcat、WebLogic)有自己的类加载器层次,可能导致WSS4J的类和JAX-WS的类来自不同的类加载器,出现奇怪的兼容性问题。
解决办法:
- 确保Web应用的
WEB-INF/lib中的WSS4J、JAX-WS相关依赖版本和本地运行的一致,避免容器提供的旧版本覆盖你的依赖。 - 如果是Tomcat,可以在
conf/catalina.properties中添加org.apache.wss4j相关包的优先加载配置,或者把依赖放到容器的lib目录(不推荐,除非必要)。
5. 时间戳同步问题
你添加了WSSecTimestamp,如果Web服务器的系统时间和Web服务端的时间差过大,可能导致签名验证失败。
解决办法:
- 检查Web服务器的系统时间,确保和服务端时间同步(误差尽量在几分钟内)。
- 可以调整时间戳的有效期,比如
timestamp.setTimeToLive(300)(设置5分钟有效期)。
替代方案(如果上述都无效)
如果确实遇到WSS4J在Web环境下的兼容性问题,你可以考虑:
- 使用JAX-WS内置的
javax.xml.ws.handler.soap.SOAPHandler结合Java Security API手动实现签名,但复杂度较高。 - 改用Apache CXF作为JAX-WS客户端实现,它内置了对WS-Security的支持,配置更简洁,在Web环境下的兼容性更好。
内容的提问来源于stack exchange,提问作者Arun

