Spring Boot 3.4迁移后调用SOAP服务遇403证书认证缺失问题
将Spring Boot从2.5版本升级至3.4版本后,调用客户端SOAP服务时出现403错误,提示「Missing certificate authentication」。已配置正确的keystore与cacerts,且同一主机下的REST API可正常获取响应。以下是Marshaller配置类:
@SpringBootConfiguration @EnableAutoConfiguration public class SoapConfiguration { @Bean public Jaxb2Marshaller awardsMarshaller() { Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("com.google.get"); return marshaller; } @Bean public AwardsClient awardsClient(Jaxb2Marshaller awardsMarshaller) throws Exception { AwardsClient client = new AwardsClient(); client.setDefaultUri(kpmgUrl); client.setMarshaller(awardsMarshaller); client.setUnmarshaller(awardsMarshaller); client.setMessageSender(getKpmgHttpComponentsMessageSender()); return client; } @Bean public HttpComponents5MessageSender getKpmgHttpComponentsMessageSender() throws Exception { final HttpComponents5MessageSender sender = new HttpComponents5MessageSender(); sender.setConnectionTimeout(Duration.ofMillis(kpmgConnectionTimeout)); sender.setReadTimeout(Duration.ofMillis(kpmgReadTimeout)); sender.setHttpClient(getKpmgHttpClient()); return sender; } @Bean public HttpClient getKpmgHttpClient() throws Exception { PoolingHttpClientConnectionManager httpClientConnectionManager; SSLConnectionSocketFactory sslSocketFactory = getSSLConnectionSocketFactory(); httpClientConnectionManager = new PoolingHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslSocketFactory).build()); httpClientConnectionManager.setDefaultMaxPerRoute(3000); httpClientConnectionManager.setMaxTotal(4000); httpClientConnectionManager.setDefaultConnectionConfig(ConnectionConfig.custom().setValidateAfterInactivity(TimeValue.ofMilliseconds(120000)) .setConnectTimeout(Timeout.ofMilliseconds(3000)).build()); HttpClientBuilder httpClientBuilder = HttpClients.custom() .setConnectionManager(httpClientConnectionManager).setConnectionManagerShared(true) .addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor()) ; return httpClientBuilder.build(); } private SSLConnectionSocketFactory getSSLConnectionSocketFactory() throws Exception { HostnameVerifier hostnameVerifier = null; hostnameVerifier = NoopHostnameVerifier.INSTANCE; try { return new SSLConnectionSocketFactory(getKpmgSSLContext(), hostnameVerifier); } catch (Exception e) { throw new Exception(e.getCause()); } } @Bean public SSLContext getKpmgSSLContext() { SSLContext sslContext; try { byte[] passwd = Base64.getDecoder().decode(keyStorePassword); byte[] keyStoreBytes = BaseEncoding.base64().decode(encodedkeyStore); String keypassword = new String(passwd, StandardCharsets.UTF_8); KeyStore keyStore = KeyStore.getInstance("jks"); byte[] trustStoreBytes = BaseEncoding.base64().decode(encodedTrustStore); KeyStore trustStore = KeyStore.getInstance("jks"); keyStore.load(new ByteArrayInputStream(keyStoreBytes), null); trustStore.load(new ByteArrayInputStream(trustStoreBytes), null); sslContext = new SSLContextBuilder().loadKeyMaterial(keyStore, keypassword.toCharArray()).loadTrustMaterial(new File("\\cacerts2.jks"), keypassword.toCharArray()) .build(); return sslContext; } catch (KeyManagementException | KeyStoreException | UnrecoverableKeyException e) { } catch (NoSuchAlgorithmException | CertificateException | IOException e) { } return null; } }
异常信息:
org.springframework.ws.client.WebServiceTransportException: Missing certificate authentication [403] at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:666) ~[spring-ws-core-4.0.11.jar:?] at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:588) ~[spring-ws-core-4.0.11.jar:?] at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:539) ~[spring-ws-core-4.0.11.jar:?] at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:391) ~[spring-ws-core-4.0.11.jar:?] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[?:?] at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52) ~[jersey-server-3.1.10.jar:?] at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:146) [jersey-server-3.1.10.jar:?] at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:189) [jersey-server-3.1.10.jar:?] at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$TypeOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:219) [jersey-server-3.1.10.jar:?] at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:93) [jersey-server-3.1.10.jar:?]
核心问题定位
从报错和代码来看,主要是客户端证书未被正确传递到SOAP服务端,结合Spring Boot版本升级的背景,重点检查SSLContext的构建逻辑是否存在兼容性问题或配置错误。
1. 修正KeyStore加载的密码问题
代码中keyStore.load(new ByteArrayInputStream(keyStoreBytes), null)传入了null作为KeyStore密码,这会导致KeyStore无法正确解锁(如果KeyStore本身设置了密码),进而无法读取其中的客户端证书。
修改方式:将null替换为KeyStore的密码(与后续loadKeyMaterial使用的密码一致):
keyStore.load(new ByteArrayInputStream(keyStoreBytes), keypassword.toCharArray()); trustStore.load(new ByteArrayInputStream(trustStoreBytes), keypassword.toCharArray()); // 若trustStore有独立密码,替换为对应密码
2. 统一信任库配置
代码中同时加载了base64解码的trustStore和本地文件cacerts2.jks,这会导致信任链逻辑冲突,应该统一使用已经加载好的trustStore对象,而非重复读取本地文件:
sslContext = new SSLContextBuilder() .loadKeyMaterial(keyStore, keypassword.toCharArray()) .loadTrustMaterial(trustStore, null) // 使用已加载的trustStore,而非本地文件 .build();
3. 修复异常吞掉的问题
原代码中所有catch块为空,直接返回null,这会导致SSLContext构建失败时无法看到具体错误信息,无法定位问题。修改为抛出异常或打印详细日志:
catch (KeyManagementException | KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | IOException e) { throw new RuntimeException("Failed to initialize SSLContext for SOAP client", e); }
4. 确保客户端证书被正确选择
如果KeyStore中有多个证书,服务端可能要求指定别名的证书,可以显式配置KeyManagerFactory来指定密钥别名:
// 在getKpmgSSLContext方法中添加 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keypassword.toCharArray()); // 若需要指定别名,可自定义X509KeyManager // KeyManager[] keyManagers = new KeyManager[]{new CustomX509KeyManager(kmf.getKeyManagers(), "your-cert-alias")}; sslContext = new SSLContextBuilder() .setKeyManagers(kmf.getKeyManagers()) .loadTrustMaterial(trustStore, null) .build();
5. 开启SSL调试日志
添加JVM启动参数-Djavax.net.debug=ssl,handshake,查看SSL握手过程的详细日志,确认:
- 客户端是否发送了证书
- 服务端是否接受该证书
- 信任链是否完整
6. 验证HTTP客户端兼容性
Spring Boot 3.x默认使用HttpClient 5,而2.5版本使用HttpClient 4,检查HttpComponents5MessageSender的配置是否完整,比如是否需要设置sslContext直接传递,而非通过HttpClient?
可以尝试简化配置,直接在HttpComponents5MessageSender上设置SSLContext:
@Bean public HttpComponents5MessageSender getKpmgHttpComponentsMessageSender() throws Exception { final HttpComponents5MessageSender sender = new HttpComponents5MessageSender(); sender.setConnectionTimeout(Duration.ofMillis(kpmgConnectionTimeout)); sender.setReadTimeout(Duration.ofMillis(kpmgReadTimeout)); sender.setSslContext(getKpmgSSLContext()); // 直接设置SSLContext return sender; }
最终修改后的getKpmgSSLContext示例
@Bean public SSLContext getKpmgSSLContext() throws Exception { byte[] passwd = Base64.getDecoder().decode(keyStorePassword); byte[] keyStoreBytes = BaseEncoding.base64().decode(encodedkeyStore); String keypassword = new String(passwd, StandardCharsets.UTF_8); // 加载KeyStore KeyStore keyStore = KeyStore.getInstance("jks"); keyStore.load(new ByteArrayInputStream(keyStoreBytes), keypassword.toCharArray()); // 加载TrustStore byte[] trustStoreBytes = BaseEncoding.base64().decode(encodedTrustStore); KeyStore trustStore = KeyStore.getInstance("jks"); trustStore.load(new ByteArrayInputStream(trustStoreBytes), keypassword.toCharArray()); // 初始化KeyManagerFactory KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keypassword.toCharArray()); // 构建SSLContext return new SSLContextBuilder() .setKeyManagers(kmf.getKeyManagers()) .loadTrustMaterial(trustStore, null) .build(); }
内容的提问来源于stack exchange,提问作者NOT_FOUND

