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

Spring Boot单路由TLS客户端证书认证配置异常排查

问题:Spring Boot仅特定路由启用TLS客户端证书认证失败

需实现仅对/receivePDRDataFromIntegrationGAP2/**路由要求TLS客户端证书认证,其余路由后续单独配置。当前配置下该路由返回403;若将client-auth改为need并移除.authenticated(),全站都会要求证书,不符合需求。

SecurityFilterChain配置

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    System.out.println("version" + SystemProperties.get("java.version"));
    http
            .csrf(csrf -> csrf.disable())
            .cors(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth.requestMatchers("/receivePDRDataFromIntegrationGAP2/**").authenticated())
            .requiresChannel(channel -> channel
                    .requestMatchers("/receivePDRDataFromIntegrationGAP2/**")
                    .requiresSecure()
            )

            .authorizeHttpRequests((authz) -> authz
                    .anyRequest().permitAll()
            );
    return http.build();
}

application.yaml配置

ssl:
    key-store: gap2-certificate/keystore.jks
    key-store-password: changeit
    key-alias: localhost
    key-password: changeit
    enabled: true
    trust-store: gap2-certificate/truststore.jks
    trust-store-password: changeit
    client-auth: want

原配置下的Spring Security日志

2023-10-09 15:04:45,708 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.FilterChainProxy: Securing POST /receivePDRDataFromIntegrationGAP2/createPDRData?patientFiscalCode=TEST1&doctorFiscalCode=TEST2
2023-10-09 15:04:45,722 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.a.c.ChannelProcessingFilter: Request: filter invocation [POST /receivePDRDataFromIntegrationGAP2/createPDRData?patientFiscalCode=TEST1&doctorFiscalCode=TEST2]; ConfigAttributes: [REQUIRES_SECURE_CHANNEL]
2023-10-09 15:04:45,730 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.a.AnonymousAuthenticationFilter: Set SecurityContextHolder to anonymous SecurityContext
2023-10-09 15:04:45,736 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.a.Http403ForbiddenEntryPoint: Pre-authenticated entry point called. Rejecting access
2023-10-09 15:04:45,740 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.FilterChainProxy: Securing POST /error?patientFiscalCode=TEST1&doctorFiscalCode=TEST2
2023-10-09 15:04:45,746 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.FilterChainProxy: Secured POST /error?patientFiscalCode=TEST1&doctorFiscalCode=TEST2
2023-10-09 15:04:45,785 DEBUG [https-jsse-nio-8082-exec-8] o.s.s.w.a.AnonymousAuthenticationFilter: Set SecurityContextHolder to anonymous SecurityContext

发送端收到的报错

403 : "{\"timestamp\":\"2023-10-09T13:04:52.401+00:00\",\"status\":403,\"error\":\"Forbidden\",\"path\":\"/api/receivePDRDataFromIntegrationGAP2/createPDRData\"}"

发送端请求代码

// Send a POST request
response = restTemplate.postForEntity(uri, requestEntity, String.class);

发送端RestTemplate证书配置

@Bean
public RestTemplate restTemplate() throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException {
    KeyStore clientStore = KeyStore.getInstance("PKCS12");
    clientStore.load(new FileInputStream("store/clientBob.p12"), "changeit".toCharArray());

    SSLContext sslContext = SSLContextBuilder.create()
            .setProtocol("TLS")
            .loadKeyMaterial(clientStore, "changeit".toCharArray())
            .loadTrustMaterial(new TrustSelfSignedStrategy())
            .build();

    SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
    HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create().setSSLSocketFactory(sslConnectionSocketFactory).build();
    CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build();

        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

    return new RestTemplate(requestFactory);
}

请求头信息

Request Line: POST https://localhost:8082/api/receivePDRDataFromIntegrationGAP2/createPDRData?patientFiscalCode=TEST1&doctorFiscalCode=TEST2
Request Headers:
Accept: [text/plain, application/json, application/*+json, */*]
Content-Type: [application/json]
Content-Length: [1167]
Request Body: {"id":null, some object....}

解决方案

问题核心是当前配置仅要求路由认证,但未启用Spring Security对客户端证书的解析与校验逻辑,导致客户端证书未被识别,请求被判定为匿名访问而拒绝。

步骤1:修改SecurityFilterChain,添加X509认证支持

启用X509认证处理,让Spring Security解析客户端证书并校验合法性:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .csrf(csrf -> csrf.disable())
            .cors(AbstractHttpConfigurer::disable)
            // 启用X509客户端证书认证
            .x509(x509 -> x509
                    .subjectPrincipalRegex("CN=(.*?)(?:,|$)") // 从证书CN字段提取用户名
                    .userDetailsService(userDetailsService())
            )
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/receivePDRDataFromIntegrationGAP2/**").authenticated()
                    .anyRequest().permitAll()
            )
            .requiresChannel(channel -> channel
                    .requestMatchers("/receivePDRDataFromIntegrationGAP2/**").requiresSecure()
            );
    return http.build();
}

// 自定义用户校验逻辑,实际可替换为从数据库/配置加载信任列表
@Bean
public UserDetailsService userDetailsService() {
    return username -> {
        // 示例:仅信任CN为指定值的证书
        if ("clientBob".equals(username)) {
            return User.withUsername(username)
                    .password("")
                    .authorities("ROLE_USER")
                    .build();
        }
        throw new UsernameNotFoundException("Unknown certificate: " + username);
    };
}

步骤2:保持application.yaml的client-auth配置

维持client-auth: want,确保仅目标路由会强制校验证书,其余路由无证书也可访问:

ssl:
    key-store: gap2-certificate/keystore.jks
    key-store-password: changeit
    key-alias: localhost
    key-password: changeit
    enabled: true
    trust-store: gap2-certificate/truststore.jks
    trust-store-password: changeit
    client-auth: want

关键注意事项

  1. 证书信任前置:务必将客户端证书的公钥导入服务端的truststore.jks,否则SSL层会直接拒绝请求,无法进入Spring Security认证流程。
  2. 规则顺序:确保目标路由的认证规则在anyRequest().permitAll()之前,Spring Security按顺序匹配规则。
  3. 用户校验逻辑:UserDetailsService需根据实际业务调整,比如校验证书CN是否在信任列表、证书是否过期等。

验证要点

  • 检查日志是否出现X509AuthenticationFilter相关调试信息,确认证书被正确解析。
  • 测试非目标路由(如/test)无需证书即可访问,目标路由必须携带有效证书才能通过认证。

内容的提问来源于stack exchange,提问作者marcus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 03:00:11