【问题标题】:CXF Failover Conduit modified on the fly - guarantee same SSL session and client thread safety?动态修改的 CXF 故障转移管道 - 保证相同的 SSL 会话和客户端线程安全?
【发布时间】:2025-12-20 17:45:11
【问题描述】:

我想为我们的应用程序中的两个 JAX-WS/RS 客户端实现 CXF 的故障转移功能,这些客户端必须使用客户端证书通过 HTTPS 调用远程 Web 服务。有 2 台远程服务器:主服务器 + 备用服务器。

我有点迷失在故障转移发生后如何保证相同的功能(使用正确的 TLS 参数/SSL 会话)。

JAX-WS 客户端

有 6 个 ClientServices,它们 extend AbstractClientServiceImpl 使用相同的 PortType wsClient bean 和远程服务器的相同 basePath,但是它们使用自己的 String getEndpointUrl(){ return "X";} 方法将它们设置为被调用服务的最后一个 uri 部分。

基本路径:https://remote1.server.com:443/api

备用地址:https://remote2.server.com:443/api

请查看代码 - ClientEndpointAddressInterceptor。使用这个拦截器,我可以组合 basePath + lastUriPart 并为特定的 ClientService 调用正确的目标端点——即使发生故障转移。例如:

ClientService1.class https://remote1.server.com:443/api/service1的目标端点

ClientService2.class 的目标端点:https://remote1.server.com:443/api/service2

两周来,我一直在为正确的设置/配置而苦苦挣扎。 如果我没有像这样将 tlsClientParameters 或 HttpClientPolicy 添加到 extensor,那么在 发生故障转移之后,我将无法看到新创建的任何 TLS 设置导管!

// ssl settings
endpointInfo.addExtensor(tlsClientParameters);

我不知道这是否是正确的方法,但是通过这种 hacky 解决方法,我可以设法为 2 个远程调用(主要 +备用远程服务器地址)- 客户端证书的 SAN 具有两个服务器的 DNS 名称。

技术:SpringBoot v2.1 + CXF 3.3.0 + Tomcat8.5

@Bean
public PortType wsClient(Properties properties,
                                        TLSClientParameters tlsClientParameters,
                                        LoggingFeature loggingFeature,
                                        ClientEndpointAddressFeature clientEndpointAddressFeature) {
        return createClient(properties, huTlsClientParameters, loggingFeature, 
    createFailoverFeature(properties.getFailover().getAddresses(), properties.getFailover().getRetryDelay()), 
                clientEndpointAddressFeature);
}

private FailoverFeature createFailoverFeature(String[] alternateAddresses, long failOverRetryDelay) {

    final FailoverFeature failOverFeature = new FailoverFeature();
    final SequentialStrategy strategy = new SequentialStrategy();
    strategy.setAlternateAddresses(Arrays.asList(alternateAddresses));
    strategy.setDelayBetweenRetries(failOverRetryDelay);
    failOverFeature.setStrategy(strategy);
    return failOverFeature;
}

private PortType createClient(Properties properties, TLSClientParameters tlsClientParameters, WebServiceFeature... features) {
    final Service service = new Service();
    final PortType client = service.getPortType(features);
    final Client clientProxy = ClientProxy.getClient(client);
    final EndpointInfo endpointInfo = clientProxy.getEndpoint().getEndpointInfo();

    final HTTPClientPolicy httpClientPolicy = Optional.ofNullable(endpointInfo.getExtensor(HTTPClientPolicy.class))
            .orElseGet(() -> {
                // if there is no XYFeature, policy has to be initialized at this point
                final HTTPClientPolicy policy = new HTTPClientPolicy();
                policy.setAccept(HuHttpHeaders.HEADER_ACCEPT_VALUE);
                endpointInfo.addExtensor(policy);
                return policy;
            });
    // timeout settings
    httpClientPolicy.setConnectionTimeout(properties.getConnectionTimeout());
    httpClientPolicy.setReceiveTimeout(properties.getReadTimeout());
    // set content-length by default
    httpClientPolicy.setAllowChunking(false);
    // ssl settings
    endpointInfo.addExtensor(tlsClientParameters);
    // set global requestContext
    setRequestContext((BindingProvider) client, properties.getUrl());
    return client;
}


private void setRequestContext(BindingProvider bp, String server) {
    bp.getRequestContext().put(Message.ENDPOINT_ADDRESS, server);
    bp.getRequestContext().put(ClientImpl.THREAD_LOCAL_REQUEST_CONTEXT, true);
    bp.getRequestContext().put(Message.SCHEMA_VALIDATION_ENABLED, true);
    bp.getRequestContext().put(BindingProvider.SOAPACTION_USE_PROPERTY, true);
}


public class ClientEndpointAddressOutInterceptor extends AbstractPhaseInterceptor<Message> {
    public ClientEndpointAddressOutInterceptor() {
        super(Phase.PREPARE_SEND);
        addBefore(MessageSenderInterceptor.class.getName());
    }

    @Override
    public void handleMessage(Message message) throws Fault {
        final String previousEndpointAddress = (String) message.get(Message.ENDPOINT_ADDRESS);
        final String lastUriPath = (String) message.get("lastUriPath");
        message.put(Message.ENDPOINT_ADDRESS, previousEndpointAddress + lastUriPath);
   }
}
public abstract class AbstractClientServiceImpl implements ClientService {
        public AbstractClientServiceImpl(PortType PortType) {
            this.portType = portType;
        }
        @Override
        public HttpStatus sendRequest(String xmlData) {
            ...
            final BindingProvider bindingProvider = (BindingProvider) this.portType;
            try {
            // set http header for this particular request          
            // also store bindingProvider.getRequestContext().put("lastUriPath", getEndpointUrl()); 
            HttpHeaderUtil.setHttpHeader(getSoapActionUrl(), bindingProvider, getEndpointUrl());
                execute(xmlData, createSoapHeader());
            } catch (Exception ex) {
                ...
            }
            ...
        }
        // last uri part 
        protected abstract String getEndpointUrl();
        // execute is responsible for calling a particular service. e.g: in ClientService1.class portType.callService1(xmlData);
        protected abstract void execute(String xmlData, TransactionHeader transactionHeader);
}

问题

JAX-WS 客户端

  • 发生故障转移后,先前设置的 (thread-local-requests,true) 设置会发生什么情况? 6 ClientServices 类调用的以下服务调用之后会保持线程安全吗?
  • 我有一个要求,我应该使用 SSL 会话重用机制。如果 CXF 的故障转移功能移除了管道并创建了一个新管道,那么如何在故障转移后再次应用它?或者Tomcat是否以某种方式处理了这个并且不需要为此烦恼?我不是 CXF 专家,在 CXF 的站点/邮件列表中没有找到很多与 SSL 会话相关的信息。

JAX-RS 客户端

实际上与上面针对 JAX-WS 客户端解决的 2 个关注点/问题相同。

唯一的区别是 RS 有 3 个客户端调用方法,使用声明为

的相同客户端实例
private WebClient webClient(){
   final JAXRSClientFactoryBean clientFactoryBean = new JAXRSClientFactoryBean();
   clientFactoryBean.setThreadSafe(true);
   final WebClient webClient = clientFactoryBean.createWebClient();
        final ClientConfiguration config = WebClient.getConfig(webClient);
        config.getRequestContext().put(HTTPConduit.NO_IO_EXCEPTIONS, Boolean.TRUE);
        // ssl settings
    config.getEndpoint().getEndpointInfo().addExtensor(tlsClientParameters);
    return webClient;
}

提前感谢您的帮助。

【问题讨论】:

    标签: java spring-boot ssl tomcat cxf


    【解决方案1】:

    解决方案是使用 CXF 的 HTTPConduitConfigurator,详见此处:https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=49941#ClientHTTPTransport(includingSSLsupport)-HowtouseHTTPConduitConfigurer?

    HTTPConduitConfigurer httpConduitConfigurer = new HTTPConduitConfigurer() {
        public void configure(String name, String address, HTTPConduit c) {
            c.setTlsClientParameters(_tlsParams);
        }
    }
    bus.setExtension(httpConduitConfigurer, HTTPConduitConfigurer.class);
    

    这将在所有创建的管道上设置 TLS 客户端参数,例如使用故障转移时。

    【讨论】:

    • 感谢您的回复。这听起来很有希望,但是我们已经切换到硬件解决方案(apache 代理),而不是依赖 cxf 的故障转移机制。不管怎样,我会接受你的回答