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

如何开启Spring Cloud Gateway的客户端证书主机名验证?

在Spring Cloud Gateway中开启客户端证书主机名验证

我之前刚好踩过这个坑!Netty 4.x确实默认不会自动验证客户端证书的主机名,而且Reactor Netty的默认SSL配置也不会帮你自动设置对等主机名,必须手动介入配置才能搞定。下面是我成功实现的两种场景方案,你可以根据自己的实际需求来选:

场景1:Gateway作为客户端,调用后端服务时验证服务端证书的主机名

如果你的需求是Gateway作为客户端请求后端服务,需要验证后端服务证书的主机名是否匹配,你可以通过自定义Reactor Netty的HttpClient来实现:

  1. 自定义HttpClientCustomizer,手动配置SSL主机名验证逻辑:
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.stereotype.Component;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;
import java.security.KeyStore;

@Component
public class CustomHttpClientSslCustomizer implements HttpClientCustomizer {

    @Override
    public HttpClient customize(HttpClient httpClient) {
        try {
            // 加载自定义信任库(如果用默认系统信任库可跳过此部分)
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            try (InputStream is = getClass().getResourceAsStream("/truststore.jks")) {
                trustStore.load(is, "your-truststore-password".toCharArray());
            }

            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustStore);

            return httpClient.secure(sslContextSpec -> sslContextSpec
                    .sslContextBuilder(builder -> builder.trustManager(tmf))
                    .sslConfigurator(sslConfig -> {
                        // 启用JDK默认的HTTPS主机名验证算法
                        sslConfig.sslParametersCustomizer(params -> {
                            params.setEndpointIdentificationAlgorithm("HTTPS");
                        });
                        // 如果你需要固定验证某个特定主机,也可以手动设置对等主机名
                        // sslConfig.handlerConfigurator(sslHandler -> {
                        //     sslHandler.engine().setPeerHost("your-backend-service-host.com");
                        // });
                    }));
        } catch (Exception e) {
            throw new RuntimeException("Failed to configure SSL for HttpClient", e);
        }
    }
}
  1. 配套的application.yml配置:
spring:
  cloud:
    gateway:
      httpclient:
        ssl:
          enabled: true
          # 若使用自定义信任库,配置路径和密码;用系统默认则可省略
          trust-store: classpath:truststore.jks
          trust-store-password: your-truststore-password

场景2:Gateway作为服务端,验证客户端证书的主机名

如果你的需求是Gateway作为服务端,接收客户端请求时验证客户端证书的主机名是否合法,需要自定义Reactor Netty的HttpServer配置:

  1. 自定义HttpServerCustomizer,添加客户端证书主机名验证逻辑:
import org.springframework.cloud.gateway.config.HttpServerCustomizer;
import org.springframework.stereotype.Component;
import reactor.netty.http.server.HttpServer;
import reactor.netty.tcp.SslProvider;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class CustomHttpServerSslCustomizer implements HttpServerCustomizer {

    @Override
    public HttpServer customize(HttpServer httpServer) {
        return httpServer.secure(sslContextSpec -> sslContextSpec
                .sslContextBuilder(builder -> {
                    try {
                        // 加载服务端密钥库
                        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                        try (InputStream is = getClass().getResourceAsStream("/keystore.jks")) {
                            keyStore.load(is, "your-keystore-password".toCharArray());
                        }
                        // 加载验证客户端证书的信任库
                        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
                        try (InputStream is = getClass().getResourceAsStream("/client-truststore.jks")) {
                            trustStore.load(is, "your-truststore-password".toCharArray());
                        }

                        var kmf = java.security.KeyManagerFactory.getInstance(java.security.KeyManagerFactory.getDefaultAlgorithm());
                        kmf.init(keyStore, "your-keystore-password".toCharArray());
                        var tmf = javax.net.ssl.TrustManagerFactory.getInstance(javax.net.ssl.TrustManagerFactory.getDefaultAlgorithm());
                        tmf.init(trustStore);

                        builder.keyManager(kmf).trustManager(tmf).clientAuth(SslProvider.ClientAuth.REQUIRE);
                    } catch (Exception e) {
                        throw new RuntimeException("Failed to configure SSL for HttpServer", e);
                    }
                })
                .handlerConfigurator(sslHandler -> {
                    sslHandler.engine().setNeedClientAuth(true);
                    // 添加自定义主机名验证逻辑
                    sslHandler.handlerAdded(ctx -> {
                        SSLSession session = sslHandler.engine().getSession();
                        if (session != null) {
                            try {
                                Certificate[] peerCerts = session.getPeerCertificates();
                                if (peerCerts.length > 0 && peerCerts[0] instanceof X509Certificate) {
                                    X509Certificate clientCert = (X509Certificate) peerCerts[0];
                                    // 获取客户端实际连接的主机地址
                                    String clientHost = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", "");
                                    if (!validateClientCertHost(clientCert, clientHost)) {
                                        ctx.close();
                                        throw new SSLPeerUnverifiedException("Client certificate hostname does not match: " + clientHost);
                                    }
                                }
                            } catch (Exception e) {
                                ctx.close();
                                throw new RuntimeException("Failed to validate client certificate hostname", e);
                            }
                        }
                    });
                }));
    }

    private boolean validateClientCertHost(X509Certificate clientCert, String clientHost) {
        // 优先检查证书的SAN(主题备用名称),RFC已废弃用CN做主机名验证
        List<String> sanList = getSanList(clientCert);
        if (!sanList.isEmpty() && sanList.contains(clientHost)) {
            return true;
        }
        // 降级检查CN字段(兼容旧证书)
        String cn = getCnFromCert(clientCert);
        return clientHost.equals(cn);
    }

    private List<String> getSanList(X509Certificate cert) {
        try {
            Collection<List<?>> subjectAltNames = cert.getSubjectAlternativeNames();
            if (subjectAltNames == null) {
                return Collections.emptyList();
            }
            return subjectAltNames.stream()
                    .filter(entry -> entry.size() >= 2 && (int) entry.get(0) == 2) // 2代表DNS类型
                    .map(entry -> (String) entry.get(1))
                    .collect(Collectors.toList());
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }

    private String getCnFromCert(X509Certificate cert) {
        String dn = cert.getSubjectX500Principal().getName();
        return dn.split("CN=")[1].split(",")[0];
    }
}
  1. 配套的application.yml配置:
spring:
  cloud:
    gateway:
      httpserver:
        ssl:
          enabled: true
          key-store: classpath:keystore.jks
          key-store-password: your-keystore-password
          trust-store: classpath:client-truststore.jks
          trust-store-password: your-truststore-password
          client-auth: require

关键注意点

  • 核心逻辑是手动介入Reactor Netty的SSL配置流程:要么通过sslParametersCustomizer启用JDK默认的主机名验证算法,要么手动实现证书主机名的匹配逻辑。
  • 如果你使用Spring Cloud Gateway 3.x及以上版本,Reactor Netty的API可能略有差异,注意根据版本调整代码细节。
  • 验证客户端证书时,优先使用SAN字段而非CN字段,这是符合现代安全规范的做法。

内容的提问来源于stack exchange,提问作者andrew.z

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 19:17:43