You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 11:50:55