使用Ktor与JWT RS256进行身份验证会导致非法base64字符异常

问题描述 投票:0回答:1

我正在尝试使用 JWT 在我的 Ktor 服务器中实现身份验证,特别是使用 RS256 签名。

我正在遵循官方文档示例项目,但我偶然发现了一个我无法弄清楚的

500: java.lang.IllegalArgumentException: Illegal base64 character 2b
错误。

这是我的 HOCON 配置和示例私钥:

ktor {
  development = true
  deployment {
    port = 8080
    port = ${?PORT}
    autoreload = true
    watch = [backend]
  }
  application {
    modules = [app.company.ApplicationKt.module]
  }
}

jwt {
  privateKey = "VFVsSlJreFVRbGhDWjJ0eGFHdHBSemwzTUVKQ1VUQjNVMnBCY0VKbmEzRm9hMmxIT1hjd1FrSlJkM2RJUVZGSmExTXdZMWR5ZVVkcldtTkRRV2RuUVUxQmQwZERRM0ZIVTBsaU0wUlJTVXBDVVVGM1NGRlpTbGxKV2tsQlYxVkVRa0ZGY1VKQ1FpOW9UMDVPYzBwaE9GY3ZNMFUwV2xadkwwMDVaRUpKU1VVZ01FRTFUa2RGU2pkc1lYQk9kSFozUkM5TVNrSklRV3BzVkRGblRXeEZlWEZVZG1GcVREYzFUM2xaY2lzdmIwZGpWbmRVY0djcldVbHdZekZuV2pZeWR5QmxRMjR2TlZGaFFua3pRa2RHWkVOVVFsZENWSGhPUmtacFNUbFNZMGxuUVVoVFZYVlpUWGxOT1V3MGR6Uk1Va00wUW1ObFNHRklWMHRFZEcweWMyZDJJRmRUVDNaVlNETkZVM2xFWm1kbGEzUnVSVGM0TldkdVpWWXpjRWhPY0RWVWFYbzVVblIyVTNkaFJXcHNkR2N3TWxSSVdFZFVSVzlsWTI4eVIwMUVjQ3NnVDJWVGJsWkJRM1pFTUZremRIVXhaak00UkVsT1pFOVZWamxrY0Znek0zUkxUSE53Tm1GeGMyOTFUVVZFVDBSM1JTc3lTVGxRYmtoNGNXbENXVXc0YUNCbVZWa3lXRlZUUVdnMVNDczVWMnh5UlUwM2QwTmlOekV6VEdveGJXRm9SSGxIVmxGd05sZ3pNWGRWVlc1WFZsQlJUa2N4TURNNFUySXlibkJqY2xrM0lFRTBkRlpUUzJZeFZYbHhNMUJUWmxOV2VXZE9ObFpJYVdzelN6aFJXSGxoS3poUVRXTkVRVXRyY3pjeVZWWk1ja2xHWXpGV0wyVTViREJXV2xNd1IyUWdjbGh6TVRKblFXNXJZMk5XUmpBMVVuWTFjRUZGUnpOb1pXMHZUVEl5WTFOME5HOVpOVU5FUkhOR2VuTnNOM1ZtWkhaTmJsYzVXV0ZTZDJ0VlRITjFZaUJKY0M4dlZuZFlNVkJsZFhGT1ZEVXlNM2N6UlRsUlowUnBhazV0WVZOek1FaDRWM3A0U0ZWdGFETjViVlJGTDFVMFNrMTRaa1p0VW1aTE9FTmFZMVZrSUVRelNUaGhTbFZ5UXpJM2NUZHRjbXRWUkd4Mk0wcDJjVUpzVUZReVpsWXlhQzl6VFd4b01sSjVNRTh3TkdadlFXRmpaMnRPTldkVldFRXlTblJZVjFJZ2RXRXdOVmg1Y1dGM2FDOHpPVVZMWVROcVYwRnJkakZHUldvdmJqQlVRWHBRYUhGb2JFaHllamhyY0hKRGExTTJRMDlpZVZsTkwzbE9ZMEZpVG5ob1ppQnhaMUV5Y1ZOSmFpOTRabEE1V0hKTFdHOXFkRTVIVHpOTGNIaEVMMEZTVkhwT1VXVnhPVUk1TjFscmJrZG5aVXhLYlhjeWRsRkVVM0ZSY1hRM2FVeDRJSFp0Y2tSc1dHTjBSWFpzYzJ4TGRUaEhUVmgzVEhVeVVXTXlNamxGY1hWelIydFVNSGxDTlVoMGRVbFFXVFF6YWxVdmRFTXZVM2c1YVhBdlVFc3JhVTRnY0hOUVZFMU9jbWxSU3pWbmRrdFNRMjVLT0hWMFZteDJiR0p2WWtWNFJqVkNRa3A2UWxObk4yMU1lRWgxVVhCc1VVaDVNV0l3WlUxb2MxVlpVemxoV0NCa2VFNXNVbTUzVVhOalJFZGxUR2hSVUhOUmNqQkxOREJWTVM5NU1USkZiemx5WlhoRFp5dHRVMmxsTDNOMWMzTjNUM0pTYm1WR2NXOXpVSFpHVDBkWUlFMTFTV0p5VFZSRVJuSnFPVGRLZWtwWFJERk9SR0ZpZFdoc056RkhTSFp1Wkdob2RIUjZVMDlsV0dkWVlsTjVRVkV6ZVdneFUxZDBSemh6WWtVeEwyRWdjMmRZTkVkTlpGSkxNMVZFWVhCNWFUSlVSVlJIVFhKTU1VWlJTbXBUV1VONWJ6QjBhVXBtV1c5VGEyaFFWU3R4YlZObmNVWlBkMWRCVG1SRVFqaHNZaUE1WkhsaFMwUlpPVzVZYW1aRGJqVXJTWEZJTTNoVmFTdFFRMU5NY0hKQlNVRjRaM1ZQWTJ4c04xZFBZbFkxWW5aelQyRkJRaTluUzJSd2JuTnpkVUo2SUM4eWVIRnlTM1ZMWlVrd1lqTk9Xa05UUVdoeE0wcGlaMHQ0TkdZM0wxWlJhR0pKV1hkVEszUkhjVXRMTjBoeE5UVlVlSHBaYXpWUVJrdFJZVTFqZWtvZ1JUZERUWEY0VFcxYVNtNDJTVWhxWXpKS1l6aE1ObEJUVFM5d2NIcHFhbEV2VjJKVU9HaEpPVFpOUkdsek1HMHlWMkk1ZEM5WlYxVk9aSFZ0YW1oaU5DQjRja0Z3VFV0T1ZYSTVkek0zZGs1SVR6TlJUSEJtTlRBdmIwMHpkMnBEZWpSRFVYSjNXREZaTDBwdlJHSk5kRnBrYjFaNE9HcHJSRFZJSzJORVNuRklJRUV3WWpKSGQwWjVhR05aYUd4RFF6YzRTSEZOZUVOYVYxcHdVVlEzWjBoR1pYRllSVU5yYUhJNWQwZGtaVTl1TWpSMmVEUlZMMmxQV1RkMlJYaGhiV2NnU0VKeUt6Tm1iM1JMYjFoRVdsRnFiREpZU1hSTVFuSkdXV0ZhTW1SSVNUbGlNRXhSY1V4a2JIVjBSRlV5UzB4SU1tUlRWVkJMVTNjNGFWcEtlbWxhZHlCQ1JESnpRV00yYkZCRUx6TkdVRVkxYUdGVEt6QTFOVVpCVEVaeVQzcDVUV2wwYUZGdVdrSXpTall6TDBoSU1USlJVa3BUUjNWT2JtVmxNRVpOTTNsTElDdDVSekJVWjNVeVJtbHROSEZOTjJwT1ZYUXdSSFozVkZoc2RFc3hhR0pYTVVsM1ZIZG9hVkkxVTBwWE5rNUJVbkZLVlZOeWVuTmxWWFpSTW1Rek1ra2dTa0ZVVTBKVGJUTnJiWGN2TVZWa1NETXdOV2hwS3pSdGR6RnJSblZCYlU5NFpXTnNjMnR6ZGxCS2IzSXJSR1ZEYTJWTUsyNTRPVTl3YzNCb1drRXhaeUF3VUVOQmVVUjJSMDk1VXpCUVFXUlNOVUZFVkZjNGF6UXphSGxCVHpGb2NIWk1iMlp0VWtKYVowRm1TUQ"
  issuer = "http://0.0.0.0:8080/"
  audience = "http://0.0.0.0:8080/login"
  realm = "Company Login"
}

postgres {
  url = ${pg_url}
  user = ${pg_username}
  password = ${pg_password}
}

我的配置安全模块:

fun Application.configureSecurity() {
    val jwtAudience = environment.config.property("jwt.audience").getString()
    val jwtIssuer = environment.config.property("jwt.issuer").getString()
    val jwtRealm = environment.config.property("jwt.realm").getString()
    val jwtPK = environment.config.property("jwt.privateKey").getString()
    val jwkProvider = JwkProviderBuilder(jwtIssuer)
        .cached(10, 24, TimeUnit.HOURS)
        .rateLimited(10, 1, TimeUnit.MINUTES)
        .build()
    install(Authentication) {
        jwt("auth-jwt") {
            realm = jwtRealm
            verifier(jwkProvider, jwtIssuer) {
                acceptLeeway(3)
            }
            validate { credential ->
                if (credential.payload.getClaim("username").asString() != "") {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
            challenge { _, _ ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
        }
    }

    routing {
        post("/login") {

            val userCredentials = call.receive<UserLoginCredentials>()

            //todo check credentials
            if (userCredentials.username != "mike" || userCredentials.password != "shh") {
                call.respond(HttpStatusCode.Unauthorized, "Credentials do not match.")
                return@post
            }

            val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey
            val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(jwtPK))
            val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8)
            val token = JWT.create()
                .withAudience(jwtAudience)
                .withIssuer(jwtIssuer)
                .withClaim("username", userCredentials.username)
                .withExpiresAt(Date(System.currentTimeMillis() + 60000))
                .sign(Algorithm.RSA256(publicKey as RSAPublicKey, privateKey as RSAPrivateKey))
            call.respond(hashMapOf("token" to token))
        }
        authenticate("auth-jwt") {
            get("/hello") {
                val principal = call.principal<JWTPrincipal>()
                val username = principal!!.payload.getClaim("username").asString()
                val expiresAt = principal.expiresAt?.time?.minus(System.currentTimeMillis())
                call.respondText("Hello, $username! Token is expired at $expiresAt ms.")
            }
        }
        staticFiles(".well-known/jwks.json", File("certs/jwks.json"))
    }
}

最后,

certs/jwks.json

{
  "keys": [
    {
      "kty": "RSA",
      "e": "65537",
      "kid": "6f8856ed-9189-488f-9011-0ff4b6c08edc",
      "n":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuYAHLNnQXSGiKS2MY4u14Pzi3Ckk2FOwghFvcE2EP9Br7pTJ4j5HDYQ7vT+LHj6N2chnhGvTxZrj2+ty5fakPyGXNsC5UEMEUfTcyWe1dN2JidRLLBsUV3CvlmGdPPVLODrQQm7TMVA5x2Rwq9w9OiuD9KMF79Z87T6l7JfM9lXG+TM+JjR1pD25bMm8pCzb5+VKXEsBYwgXMPVIZ4mSm/07daiXmsfj7XDNP6U1B5xltZSdb3ZFiNYLrZNZHvsO+Q2shma9UwqRLqr/Fqx4oNggbLLqut4BujLj/Gq76E4LgwYUqdx7hVOMjZGt9E5tyFMWPRJ1SyK37zXeYxSRcwIDAQAB"
    }
  ]
}

密钥对是使用

openssl genpkey -algorithm RSA -out private_key.pem -aes256
生成的。当我向 /login 端点发出 POST 请求(发送“mike”、“shh”负载)时,我收到非法字符错误。调试服务器似乎指向我不明白的
val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey
。我错过了什么?

更新: 好吧,我已经使用

ssh-keygen -t rsa -C "email"
生成了新的密钥对,然后使用
ssh-keygen -f ktor.pub -e -m pem > ktor.pub.pem
作为公钥,
ssh-keygen -p -m PEM -f ktor
作为私钥将它们转换为 pem 文件。然后,我删除了所有新行并将它们转换为 Base64URL 编码。我还使用
openssl rsa -pubin -in ktor.pub.pem -text -noout | grep "Exponent" | awk '{prin t $2}'
编码了指数(“e”)并更新了项目中的字段。现在,当我调试调用时,异常似乎是由该函数调用
500: java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
引起的
val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey
,特别是以下中的
publicKey = kf.generatePublic(new RSAPublicKeySpec(modulus, exponent));

public PublicKey getPublicKey() throws InvalidPublicKeyException {
    PublicKey publicKey = null;

    switch (type) {
        case ALGORITHM_RSA:
            try {
                KeyFactory kf = KeyFactory.getInstance(ALGORITHM_RSA);
                BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("n")));
                BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("e")));
                publicKey = kf.generatePublic(new RSAPublicKeySpec(modulus, exponent));
            } catch (InvalidKeySpecException e) {
                throw new InvalidPublicKeyException("Invalid public key", e);
            } catch (NoSuchAlgorithmException e) {
                throw new InvalidPublicKeyException("Invalid algorithm to generate key", e);
            }
            break;
kotlin jwt ktor
1个回答
0
投票

最初,您发布的私钥值显然是假的(这很好,您不想公开真正的私钥),但是

ssh-keygen (mod) -mPEM
的结果不是 PKCS8,并且不适合在标准 Java 加密中使用,即
 PKCS8EncodedKeySpec
。 OTOH
ssh-keygen (mod) -mPKCS8
(空密码)的结果是,没有密码或 3.0 以上的
openssl genpkey
的输出也是如此,或者
openssl genrsa
没有密码并以传统
or
PKCS8 输入格式。

RSA 公钥的 JWK

必须具有 e base64url 中的指数(不是像您那样的十进制)和 n base64url 中的模数,而不是像您那样的 base64 中的整个 X.509/PKIX SPKI 结构(显然来自 OpenSSL 格式)公钥文件,OpenSSH openssl pkey 奇怪地称之为

ssh-keygen
而不是
-mPKCS8
)。
您引用的示例代码(我链接的是当前版本,但它似乎没有改变)的公钥 JWK 位于 

https://github.com/ktorio/ktor-documentation/blob/2.3.7/ codeSnippets/snippets/auth-jwt-rs256/certs/jwks.json

对应于中的私钥 https://github.com/ktorio/ktor-documentation/blob/2.3.7/codeSnippets/snippets/auth-jwt- rs256/src/main/resources/application.conf——采用base64 PKCS8-clear,因此在Java crypto PKCS8EncodedKeySpec中解码后可用。但是 AFAICS 没有任何相关的 ktor 文档告诉您如何创建它。如果您在 Unix 上有 OpenSSL(包括几乎 Unix,如 WSL 或 git4win),您可以这样做 -mPEM

© www.soinside.com 2019 - 2024. All rights reserved.