This is an automated email from the ASF dual-hosted git repository.
ffang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 84a6f09667 [CXF-9159]CXF client to support spring boot SSL bundles
(#2607)
84a6f09667 is described below
commit 84a6f096676f9ab2bbf12d05266982cb6f34bae7
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Wed Sep 17 09:28:05 2025 -0400
[CXF-9159]CXF client to support spring boot SSL bundles (#2607)
* [CXF-9159]CXF client to support spring boot SSL bundles
* [CXF-9159]rename CxfClientSslProperties inner Client Bundle list
* [CXF-9159]rename one pair of getter/setter method
* [CXF-9159]rename property name for defaultBundle
* [CXF-9159]rename property name for defaultBundle-tests
---
.../autoconfigure/ssl/CxfClientSslProperties.java | 127 +++++++++++++++++
.../ssl/CxfSslBundlesAutoConfiguration.java | 53 ++++++++
.../ssl/SslBundleHttpConduitConfigurer.java | 110 +++++++++++++++
...rk.boot.autoconfigure.AutoConfiguration.imports | 1 +
systests/spring-boot/pom.xml | 23 ++++
.../spring/boot/ssl/SpringClientSslBundleTest.java | 150 +++++++++++++++++++++
.../src/test/resources/application-ssl.yml | 46 +++++++
7 files changed, 510 insertions(+)
diff --git
a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfClientSslProperties.java
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfClientSslProperties.java
new file mode 100644
index 0000000000..2b50f55408
--- /dev/null
+++
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfClientSslProperties.java
@@ -0,0 +1,127 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.spring.boot.autoconfigure.ssl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Client-side SSL integration properties for CXF when Spring Boot SslBundles
are present.
+ * bundles are applied once when an HTTPConduit is configured.
+ */
+@ConfigurationProperties("cxf.client.ssl")
+public class CxfClientSslProperties {
+ private boolean enabled;
+ //Name of the Spring SSL bundle to use for outbound CXF clients. */
+ private String defaultBundle = "cxf-client";
+ // Convenience flag for tests
+ private Boolean disableCnCheck = Boolean.FALSE;
+ private List<CxfClientSslBundle> cxfClientSslBundles = new ArrayList<>();
+
+ public static class CxfClientSslBundle {
+ private String name;
+ private String address;
+ private String bundle;
+ private String protocol;
+ private List<String> cipherSuites;
+ private Boolean disableCnCheck = Boolean.FALSE;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getBundle() {
+ return bundle;
+ }
+
+ public void setBundle(String bundle) {
+ this.bundle = bundle;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public List<String> getCipherSuites() {
+ return cipherSuites;
+ }
+
+ public void setCipherSuites(List<String> cipherSuites) {
+ this.cipherSuites = cipherSuites;
+ }
+
+ public Boolean getDisableCnCheck() {
+ return disableCnCheck;
+ }
+
+ public void setDisableCnCheck(Boolean disableCnCheck) {
+ this.disableCnCheck = disableCnCheck;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getDefaultBundle() {
+ return defaultBundle;
+ }
+
+ public void setDefaultBundle(String bundle) {
+ this.defaultBundle = bundle;
+ }
+
+ public Boolean getDisableCnCheck() {
+ return disableCnCheck;
+ }
+
+ public void setDisableCnCheck(Boolean disableCnCheck) {
+ this.disableCnCheck = disableCnCheck;
+ }
+
+ public List<CxfClientSslBundle> getCxfClientSslBundles() {
+ return cxfClientSslBundles;
+ }
+
+ public void setCxfClientSslBundles(List<CxfClientSslBundle>
clientSslBundles) {
+ this.cxfClientSslBundles = clientSslBundles;
+ }
+}
diff --git
a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfSslBundlesAutoConfiguration.java
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfSslBundlesAutoConfiguration.java
new file mode 100644
index 0000000000..ceaed83912
--- /dev/null
+++
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/CxfSslBundlesAutoConfiguration.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.spring.boot.autoconfigure.ssl;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.spring.boot.autoconfigure.CxfAutoConfiguration;
+import org.apache.cxf.transport.http.HTTPConduitConfigurer;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.context.annotation.Bean;
+
+
+/**
+ * Auto-configuration that bridges Spring Boot SslBundles to CXF HTTP
conduits. Applies a configured bundle to
+ * every HTTPS client created through the CXF Bus.
+ */
+@AutoConfiguration(after = CxfAutoConfiguration.class)
+@EnableConfigurationProperties(CxfClientSslProperties.class)
+@ConditionalOnClass({
+ SslBundles.class, HTTPConduitConfigurer.class
+})
+@ConditionalOnProperty(prefix = "cxf.client.ssl", name = "enabled",
+ havingValue = "true", matchIfMissing = false)
+public class CxfSslBundlesAutoConfiguration {
+
+ @Bean
+ public HTTPConduitConfigurer cxfSslBundleConfigurerInstaller(Bus bus,
SslBundles sslBundles,
+ CxfClientSslProperties
props) {
+ HTTPConduitConfigurer sslHttpConduitConfigurer = new
SslBundleHttpConduitConfigurer(sslBundles, props);
+ bus.setExtension(sslHttpConduitConfigurer,
+ HTTPConduitConfigurer.class);
+ return sslHttpConduitConfigurer;
+ }
+}
diff --git
a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/SslBundleHttpConduitConfigurer.java
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/SslBundleHttpConduitConfigurer.java
new file mode 100644
index 0000000000..b6969a5e30
--- /dev/null
+++
b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/ssl/SslBundleHttpConduitConfigurer.java
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.spring.boot.autoconfigure.ssl;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.cxf.configuration.jsse.TLSClientParameters;
+import org.apache.cxf.transport.http.HTTPConduit;
+import org.apache.cxf.transport.http.HTTPConduitConfigurer;
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.StringUtils;
+
+/**
+ * HTTPConduitConfigurer that applies a Spring Boot SslBundle to CXF HTTP
clients. Bundle selection can be
+ * global or by address pattern.
+ */
+final class SslBundleHttpConduitConfigurer implements HTTPConduitConfigurer {
+ private final SslBundles sslBundles;
+ private final CxfClientSslProperties props;
+ private final AntPathMatcher matcher = new AntPathMatcher();
+
+ SslBundleHttpConduitConfigurer(SslBundles sslBundles,
CxfClientSslProperties props) {
+ this.sslBundles = Objects.requireNonNull(sslBundles, "sslBundles");
+ this.props = Objects.requireNonNull(props, "props");
+ }
+
+ @Override
+ public void configure(String name, String address, HTTPConduit conduit) {
+ CxfClientSslProperties.CxfClientSslBundle cxfClientSslBundle
+ = findMatchCxfClientSslBundle(address,
props.getCxfClientSslBundles());
+ String bundleName = cxfClientSslBundle != null ?
cxfClientSslBundle.getBundle() : props.getDefaultBundle();
+ if (!StringUtils.hasText(bundleName)) {
+ return;
+ }
+ SslBundle bundle = sslBundles.getBundle(bundleName);
+ TLSClientParameters tls = buildTls(bundle, cxfClientSslBundle);
+ conduit.setTlsClientParameters(tls);
+ }
+
+ private TLSClientParameters buildTls(SslBundle bundle,
+
CxfClientSslProperties.CxfClientSslBundle cxfClientSslBundle) {
+ SSLContext ctx = bundle.createSslContext();
+ TLSClientParameters tls = new TLSClientParameters();
+ tls.setSslContext(ctx);
+ if (cxfClientSslBundle != null &&
StringUtils.hasText(cxfClientSslBundle.getProtocol())) {
+ tls.setSecureSocketProtocol(cxfClientSslBundle.getProtocol());
+ }
+ if (cxfClientSslBundle != null
+ && cxfClientSslBundle.getCipherSuites() != null
+ && !cxfClientSslBundle.getCipherSuites().isEmpty()) {
+ tls.setCipherSuites(cxfClientSslBundle.getCipherSuites());
+ }
+ if (cxfClientSslBundle != null) {
+ tls.setDisableCNCheck(cxfClientSslBundle.getDisableCnCheck());
+ } else {
+ tls.setDisableCNCheck(props.getDisableCnCheck());
+ }
+ return tls;
+ }
+
+ private CxfClientSslProperties.CxfClientSslBundle
findMatchCxfClientSslBundle(
+ String address,
+ List<CxfClientSslProperties.CxfClientSslBundle>
cxfClientSslBundles) {
+ if (!StringUtils.hasText(address) || cxfClientSslBundles == null) {
+ return null;
+ }
+ for (CxfClientSslProperties.CxfClientSslBundle r :
cxfClientSslBundles) {
+ String pat = r.getAddress();
+ if (!StringUtils.hasText(pat)) {
+ continue;
+ }
+ if (isPrefix(pat) && address.startsWith(pat)) {
+ return r;
+ }
+ if (isAntStyle(pat) && matcher.match(pat, address)) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ private static boolean isPrefix(String p) {
+ return p.startsWith("http://") || p.startsWith("https://");
+ }
+
+ private static boolean isAntStyle(String p) {
+ return p.contains("*") || p.contains("?");
+ }
+}
diff --git
a/integration/spring-boot/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
b/integration/spring-boot/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 1d5db5ed73..baee6658b0 100644
---
a/integration/spring-boot/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++
b/integration/spring-boot/autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -2,3 +2,4 @@ org.apache.cxf.spring.boot.autoconfigure.CxfAutoConfiguration
org.apache.cxf.spring.boot.autoconfigure.openapi.OpenApiAutoConfiguration
org.apache.cxf.spring.boot.autoconfigure.micrometer.MicrometerMetricsAutoConfiguration
org.apache.cxf.spring.boot.autoconfigure.jaxws.CxfJaxwsAutoConfiguration
+org.apache.cxf.spring.boot.autoconfigure.ssl.CxfSslBundlesAutoConfiguration
diff --git a/systests/spring-boot/pom.xml b/systests/spring-boot/pom.xml
index 5600b4088b..4a93e1e01e 100644
--- a/systests/spring-boot/pom.xml
+++ b/systests/spring-boot/pom.xml
@@ -74,6 +74,10 @@
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-features-metrics</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-transports-http</artifactId>
+ </dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
@@ -95,6 +99,14 @@
<artifactId>cxf-testutils</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-testutils</artifactId>
+ <scope>test</scope>
+ <classifier>keys</classifier>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -112,6 +124,17 @@
<artifactId>spring-tx</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-undertow</artifactId>
+ <scope>test</scope>
+ <version>${cxf.spring.boot.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-integration-tracing-micrometer</artifactId>
diff --git
a/systests/spring-boot/src/test/java/org/apache/cxf/systest/spring/boot/ssl/SpringClientSslBundleTest.java
b/systests/spring-boot/src/test/java/org/apache/cxf/systest/spring/boot/ssl/SpringClientSslBundleTest.java
new file mode 100644
index 0000000000..8cbc28c4da
--- /dev/null
+++
b/systests/spring-boot/src/test/java/org/apache/cxf/systest/spring/boot/ssl/SpringClientSslBundleTest.java
@@ -0,0 +1,150 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.systest.spring.boot.ssl;
+
+import javax.net.ssl.SSLContext;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import org.apache.cxf.Bus;
+import org.apache.cxf.BusFactory;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
+import org.apache.cxf.spring.boot.autoconfigure.ssl.CxfClientSslProperties;
+import
org.apache.cxf.spring.boot.autoconfigure.ssl.CxfSslBundlesAutoConfiguration;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
+import
org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import
org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
+import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.ActiveProfiles;
+
+import org.junit.jupiter.api.Test;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End‑to‑end TLS integration test:
+ * - Boots Spring Boot on HTTPS using server.ssl.bundle=cxf-server with
mutual TLS required
+ * - Publishes a CXF JAX‑RS endpoint at /api
+ * - Configures outbound client via CxfClientSslProperties (bound from YAML)
+ * - Calls endpoint and asserts TLS handshake & invocation succeed
+ */
+
+@SpringBootTest(classes = SpringClientSslBundleTest.TestCfg.class,
+ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("ssl")
+
+
+
+public class SpringClientSslBundleTest {
+
+ interface HelloApi {
+ @GET
+ @Path("/hello")
+ @Produces(MediaType.TEXT_PLAIN)
+ String hello();
+ }
+
+ @Path("/")
+ public static class HelloResource implements HelloApi {
+ @Override
+ public String hello() {
+ return "hello";
+ }
+ }
+
+ @Configuration
+ @EnableConfigurationProperties(CxfClientSslProperties.class)
+ @ImportAutoConfiguration({SslAutoConfiguration.class,
CxfSslBundlesAutoConfiguration.class})
+ @EnableAutoConfiguration
+ static class TestCfg {
+
+
+ @Bean
+ public ServletWebServerFactory servletWebServerFactory() {
+ return new UndertowServletWebServerFactory(0);
+ }
+
+ @Bean
+ public org.apache.cxf.endpoint.Server jaxrsServer(Bus bus,
HelloResource resource) {
+ JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+ sf.setBus(bus);
+ sf.setAddress("/api");
+ sf.setServiceBean(resource);
+ return sf.create();
+ }
+
+ @Bean
+ public HelloResource helloResource() {
+ return new HelloResource();
+ }
+ }
+
+ @Autowired
+ private SslBundles sslBundles;
+
+ @Autowired
+ private CxfClientSslProperties clientSslProperties;
+
+ @LocalServerPort
+ private int port;
+
+ @Autowired
+ private Bus bus;
+
+
+
+
+ @Test
+ void testSSL() {
+ BusFactory.setThreadDefaultBus(bus);
+ String base = "https://localhost:" + port + "/ssl/api";
+
+ // Create CXF client
+ HelloApi api = JAXRSClientFactory.create(base, HelloApi.class);
+
+ // Retrieve configured bundle from properties
+ String bundleName = clientSslProperties.getDefaultBundle();
+ assertThat(bundleName).isEqualTo("cxf-client");
+ SslBundle clientBundle = sslBundles.getBundle(bundleName);
+ assertThat(clientBundle).isNotNull();
+
+ SSLContext ctx = clientBundle.createSslContext();
+ assertThat(ctx).isNotNull();
+
+
+
+ String resp = api.hello();
+ assertThat(resp).isEqualTo("hello");
+ }
+}
+
diff --git a/systests/spring-boot/src/test/resources/application-ssl.yml
b/systests/spring-boot/src/test/resources/application-ssl.yml
new file mode 100644
index 0000000000..69ea604124
--- /dev/null
+++ b/systests/spring-boot/src/test/resources/application-ssl.yml
@@ -0,0 +1,46 @@
+server:
+ ssl:
+ enabled: true
+ bundle: "cxf-server"
+ client-auth: need # REQUIRE mutual TLS
+ port: 0
+
+spring:
+ ssl:
+ bundle:
+ jks:
+ cxf-server:
+ alias: "cxf-server"
+ keystore:
+ location: "classpath:keys/Bethal.jks" # server private key
+ password: "password"
+ type: "JKS"
+ truststore:
+ location: "classpath:keys/Truststore.jks" # trusts the client
cert/CA
+ password: "password"
+ type: "JKS"
+ protocol: "TLSv1.2"
+ cxf-client:
+ alias: "client"
+ keystore:
+ location: "classpath:keys/Morpit.jks" # client private key for
mTLS
+ password: "password"
+ type: "JKS"
+ truststore:
+ location: "classpath:keys/Truststore.jks" # trusts the server
cert/CA
+ password: "password"
+ type: "JKS"
+ protocol: "TLSv1.2"
+
+# Client-side CXF SSL mapping (bound to CxfClientSslProperties below)
+cxf:
+ path: /ssl
+ jaxrs:
+ component-scan: true
+ client:
+ ssl:
+ enabled: true
+ defaultBundle: cxf-client
+ disable-cn-check: true # set to false if your server cert has
SAN=localhost
+
+