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

使用Ktor实现RSA256验证JWT时遇404错误求助

Ktor非对称JWT认证报错解决

问题场景

使用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,直接加载本地公钥完成验证,步骤如下:

  1. 修改安全配置,替换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
                }
            }
        }
    }
}
  1. 修正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机制,需完成以下操作:

  1. 添加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中注册这个路由。

  1. 生成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))
}
  1. 保留原安全配置即可(此时JwkProvider能正常获取到JWKS)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 08:17:02