我正在尝试使用 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;
最初,您发布的私钥值显然是假的(这很好,您不想公开真正的私钥),但是
ssh-keygen (mod) -mPEM
的结果不是 PKCS8,并且不适合在标准 Java 加密中使用,即 PKCS8EncodedKeySpec
。 OTOH ssh-keygen (mod) -mPKCS8
(空密码)的结果是,没有密码或 3.0 以上的 openssl genpkey
的输出也是如此,或者 openssl genrsa
没有密码并以传统 orPKCS8 输入格式。 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