I would like to implement CXF's Failover feature for both JAX-WS/RS clients in our application which have to call remote webservices via HTTPS using client certificates. 2 remote servers are in place: Primary + Alternate.
I am a bit lost how to guarantee the same functionality(with the right TLS params/SSL session) after the failover occurs.
JAX-WS client
There are 6 ClientServices which extend AbstractClientServiceImpl
and use the same PortType wsClient
bean and same basePath of the remote server, however they set their to be invoked service's last uri part with their own String getEndpointUrl(){ return "X";}
method.
BasePath: https://remote1.server.com:443/api
Alternate addresses: https://remote2.server.com:443/api
Please take a look at the code - ClientEndpointAddressInterceptor
. With this interceptor I am able to combine the basePath + lastUriPart and call the proper target endpoint for a particular ClientService - even when failover occurs. E.g:
target endpoint for ClientService1.class https://remote1.server.com:443/api/service1
target endpoint for ClientService2.class: https://remote1.server.com:443/api/service2
I have been struggling with the right settings/configuration for 2weeks. If I don't add tlsClientParameters or HttpClientPolicy to the extensor like so, then after having taken place the failover I would not be able to see any TLS settings for the newly created conduit!
// ssl settings
endpointInfo.addExtensor(tlsClientParameters);
I don't know if this is the right way to do it, but with this hacky workaround, I could have managed to "provide" the same tlsClientParams for the 2 remote calls(primary + alternate remote server address) - client certificate's SAN has both server's DNS names.
Technology: 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);
}
Questions
JAX-WS client
JAX-RS client
Actually the same 2 concerns/questions which are addressed above for JAX-WS client.
The only difference is that RS has 3 client call methods using the same client instance declared as
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;
}
Thank you for the help in advance.
The solutions is to use CXF's HTTPConduitConfigurator as detailed here: 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);
This will set the the TLS Client Parameters on all conduits that are created, e.g. when using failover.