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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 19:42:02