如何在Spring Security中解码并验证EdDSA JWT
1. 能否通过Spring标准机制获取publicJWK?
可以复用Spring Security依赖的nimbus-jose库中的RemoteJWKSet从JWKS URI获取公钥集合,这是Spring生态下的标准工具(Spring Security默认JwtDecoder就是基于它实现的)。你不需要自行编写HTTP请求逻辑,直接实例化RemoteJWKSet并传入JWKS URI即可,它会自动处理JWK的缓存和刷新。
2. 是否需要fork nimbus-jose库添加EdDSA支持?
不需要。你计划用Tink做Ed25519签名验证,完全可以在自定义JwtDecoder中结合nimbus的JWK解析能力和Tink的验证能力,无需修改或fork nimbus-jose库。如果你的Spring Security版本较新(如6.0+),nimbus-jose已内置EdDSA支持;若版本较低,用Tink替代签名验证逻辑即可。
3. 验证步骤必须放在JwtDecoder中吗?
建议放在JwtDecoder中,这符合Spring Security的职责划分:JwtDecoder的核心职责就是完成JWT的解码、签名验证、过期校验等合法性检查。若把验证放在后续认证环节(如AuthenticationProvider),会打破职责单一性,不符合框架设计惯例。极端情况下可拆分,但不推荐。
4. 验证步骤的具体实现
以下是整合JWKS获取、Tink Ed25519验证逻辑的自定义JwtDecoder:
import com.google.crypto.tink.subtle.Ed25519Verifier import com.nimbusds.jose.JOSEException import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.jwk.source.RemoteJWKSet import com.nimbusds.jose.proc.SecurityContext import com.nimbusds.jwt.SignedJWT import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.JwtDecoder import java.net.URL import java.nio.charset.StandardCharsets import java.util.Base64 class CustomEdDsaJwtDecoder(jwksUri: String) : JwtDecoder { // 基于nimbus实现的远程JWKS获取工具,自动缓存JWK集合 private val jwkSource: JWKSource<SecurityContext> = RemoteJWKSet(URL(jwksUri)) override fun decode(token: String): Jwt { val signedJwt = SignedJWT.parse(token) val kid = signedJwt.header.keyID ?: throw IllegalArgumentException("JWT缺失'kid'头字段") // 根据kid从JWKS中匹配对应公钥 val jwkSet = jwkSource.getJWKSet(SecurityContext()) val jwk = jwkSet.getKeyByKeyId(kid) ?: throw IllegalArgumentException("未找到对应kid的JWK: $kid") // 校验算法是否为EdDSA if (jwk.algorithm != JWSAlgorithm.EdDSA) { throw IllegalArgumentException("不支持的算法: ${jwk.algorithm}") } // 将JWK转换为Tink Ed25519Verifier所需的公钥字节数组 val publicKeyBytes = when { jwk.isOctetKeyPair -> jwk.toOctetKeyPair().publicKey.toByteArray() else -> throw IllegalArgumentException("不支持的EdDSA JWK类型") } // 使用Tink验证JWT签名 val verifier = Ed25519Verifier(publicKeyBytes) val signingInput = "${signedJwt.header.toJSONString()}.${signedJwt.payload.toJSONString()}" try { verifier.verify(signingInput.toByteArray(StandardCharsets.UTF_8), signedJwt.signature.toByteArray()) } catch (ex: Exception) { throw RuntimeException("JWT签名无效: ${ex.message}", ex) } // 校验过期、生效时间等标准Claims val claimsSet = signedJwt.jwtClaimsSet val now = System.currentTimeMillis() if (claimsSet.expirationTime.time < now) { throw RuntimeException("JWT已过期") } if (claimsSet.issueTime.time > now) { throw RuntimeException("JWT尚未生效") } // 返回解析后的标准Jwt对象 return Jwt( token, claimsSet.issueTime.toInstant(), claimsSet.expirationTime.toInstant(), signedJwt.header.toJSONObject(), claimsSet.claims ) } }
在Spring配置中注册该Decoder:
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class SecurityConfig { @Bean fun jwtDecoder(): JwtDecoder { return CustomEdDsaJwtDecoder("https://你的认证服务器地址/.well-known/jwks.json") } }
5. 更简便的方法:升级Spring Security版本
如果项目可以升级到Spring Security 6.0+,无需自定义Decoder——Spring Security 6.0开始原生支持EdDSA(依赖的nimbus-jose-jwt版本已更新至支持EdDSA的版本)。只需按普通JWT资源服务器配置即可:
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.oauth2.jwt.JwtDecoders @Configuration @EnableWebSecurity class SecurityConfig { @Bean fun filterChain(http: HttpSecurity) = http .authorizeHttpRequests { auth -> auth.anyRequest().authenticated() } .oauth2ResourceServer { oauth2 -> oauth2.jwt { jwt -> jwt.decoder(JwtDecoders.fromIssuerLocation("https://你的认证服务器地址/")) } } .build() }
该配置会自动从认证服务器的JWKS URI获取Ed25519公钥并完成验证,无需任何自定义代码。
内容的提问来源于stack exchange,提问作者D-Wire

