使用Ktor实现RSA256验证JWT时遇404错误求助
问题场景
使用Ktor开发API,实现基于非对称密钥的JWT认证,Token可正常生成,但访问受保护路由时出现404错误,提示无法获取/.well-known/jwks.json。
相关代码
安全配置代码
fun Application.configureSecurity() { authentication { jwt { val jwtAudience = "http://0.0.0.0:8080/testing" realm = "realm" val jwkProvider = JwkProviderBuilder("http://0.0.0.0:8080/") .cached(10, 24, TimeUnit.HOURS) .rateLimited(10, 1, TimeUnit.MINUTES) .build() verifier(jwkProvider, "http://0.0.0.0:8080/") { acceptLeeway(3) } validate { credential -> if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null } } } }
JWT生成服务代码
fun generateJwt(user: User): String { val publicKey = readPublicKey("src/main/resources/keys/jwt_dev_public.key") val privateKey = readPrivateKey("src/main/resources/keys/jwt_dev_private.key", "cseWKyDM9nf6PPxbjfDA85BcY") return JWT.create() .withAudience("http://0.0.0.0:8080/hello") .withIssuer("http://0.0.0.0:8080/") .withClaim("name", user.name) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) .sign(Algorithm.RSA256(publicKey, privateKey)) }
控制器代码
fun Route.testJWT() { get("/generate_jwt") { val token = generateJwt(User(UUID.randomUUID(), "name", "password", "email")) call.respond(token) } authenticate { get("test_jwt") { call.respond("OK") } } }
错误信息
2023-04-12 14:29:01.634 [eventLoopGroupProxy-4-2] INFO Application -
404 Not Found: GET - /.well-known/jwks.json in 3ms 2023-04-12
14:29:01.636 [eventLoopGroupProxy-4-1] TRACE io.ktor.auth.jwt - Failed
to get JWK com.auth0.jwk.SigningKeyNotFoundException: Failed to get
key with kid null Caused by: java.util.concurrent.ExecutionException:
com.auth0.jwk.NetworkException: Cannot obtain jwks from url
http://0.0.0.0:8080/.well-known/jwks.json Caused by:
com.auth0.jwk.NetworkException: Cannot obtain jwks from url
http://0.0.0.0:8080/.well-known/jwks.json Caused by:
java.io.FileNotFoundException:
http://0.0.0.0:8080/.well-known/jwks.json
错误原因及解决方案
核心原因
当前配置采用JWKS(JSON Web Key Set)方式验证JWT,但你的服务未暴露/.well-known/jwks.json端点;同时生成JWT时未设置kid(密钥ID),导致验证方无法匹配到对应密钥。此外,生成JWT的audience和配置中的jwtAudience不一致,即使通过JWKS验证,后续也会因受众不匹配被拒绝。
方案一:直接使用本地公钥验证(推荐,适合本地/单实例场景)
不需要依赖JWKS,直接加载本地公钥完成验证,步骤如下:
- 修改安全配置,替换JwkProvider为本地公钥验证:
fun Application.configureSecurity() { authentication { jwt { val jwtAudience = "http://0.0.0.0:8080/testing" realm = "realm" // 加载本地公钥 val publicKey = readPublicKey("src/main/resources/keys/jwt_dev_public.key") val algorithm = Algorithm.RSA256(publicKey, null) verifier(algorithm) { withIssuer("http://0.0.0.0:8080/") acceptLeeway(3) } validate { credential -> if (credential.payload.audience.contains(jwtAudience)) { JWTPrincipal(credential.payload) } else { null } } } } }
- 修正JWT生成代码的
audience,与配置中的jwtAudience保持一致:
fun generateJwt(user: User): String { val publicKey = readPublicKey("src/main/resources/keys/jwt_dev_public.key") val privateKey = readPrivateKey("src/main/resources/keys/jwt_dev_private.key", "cseWKyDM9nf6PPxbjfDA85BcY") return JWT.create() .withAudience("http://0.0.0.0:8080/testing") // 匹配配置中的jwtAudience .withIssuer("http://0.0.0.0:8080/") .withClaim("name", user.name) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) .sign(Algorithm.RSA256(publicKey, privateKey)) }
方案二:启用JWKS验证(适合多实例/密钥轮换场景)
如果需要使用JWKS机制,需完成以下操作:
- 添加JWKS端点,返回包含公钥的JWKS格式数据:
fun Route.jwksRoute() { get("/.well-known/jwks.json") { val publicKey = readPublicKey("src/main/resources/keys/jwt_dev_public.key") // 创建JWK并指定kid,后续生成JWT时要使用相同的kid val jwk = RSAKey.Builder(publicKey) .keyId("jwt-dev-key-1") .build() val jwks = JWKS(setOf(jwk)) call.respond(jwks) } }
记得在Application中注册这个路由。
- 生成JWT时添加
kid(与JWKS中的keyId一致),并修正audience:
fun generateJwt(user: User): String { val publicKey = readPublicKey("src/main/resources/keys/jwt_dev_public.key") val privateKey = readPrivateKey("src/main/resources/keys/jwt_dev_private.key", "cseWKyDM9nf6PPxbjfDA85BcY") return JWT.create() .withAudience("http://0.0.0.0:8080/testing") .withIssuer("http://0.0.0.0:8080/") .withClaim("name", user.name) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) .withKeyId("jwt-dev-key-1") // 匹配JWKS中的keyId .sign(Algorithm.RSA256(publicKey, privateKey)) }
- 保留原安全配置即可(此时JwkProvider能正常获取到JWKS)。
内容的提问来源于stack exchange,提问作者Rodrigo Batista

