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.")
}
}
}