JSON Web 令牌
必需的依赖项: io.ktor:ktor-server-auth, io.ktor:ktor-server-auth-jwt
代码示例: auth-jwt-hs256, auth-jwt-rs256
JSON Web 令牌 (JWT) 是一种开放标准,它定义了一种以 JSON 对象形式在各方之间安全传输信息的方式。由于它使用共享密钥(通过 HS256 算法)或公钥/私钥对(例如 RS256)进行签名,因此这些信息可以被验证和信任。
Ktor 处理在 Authorization 请求头中使用 Bearer 方案传递的 JWT,并允许您:
- 验证 JSON Web 令牌的签名;
- 对 JWT 载荷执行额外验证。
您可以在 Ktor 服务器中的身份验证和授权章节中获取有关 Ktor 中身份验证和授权的通用信息。
添加依赖项
要启用 JWT 身份验证,您需要在构建脚本中包含 ktor-server-auth 和 ktor-server-auth-jwt artifact:
JWT 授权流程
Ktor 中的 JWT 授权流程可能如下所示:
- 客户端向服务器应用程序中特定身份验证路由发起包含凭据的
POST请求。以下示例显示了一个 HTTP 客户端的POST请求,其中凭据以 JSON 形式传递:HTTPPOST http://localhost:8080/login Content-Type: application/json { "username": "jetbrains", "password": "foobar" } - 如果凭据有效,服务器会生成一个 JSON Web 令牌,并使用指定算法对其签名。例如,这可能是使用特定共享密钥的
HS256,或使用公钥/私钥对的RS256。 - 服务器将生成的 JWT 发送给客户端。
- 客户端现在可以使用 JSON Web 令牌向受保护资源发出请求,该令牌在
Authorization请求头中使用Bearer方案传递。HTTPGET http://localhost:8080/hello Authorization: Bearer {{auth_token}} - 服务器收到请求并执行以下验证:
- 验证后,服务器响应受保护资源的内容。
安装 JWT
要安装 jwt 身份验证提供程序,请在 install 代码块内调用 jwt 函数:
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
//...
install(Authentication) {
jwt {
// 配置 jwt 身份验证
}
}您可以可选地指定一个提供程序名称,该名称可用于身份验证指定的路由。
配置 JWT
在本节中,我们将介绍如何在服务器 Ktor 应用程序中使用 JSON Web 令牌。我们将演示两种签名令牌的方法,因为它们需要稍微不同的令牌验证方式:
- 使用
HS256和指定共享密钥。 - 使用
RS256和公钥/私钥对。
您可以在此处找到完整的项目:auth-jwt-hs256,auth-jwt-rs256。
步骤 1:配置 JWT 设置
要配置 JWT 相关设置,您可以在配置文件中创建自定义的 jwt 组。例如,application.conf 文件可能如下所示:
jwt {
secret = "secret"
issuer = "http://0.0.0.0:8080/"
audience = "http://0.0.0.0:8080/hello"
realm = "Access to 'hello'"
}jwt {
privateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+FBquToDeYcAWBe1EaLVyC45HG60zwfG1S4S3IB+y4INz1FHuZppDjBh09jptQNd+kSMlG1LkAc/3znKTPJ7QIhANpyB0OfTK44lpH4ScJmCxjZV52mIrQcmnS3QzkxWQCDAiEA1Tn7qyoh+0rOO/9vJHP8U/beo51SiQMw0880a1UaiisCIQDNwY46EbhGeiLJR1cidr+JHl86rRwPDsolmeEF5AdzRQIgK3KXL3d0WSoS//K6iOkBX3KMRzaFXNnDl0U/XyeGMuUCIHaXv+n+Brz5BDnRbWS+2vkgIe9bUNlkiArpjWvX+2we"
issuer = "http://0.0.0.0:8080/"
audience = "http://0.0.0.0:8080/hello"
realm = "Access to 'hello'"
}请注意,敏感信息不应以纯文本形式存储在配置文件中。考虑使用环境变量来指定此类参数。
您可以通过以下方式在代码中访问这些设置:
val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()val privateKeyString = environment.config.property("jwt.privateKey").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()步骤 2:生成令牌
要生成 JSON Web 令牌,您可以使用 JWTCreator.Builder。下面的代码片段展示了如何为 HS256 和 RS256 算法执行此操作:
post("/login") {
val user = call.receive<User>()
// Check username and password
// ...
val token = JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
.sign(Algorithm.HMAC256(secret))
call.respond(hashMapOf("token" to token))
}post("/login") {
val user = call.receive<User>()
// Check username and password
// ...
val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey
val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString))
val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8)
val token = JWT.create()
.withAudience(audience)
.withIssuer(issuer)
.withClaim("username", user.username)
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
.sign(Algorithm.RSA256(publicKey as RSAPublicKey, privateKey as RSAPrivateKey))
call.respond(hashMapOf("token" to token))
}post("/login")定义了一个用于接收POST请求的身份验证路由。call.receive<User>()接收以 JSON 对象形式发送的用户凭据,并将其转换为User类对象。JWT.create()生成一个具有指定 JWT 设置的令牌,添加一个包含所接收用户名的自定义声明,并使用指定算法对令牌签名:- 对于
HS256,使用共享密钥对令牌签名。 - 对于
RS256,使用公钥/私钥对。
- 对于
call.respond将令牌作为 JSON 对象发送给客户端。
步骤 3:配置 realm
realm 属性允许您设置在访问受保护路由时要在 WWW-Authenticate 请求头中传递的 realm。
val myRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
jwt("auth-jwt") {
realm = myRealm
}
}步骤 4:配置令牌验证器
verifier 函数允许您验证令牌格式及其签名:
- 对于
HS256,您需要传递一个 JWTVerifier 实例来验证令牌。 - 对于
RS256,您需要传递 JwkProvider,它指定一个 JWKS 端点,用于访问用于验证令牌的公钥。在我们的例子中,issuer 是http://0.0.0.0:8080,因此 JWKS 端点地址将是http://0.0.0.0:8080/.well-known/jwks.json。
val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
jwt("auth-jwt") {
realm = myRealm
verifier(JWT
.require(Algorithm.HMAC256(secret))
.withAudience(audience)
.withIssuer(issuer)
.build())
}
}val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
val myRealm = environment.config.property("jwt.realm").getString()
val jwkProvider = JwkProviderBuilder(issuer)
.cached(10, 24, TimeUnit.HOURS)
.rateLimited(10, 1, TimeUnit.MINUTES)
.build()
install(Authentication) {
jwt("auth-jwt") {
realm = myRealm
verifier(jwkProvider, issuer) {
acceptLeeway(3)
}
}
}步骤 5:验证 JWT 载荷
validate函数允许您对 JWT 载荷执行额外验证。检查credential形参,它表示一个 JWTCredential 对象并包含 JWT 载荷。在下面的示例中,检查了自定义username声明的值。kotlininstall(Authentication) { jwt("auth-jwt") { validate { credential -> if (credential.payload.getClaim("username").asString() != "") { JWTPrincipal(credential.payload) } else { null } } } }如果身份验证成功,则返回 JWTPrincipal。
challenge函数允许您配置在身份验证失败时发送的响应。kotlininstall(Authentication) { jwt("auth-jwt") { challenge { defaultScheme, realm -> call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") } } }
步骤 6:保护特定资源
配置 jwt 提供程序后,您可以使用 authenticate 函数保护应用程序中的特定资源。如果身份验证成功,您可以使用 call.principal 函数在路由处理程序内部检索已验证的 JWTPrincipal,并获取 JWT 载荷。在下面的示例中,检索了自定义 username 声明的值和令牌过期时间。
routing {
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.")
}
}
}