如何为基于Reactor的Spring Cloud Gateway配置多域名HTTPS的SNI?
解决Spring Cloud Gateway基于SNI选择多域名HTTPS证书的问题
你遇到的问题很典型:Spring Boot默认的server.ssl配置只会从keystore中加载第一个密钥条目,所以即使你把多个域名的证书放在同一个keystore里,它也不会自动根据SNI(Server Name Indication)选择对应证书。不过好在Reactor Netty本身支持SNI,我们只需要自定义Netty的SSL上下文配置就能解决这个问题。
下面是具体的实现步骤:
1. 移除默认的SSL配置(避免冲突)
首先,把application.yml中原来的server.ssl.key-store、server.ssl.trust-store这些配置注释掉,因为我们要手动构建SSL上下文,不需要Spring Boot自动配置这部分。
修改后的application.yml片段:
server: ssl: enabled: true # 注释掉自动配置的密钥库/信任库配置,改为手动实现 # key-store: config/keystore.jks # trust-store: config/truststore.jks client-auth: need --- spring: profiles: development # 这里也去掉对应的ssl配置
2. 自定义Netty SSL上下文配置类
创建一个配置类,手动构建支持SNI的SslContext,并添加Netty的SniHandler来处理SNI请求:
import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SniHandler; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import java.io.InputStream; import java.security.KeyStore; @Configuration public class SniSslConfig { @Bean public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> sniSslCustomizer() { return factory -> factory.addServerCustomizers(httpServer -> { try { // 加载你的密钥库(keystore.jks) KeyStore keyStore = KeyStore.getInstance("JKS"); try (InputStream keyStoreStream = new ClassPathResource("config/keystore.jks").getInputStream()) { keyStore.load(keyStoreStream, "你的密钥库密码".toCharArray()); } // 加载信任库(truststore.jks),用于双向认证验证客户端证书 KeyStore trustStore = KeyStore.getInstance("JKS"); try (InputStream trustStoreStream = new ClassPathResource("config/truststore.jks").getInputStream()) { trustStore.load(trustStoreStream, "你的信任库密码".toCharArray()); } // 初始化密钥管理器和信任管理器 KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, "你的密钥条目密码".toCharArray()); // 如果所有密钥条目密码相同,用这个;不同的话需要单独处理 TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(trustStore); // 构建基础SSL上下文,包含所有密钥条目 SslContext baseSslContext = SslContextBuilder.forServer(keyManagerFactory) .trustManager(trustManagerFactory) .clientAuth(io.netty.handler.ssl.ClientAuth.REQUIRE) // 对应双向认证的need配置 .build(); // 配置SNI处理器,根据客户端发送的主机名选择对应证书 return httpServer.tcpConfiguration(tcp -> tcp.bootstrap(bootstrap -> bootstrap.childHandler(channel -> channel.pipeline().addLast(new SniHandler((sniHostName, sslEngine) -> { // Netty会自动从baseSslContext中匹配与SNI主机名对应的证书(前提是keystore里的证书CN/SAN匹配) return baseSslContext.newHandler(channel.alloc(), sniHostName, 443); })) ) ) ); } catch (Exception e) { throw new RuntimeException("配置SNI SSL上下文失败", e); } }); } }
3. 针对不同密钥密码的扩展(可选)
如果你的两个域名证书在keystore中使用了不同的密钥密码,可以在SNI回调中单独初始化对应密钥条目:
// 在SniHandler的回调中修改: return httpServer.tcpConfiguration(tcp -> tcp.bootstrap(bootstrap -> bootstrap.childHandler(channel -> channel.pipeline().addLast(new SniHandler((sniHostName, sslEngine) -> { KeyManagerFactory targetKmf; if ("your-first-domain.com".equals(sniHostName)) { targetKmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); targetKmf.init(keyStore, "第一个域名的密钥密码".toCharArray()); } else if ("your-second-domain.com".equals(sniHostName)) { targetKmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); targetKmf.init(keyStore, "第二个域名的密钥密码".toCharArray()); } else { // 没有匹配到的域名,使用默认密钥 targetKmf = keyManagerFactory; } SslContext context = SslContextBuilder.forServer(targetKmf) .trustManager(trustManagerFactory) .clientAuth(io.netty.handler.ssl.ClientAuth.REQUIRE) .build(); return context.newHandler(channel.alloc(), sniHostName, 443); })) ) ) );
关键注意事项
- 确保keystore中的每个证书的**CommonName(CN)或Subject Alternative Names(SAN)**与目标域名完全匹配,Netty会根据这个匹配SNI主机名。
- 替换代码中所有的密码占位符为你实际的keystore/truststore/密钥密码。
- 双向认证的逻辑已经通过
ClientAuth.REQUIRE配置好了,对应原来的server.ssl.client-auth: need。
这样配置完成后,你的Spring Cloud Gateway就能根据客户端发送的SNI信息自动选择对应域名的证书,实现多域名的HTTPS双向认证了。
内容的提问来源于stack exchange,提问作者Wes
相关产品推荐
相关产品推荐

