在Spring Boot中实现X.509证书认证,本质上是把身份校验前置到TLS握手阶段。普通登录方案先建立连接再提交账号密码,服务端在应用层判断身份;而双向TLS要求客户端在握手时即出示证书,如果证书不受信任或已过期,连接根本不会进入Spring MVC层。这个机制让认证过程对业务代码几乎透明,但也对证书管理和容器配置提出了更严格的要求。

一、X.509证书认证的握手与提取流程
单向TLS只验证服务器身份,客户端可以确认自己连的是不是目标服务器,但服务器并不知道客户端是谁。双向TLS在ServerHello之后会发送CertificateRequest消息,要求客户端提交自己的证书。客户端收到请求后,会发送Certificate消息携带证书链,并通过CertificateVerify消息证明自己持有对应私钥。服务端在握手阶段校验证书链、有效期、签名以及证书是否被吊销,任何一项不通过,连接都会被直接断开。
对于Spring Boot内嵌的Tomcat或Undertow容器,当client-auth设置为need时,容器会把客户端证书数组写入请求属性javax.servlet.request.X509Certificate。Spring Security提供了X509AuthenticationFilter,它从该属性中读取证书,提取X509Certificate对象,并构造PreAuthenticatedAuthenticationToken。后续的认证管理器只需要根据证书中的Subject DN或SAN字段加载用户信息即可,不需要再处理密码比对逻辑。
证书中的Subject DN通常包含CN、OU、O、L、ST、C等字段,其中最常用的是CN。拿到的DN字符串可能是CN=client01, OU=dev, O=example, L=Shanghai, ST=Shanghai, C=CN这种形式。Spring Security允许通过正则表达式从DN中提取用户名,例如只保留CN部分。证书里也可能包含SAN扩展,如果客户端设备较多,可以优先使用SAN中的邮箱或UPN作为登录标识。
二、Spring Boot配置与安全过滤链
服务端需要准备两个证书库:key-store保存服务端自己的证书和私钥,trust-store保存用来验证客户端证书的CA证书链。trust-store中并不需要导入每一个客户端证书,只要客户端证书由该CA签发,握手时就能通过验证。配置示例如下。
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: server
client-auth: need
trust-store: classpath:truststore.p12
trust-store-password: changeit
trust-store-type: PKCS12
client-auth的可选值有none、want和need。none表示不要求客户端证书,want表示客户端可以携带证书,但不强制,need表示客户端必须携带合法证书。做X.509认证时建议使用need,避免某些客户端绕过证书验证后触发Spring Security的预认证异常。trust-store-type推荐使用PKCS12,它比传统的JKS更容易迁移,也更适合现代Java版本。
Spring Security部分需要启用x509登录方式。可以保留默认的X509AuthenticationFilter,但通常需要自定义主体提取规则和UserDetailsService。下面的配置把Subject DN中的CN值作为用户名,然后交给UserDetailsService加载用户。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.x509(x509 -> x509
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(username -> new User(
username,
"",
AuthorityUtils.createAuthorityList("ROLE_USER")
))
);
return http.build();
}
}
subjectPrincipalRegex中的正则CN=(.*?)(?:,|$)表示从DN中提取第一个CN字段,遇到逗号或字符串结束就停止。这样可以避免把OU、O等后续字段一并当作用户名。UserDetailsService回调里可以根据CN查询数据库、LDAP或本地用户表,把真实权限和账号状态返回给Spring Security。
三、用keytool生成证书链
开发调试阶段可以直接用JDK自带的keytool生成一套CA、服务端证书和客户端证书。CA用于签发服务端和客户端证书,服务端和客户端都信任同一个CA,因此双向TLS能够通过验证。以下是完整的命令步骤。
# 生成CA证书 keytool -genkeypair -alias ca -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore ca.p12 -storepass changeit -dname "CN=Local Test CA" -ext bc=ca:true -validity 3650 # 导出CA证书 keytool -exportcert -alias ca -keystore ca.p12 -storepass changeit -file ca.cer -rfc # 生成服务端证书 keytool -genkeypair -alias server -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore server.p12 -storepass changeit -dname "CN=localhost" keytool -certreq -alias server -keystore server.p12 -storepass changeit -file server.csr keytool -gencert -alias ca -keystore ca.p12 -storepass changeit -infile server.csr -outfile server.cer -ext san=dns:localhost,ip:127.0.0.1 -rfc keytool -importcert -alias ca -keystore server.p12 -storepass changeit -file ca.cer -noprompt keytool -importcert -alias server -keystore server.p12 -storepass changeit -file server.cer -noprompt # 生成客户端证书 keytool -genkeypair -alias client -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore client.p12 -storepass changeit -dname "CN=client01" keytool -certreq -alias client -keystore client.p12 -storepass changeit -file client.csr keytool -gencert -alias ca -keystore ca.p12 -storepass changeit -infile client.csr -outfile client.cer -rfc keytool -importcert -alias ca -keystore client.p12 -storepass changeit -file ca.cer -noprompt keytool -importcert -alias client -keystore client.p12 -storepass changeit -file client.cer -noprompt # 服务端信任库导入CA keytool -importcert -alias ca -keystore truststore.p12 -storepass changeit -file ca.cer -noprompt
生成服务端证书时必须添加SAN扩展,现代浏览器和Java客户端会严格校验SAN,如果只有CN而没有SAN,访问时可能报错。上述命令中的san=dns:localhost,ip:127.0.0.1同时覆盖了域名和IP访问场景。客户端证书也可以不添加SAN,因为Spring Security默认从Subject DN中提取主体,只要DN里包含CN即可。
命令执行完后,把server.p12和truststore.p12放入Spring Boot项目的classpath下,把client.p12安装到浏览器或通过curl测试。curl测试命令如下:
curl -k --cert client.p12:changeit --cert-type P12 https://localhost:8443/api/user
四、生产环境容易踩的坑
证书链不完整是最常见的握手失败原因。如果客户端只导入了自己的证书而没有导入CA证书,服务端可能看不到完整的签发链,导致验证失败。排查时可以在Java服务端临时开启javax.net.debug=ssl,handshake,观察握手日志中的证书链长度。生产环境不要长期开启该选项,它会输出大量敏感信息。
如果Spring Boot前端还挂了Nginx或其他反向代理,客户端证书会在代理层被消费,后端拿不到原始证书。有些方案会用X-Forwarded-Client-Cert头透传证书,但直接信任这个头非常危险,攻击者可以伪造头部绕过认证。安全做法是在代理与后端之间建立内部TLS,并由代理对证书头做签名,或者直接在代理层完成证书校验,把已认证的用户标识传递给Spring Boot。
证书过期和吊销也需要纳入管理。X.509证书有明确的有效期,过期后握手必然失败,因此最好建立证书到期监控。对于被泄露的证书,仅靠有效期无法及时阻止,需要配置CRL分发点或OCSP校验。Spring Security默认不会校验吊销状态,如果安全要求较高,可以在认证流程中额外检查证书序列号是否在吊销列表中。
Spring BootX.509证书认证客户端证书修改时间:2026-10-03 04:46:08