使用Undertow的Spring Boot Rest服务如何获取入站请求客户端证书的CN
基于Spring Boot + Undertow的客户端证书CN校验实现方案
一、前置配置确认
首先需要确保Undertow已开启客户端证书认证,修改application.yml配置如下:
server: port: 8443 ssl: enabled: true key-store: classpath:server.keystore key-store-password: 你的密钥库密码 key-alias: 服务端证书别名 trust-store: classpath:server.truststore trust-store-password: 你的信任库密码 client-auth: need # 强制客户端提交证书,可选为want(非强制) undertow: max-http-post-size: 10MB
二、两种实现方案
两种方案可根据业务场景选择:
- 方案1适合部分接口需要校验CN、或不同接口需匹配不同CN规则的场景
- 方案2适合全局所有接口强制校验CN的场景,校验失败直接在SSL握手阶段拒绝,性能更高
方案1:Servlet过滤器实现(按需校验接口)
- 第一步:编写自定义过滤器,实现CN校验逻辑
import org.springframework.web.filter.OncePerRequestFilter; import javax.naming.NamingException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.cert.X509Certificate; import java.util.List; @Component public class ClientCertCNFilter extends OncePerRequestFilter { // 从配置文件注入合法CN列表,配置项示例:security.client-cert.valid-cns=cn1,cn2 @Value("${security.client-cert.valid-cns:}") private List<String> validCns; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求属性中获取客户端证书链 X509Certificate[] certChain = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if (certChain == null || certChain.length == 0) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Missing client certificate"); return; } try { String cn = getCertCN(certChain[0]); if (cn == null || !validCns.contains(cn)) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("Invalid client certificate CN"); return; } } catch (NamingException e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().write("Failed to parse client certificate"); return; } filterChain.doFilter(request, response); } // 从证书中提取CN字段,使用LdapName解析避免手动分割字符串出错 private String getCertCN(X509Certificate cert) throws NamingException { String subjectDn = cert.getSubjectX500Principal().getName(); LdapName ldapName = new LdapName(subjectDn); for (Rdn rdn : ldapName.getRdns()) { if (rdn.getType().equalsIgnoreCase("CN")) { return rdn.getValue().toString(); } } return null; } }
- 第二步:注册过滤器,指定校验的接口路径
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<ClientCertCNFilter> clientCertCNFilterRegistration(ClientCertCNFilter filter) { FilterRegistrationBean<ClientCertCNFilter> registration = new FilterRegistrationBean<>(); registration.setFilter(filter); registration.addUrlPatterns("/api/*"); // 仅校验/api路径下的接口,可按需修改 registration.setOrder(Ordered.HIGHEST_PRECEDENCE); return registration; } }
方案2:Undertow全局信任管理器校验(全接口强制校验)
- 第一步:自定义X509信任管理器,扩展CN校验逻辑
import javax.naming.NamingException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.net.ssl.X509TrustManager; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; public class CustomX509TrustManager implements X509TrustManager { private final X509TrustManager defaultTrustManager; private final List<String> validCns; public CustomX509TrustManager(X509TrustManager defaultTrustManager, List<String> validCns) { this.defaultTrustManager = defaultTrustManager; this.validCns = validCns; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { // 先执行默认的证书信任链校验 defaultTrustManager.checkClientTrusted(chain, authType); // 额外校验CN字段 try { String cn = getCertCN(chain[0]); if (cn == null || !validCns.contains(cn)) { throw new CertificateException("Client certificate CN is not allowed: " + cn); } } catch (NamingException e) { throw new CertificateException("Failed to parse client certificate CN", e); } } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { defaultTrustManager.checkServerTrusted(chain, authType); } @Override public X509Certificate[] getAcceptedIssuers() { return defaultTrustManager.getAcceptedIssuers(); } private String getCertCN(X509Certificate cert) throws NamingException { String subjectDn = cert.getSubjectX500Principal().getName(); LdapName ldapName = new LdapName(subjectDn); for (Rdn rdn : ldapName.getRdns()) { if (rdn.getType().equalsIgnoreCase("CN")) { return rdn.getValue().toString(); } } return null; } }
- 第二步:自定义Undertow配置,替换默认信任管理器
import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.security.KeyStore; import java.util.List; @Configuration public class UndertowConfig { @Value("${security.client-cert.valid-cns:}") private List<String> validCns; @Bean public UndertowDeploymentInfoCustomizer undertowDeploymentInfoCustomizer() throws Exception { // 加载系统默认信任管理器 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init((KeyStore) null); // 如需自定义信任库可在此处加载指定KeyStore X509TrustManager defaultTm = null; for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { defaultTm = (X509TrustManager) tm; break; } } CustomX509TrustManager customTm = new CustomX509TrustManager(defaultTm, validCns); return deploymentInfo -> deploymentInfo.addSSLContextCustomizer(sslContextBuilder -> { sslContextBuilder.setTrustManager(customTm); }); } }
内容的提问来源于stack exchange,提问作者vijay
相关产品推荐
相关产品推荐

