如何开启Spring Cloud Gateway的客户端证书主机名验证?
在Spring Cloud Gateway中开启客户端证书主机名验证
我之前刚好踩过这个坑!Netty 4.x确实默认不会自动验证客户端证书的主机名,而且Reactor Netty的默认SSL配置也不会帮你自动设置对等主机名,必须手动介入配置才能搞定。下面是我成功实现的两种场景方案,你可以根据自己的实际需求来选:
场景1:Gateway作为客户端,调用后端服务时验证服务端证书的主机名
如果你的需求是Gateway作为客户端请求后端服务,需要验证后端服务证书的主机名是否匹配,你可以通过自定义Reactor Netty的HttpClient来实现:
- 自定义
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); } } }
- 配套的
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配置:
- 自定义
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]; } }
- 配套的
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
相关产品推荐
相关产品推荐

