Spring Boot 3升级后特定端点客户端证书验证问题求助
问题
基于Spring Boot 3和Java 17的Java应用暴露REST API,要求仅对/api/auth端点调用时提取并验证客户端证书。调用该API的curl命令如下:
curl -X POST "https://localhost:8443/api/auth" \ -H "accept: */*" \ -H "Content-Type: application/json" \ -H "skv_client_correlation_id: xyz" \ -d '{"xyz": "xyz"}' \ --cert client_public_certificate.crt \ --key client_private_key.key \ --cacert server_public_certificate.crt
此前在Spring Boot 2中,通过自定义CertificateFilter过滤器,从request的javax.servlet.request.X509Certificate属性获取证书实现验证,代码如下:
public class CertificateFilter extends OncePerRequestFilter { private final KeyStore keyStore; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { try { X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); trustManagerFactory.init(keyStore); for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { if (trustManager instanceof X509TrustManager x509TrustManager) { x509TrustManager.checkServerTrusted(certs, "RSA"); } } } catch (CertificateException | IllegalArgumentException e) { log.warn("Invalid certificate: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid certificate"); return; } catch (Exception e) { log.error("Error during certificate validation: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error during certificate validation"); return; } filterChain.doFilter(request, response); } }
升级至Spring Boot 3后,request中既无javax.servlet.request.X509Certificate属性,也无jakarta.servlet.request.X509Certificate属性(certs始终为null),需解决如何实现特定端点的客户端证书验证问题。
解决方案
1. 先调整Tomcat的客户端证书请求策略
Spring Boot 3默认Tomcat的客户端证书请求策略是SSLVerifyClient.NONE(不要求客户端提供证书),要让Tomcat允许客户端提供证书,需将clientAuth设置为want(允许但不强制),后续在特定端点触发验证。
通过自定义Tomcat配置实现:
import org.apache.catalina.connector.Connector; import org.apache.coyote.http11.Http11NioProtocol; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class TomcatSslConfig { @Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() { return factory -> { factory.addConnectorCustomizers((Connector connector) -> { Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler(); protocol.setSSLEnabled(true); // 设置为want:允许客户端提供证书,但不强制 protocol.setClientAuth("want"); // 补充你的服务器密钥库、信任库配置 // protocol.setKeystoreFile("path/to/keystore.jks"); // protocol.setKeystorePass("your-password"); // protocol.setTruststoreFile("path/to/truststore.jks"); // protocol.setTruststorePass("your-password"); }); }; } }
2. 用Spring Security实现特定端点验证(推荐)
Spring Security提供了更规范的X.509证书验证能力,可精准控制仅/api/auth端点需要验证:
步骤1:配置SecurityFilterChain
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.User; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth").authenticated() .anyRequest().permitAll() ) .x509(x509 -> x509 // 从证书Subject中提取用户名(示例取CN字段) .subjectPrincipalRegex("CN=(.*?)(?:,|$)") .userDetailsService(userDetailsService()) ); return http.build(); } // 自定义用户信息加载逻辑,可根据证书字段关联业务用户 private UserDetailsService userDetailsService() { return username -> User .withUsername(username) .password("") .authorities("ROLE_AUTH_CLIENT") .accountExpired(false) .accountLocked(false) .credentialsExpired(false) .disabled(false) .build(); } }
步骤2:配置SSL和信任库(application.properties)
server.ssl.enabled=true server.ssl.port=8443 server.ssl.key-store=classpath:server-keystore.jks server.ssl.key-store-password=your-keystore-pwd server.ssl.key-alias=server-cert-alias server.ssl.trust-store=classpath:server-truststore.jks server.ssl.trust-store-password=your-truststore-pwd server.ssl.client-auth=want
3. 修复自定义过滤器(若坚持使用过滤器)
Spring Boot 3已迁移到Jakarta EE,需调整API包和属性名,同时修正之前的方法调用错误:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.cert.TrustManagerFactory; import java.security.cert.X509TrustManager; public class CertificateFilter extends OncePerRequestFilter { private final KeyStore keyStore; public CertificateFilter(KeyStore keyStore) { this.keyStore = keyStore; } @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // 仅处理/api/auth端点 if (!"/api/auth".equals(request.getRequestURI())) { filterChain.doFilter(request, response); return; } try { // 使用Jakarta的属性名获取证书 X509Certificate[] certs = (X509Certificate[]) request.getAttribute("jakarta.servlet.request.X509Certificate"); if (certs == null || certs.length == 0) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Client certificate required"); return; } TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); trustManagerFactory.init(keyStore); for (var trustManager : trustManagerFactory.getTrustManagers()) { if (trustManager instanceof X509TrustManager x509TrustManager) { // 注意:验证客户端证书要用checkClientTrusted,之前的checkServerTrusted是验证服务器证书的 x509TrustManager.checkClientTrusted(certs, "RSA"); } } } catch (CertificateException | IllegalArgumentException e) { logger.warn("Invalid certificate: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid certificate"); return; } catch (Exception e) { logger.error("Error during certificate validation: {}", e.getMessage()); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error during certificate validation"); return; } filterChain.doFilter(request, response); } }
注册过滤器并设置优先级:
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.security.KeyStore; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<CertificateFilter> certificateFilter(KeyStore keyStore) { FilterRegistrationBean<CertificateFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CertificateFilter(keyStore)); registrationBean.addUrlPatterns("/api/auth"); // 设置优先级,确保在Security过滤器前执行 registrationBean.setOrder(1); return registrationBean; } @Bean public KeyStore keyStore() throws Exception { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); // 加载信任库文件 try (var inputStream = getClass().getResourceAsStream("/server-truststore.jks")) { keyStore.load(inputStream, "your-truststore-pwd".toCharArray()); } return keyStore; } }
关键注意事项
- Spring Boot 3已将Servlet API从
javax迁移到jakarta,证书属性名变为jakarta.servlet.request.X509Certificate。 - 原过滤器代码中误用了
checkServerTrusted,验证客户端证书必须用checkClientTrusted。 clientAuth必须设为want,若设为required则所有HTTPS请求都需要证书,不符合仅特定端点验证的需求。
内容的提问来源于stack exchange,提问作者Anthony Vinay
相关产品推荐
相关产品推荐

