You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 17:27:35