Spring Boot 2.7迁移至3.1.4:如何用cloud://路径配置SSLBundle
Spring Boot 3.1.4 迁移:从Config Server仓库加载SSL证书到SSLBundle的解决方案
问题背景
Spring Boot从2.7升级到3.1.4后,HttpClient4同步更新为HttpClient5,原有手动加载JKS创建SSLContext的逻辑失效。尝试使用Spring Boot 3.x的SSLBundle特性时,无法通过cloud://协议加载Config Server仓库中的.p12/.key/.crt证书文件,同时主机名验证(hostnameverifier)功能异常。
解决方案
1. 自定义资源解析器支持cloud://协议
Spring默认ResourceLoader不识别cloud://协议,需扩展ProtocolResolver对接Config Server资源加载逻辑:
步骤1:实现ProtocolResolver
import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ProtocolResolver; import org.springframework.cloud.config.client.ConfigServerConfigDataLoader; import org.springframework.cloud.config.client.ConfigServerConfigDataResource; import org.springframework.util.Assert; public class CloudProtocolResolver implements ProtocolResolver { private final ConfigServerConfigDataLoader configDataLoader; public CloudProtocolResolver(ConfigServerConfigDataLoader configDataLoader) { Assert.notNull(configDataLoader, "ConfigServerConfigDataLoader must not be null"); this.configDataLoader = configDataLoader; } @Override public Resource resolve(String location, ResourceLoader resourceLoader) { if (location.startsWith("cloud://")) { String resourcePath = location.substring("cloud://".length()); // 构建Config Server资源请求,根据实际Config Server配置调整参数 ConfigServerConfigDataResource configResource = new ConfigServerConfigDataResource( ConfigServerConfigDataResource.ResourceType.PROPERTIES, resourcePath, null, null, null, null ); try { return configDataLoader.load(configResource, null).iterator().next().getResource(); } catch (Exception e) { throw new RuntimeException("Failed to load cloud resource: " + location, e); } } return null; } }
步骤2:注册解析器到Spring容器
在启动类中注册自定义协议解析器:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.config.client.ConfigServerConfigDataLoader; import org.springframework.context.annotation.Bean; @SpringBootApplication public class YourApplication { public static void main(String[] args) { SpringApplication app = new SpringApplication(YourApplication.class); app.addProtocolResolver(new CloudProtocolResolver(app.getApplicationContext().getBean(ConfigServerConfigDataLoader.class))); app.run(args); } // 或者通过@Bean方式注册 @Bean public ProtocolResolver cloudProtocolResolver(ConfigServerConfigDataLoader configDataLoader) { return new CloudProtocolResolver(configDataLoader); } }
完成后即可直接使用原配置:
spring.ssl.bundle.jks.server.keystore.location=cloud://test.p12 spring.ssl.bundle.pem.client.keystore.certificate=cloud://client.crt spring.ssl.bundle.pem.client.keystore.private-key=cloud://client.key
2. 手动构建SSLBundle(替代配置方式)
若自定义协议解析器存在问题,可直接通过代码从Config Server加载证书并构建SSLBundle:
步骤1:封装Config Server资源加载逻辑
import org.springframework.cloud.config.client.ConfigClientProperties; import org.springframework.cloud.config.client.ConfigServerConfigDataLoader; import org.springframework.cloud.config.client.ConfigServerConfigDataResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @Service public class SslResourceLoader { private final ConfigServerConfigDataLoader configDataLoader; private final ConfigClientProperties configClientProperties; public SslResourceLoader(ConfigServerConfigDataLoader configDataLoader, ConfigClientProperties configClientProperties) { this.configDataLoader = configDataLoader; this.configClientProperties = configClientProperties; } public Resource loadCloudResource(String path) throws Exception { ConfigServerConfigDataResource resource = new ConfigServerConfigDataResource( ConfigServerConfigDataResource.ResourceType.PROPERTIES, path, configClientProperties.getProfile(), configClientProperties.getLabel(), configClientProperties.getName(), null ); return configDataLoader.load(resource, null).iterator().next().getResource(); } }
步骤2:构建JKS类型SSLBundle
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleBuilder; import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.jks.JksSslStoreBundle; import org.springframework.stereotype.Component; @Component public class JksSslBundleBuilder { private final SslResourceLoader sslResourceLoader; public JksSslBundleBuilder(SslResourceLoader sslResourceLoader) { this.sslResourceLoader = sslResourceLoader; } public SslBundle build(String keystorePath, String keystorePassword) throws Exception { Resource keystoreResource = sslResourceLoader.loadCloudResource(keystorePath); SslStoreBundle storeBundle = JksSslStoreBundle.of( keystoreResource.getInputStream(), keystorePassword.toCharArray(), null, null ); return SslBundleBuilder.create() .storeBundle(storeBundle) // 配置主机名验证,解决原有验证失效问题 .hostnameVerifier((hostname, session) -> javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session) ) .build(); } }
步骤3:构建PEM类型SSLBundle
import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleBuilder; import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.stereotype.Component; @Component public class PemSslBundleBuilder { private final SslResourceLoader sslResourceLoader; public PemSslBundleBuilder(SslResourceLoader sslResourceLoader) { this.sslResourceLoader = sslResourceLoader; } public SslBundle build(String certPath, String keyPath, String keyPassword) throws Exception { Resource certResource = sslResourceLoader.loadCloudResource(certPath); Resource keyResource = sslResourceLoader.loadCloudResource(keyPath); SslStoreBundle storeBundle = PemSslStoreBundle.of( certResource.getInputStream(), keyResource.getInputStream(), keyPassword != null ? keyPassword.toCharArray() : null ); return SslBundleBuilder.create() .storeBundle(storeBundle) .hostnameVerifier((hostname, session) -> javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session) ) .build(); } }
步骤4:注入SSLBundle到HttpClient5
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; import org.springframework.boot.ssl.SslBundle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class HttpClientConfig { @Bean public CloseableHttpClient httpClient(SslBundle sslBundle) { SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslBundle.createSslContext()) .setHostnameVerifier(sslBundle.getHostnameVerifier()) .build(); return HttpClients.custom() .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(sslSocketFactory) .build()) .build(); } }
3. 主机名验证注意事项
- 生产环境禁止直接返回
true跳过验证,必须使用默认验证器或自定义符合业务安全要求的验证逻辑 - 若需兼容特定域名,可在
hostnameVerifier中添加白名单判断
内容的提问来源于stack exchange,提问作者Shalaka
相关产品推荐
相关产品推荐

