Skip to content
Server Plugin

JSON Web Tokens

必要相依性io.ktor:ktor-server-authio.ktor:ktor-server-auth-jwt

程式碼範例auth-jwt-hs256, auth-jwt-rs256

原生伺服器
Ktor 支援 Kotlin/Native,並允許您在沒有額外執行階段或虛擬機的情況下執行伺服器。
支援:✖️

JSON Web Token (JWT) 是一個開放標準,定義了一種將資訊作為 JSON 物件在各方之間安全傳輸的方式。由於此資訊使用共用金鑰(使用 HS256 演算法)或公鑰/私鑰對(例如 RS256)進行簽名,因此可以被驗證和信任。

Ktor 處理在 Authorization 標頭中以 Bearer 配置傳遞的 JWT,並允許您:

  • 驗證 JSON Web Token 的簽名;
  • 對 JWT 承載內容 (payload) 執行額外的驗證。

您可以在 Ktor Server 中的驗證與授權 章節中取得有關 Ktor 驗證與授權的一般資訊。

新增相依性

要啟用 JWT 驗證,您需要在建置指令碼中包含 ktor-server-authktor-server-auth-jwt 構件:

Kotlin
Groovy
XML

JWT 授權流程

Ktor 中的 JWT 授權流程如下所示:

  1. 用戶端向伺服器應用程式中的特定驗證路由發送帶有憑據的 POST 請求。下面的範例顯示了一個 HTTP 用戶端POST 請求,憑據以 JSON 格式傳遞:
    HTTP
    POST http://localhost:8080/login
    Content-Type: application/json
    
    {
      "username": "jetbrains",
      "password": "foobar"
    }
  2. 如果憑據有效,伺服器會產生一個 JSON Web Token 並使用指定的演算法對其進行簽名。例如,這可能是帶有特定共用金鑰的 HS256,或是帶有公鑰/私鑰對的 RS256
  3. 伺服器將產生的 JWT 發送給用戶端。
  4. 用戶端現在可以使用在 Authorization 標頭中以 Bearer 配置傳遞的 JSON Web Token,向受保護的資源發送請求。
    HTTP
    GET http://localhost:8080/hello
    Authorization: Bearer {{auth_token}}
  5. 伺服器接收請求並執行以下驗證:
    • 驗證權杖 (token) 的簽名。請注意,驗證方式取決於用於簽名權杖的演算法。
    • 對 JWT 承載內容 (payload) 執行額外的驗證
  6. 驗證後,伺服器會回應受保護資源的內容。

安裝 JWT

要安裝 jwt 驗證提供程式,請在 install 區塊內呼叫 jwt 函式:

kotlin
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
//...
install(Authentication) {
    jwt {
        // 配置 jwt 驗證
    }
}

您可以選擇性地指定一個 提供程式名稱 (provider name),該名稱可用於驗證指定的路由

設定 JWT

在本節中,我們將了解如何在伺服器端 Ktor 應用程式中使用 JSON Web Token。我們將展示兩種簽名權杖的方法,因為它們需要稍微不同的方式來驗證權杖:

  • 使用帶有指定共用金鑰的 HS256
  • 使用帶有公鑰/私鑰對的 RS256

您可以在此處找到完整的專案:auth-jwt-hs256auth-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'"
}

請注意,秘密資訊不應以純文字形式儲存在設定檔中。請考慮使用環境變數來指定這些參數。

您可以透過以下方式在程式碼中存取這些設定

kotlin
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()
kotlin
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 Token,您可以使用 JWTCreator.Builder。下面的程式碼片段顯示了如何針對 HS256RS256 演算法執行此操作:

kotlin
post("/login") {
    val user = call.receive<User>()
    // 檢查使用者名稱和密碼
    // ...
    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))
}
kotlin
post("/login") {
    val user = call.receive<User>()
    // 檢查使用者名稱和密碼
    // ...
    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))
}
  1. post("/login") 定義了一個用於接收 POST 請求的驗證路由
  2. call.receive<User>() 接收以 JSON 物件發送的使用者憑據,並將其轉換為 User 類別物件。
  3. JWT.create() 使用指定的 JWT 設定產生權杖,新增一個包含所接收使用者名稱的自訂宣告 (claim),並使用指定的演算法對權杖進行簽名:
    • 對於 HS256,使用共用金鑰對權杖進行簽名。
    • 對於 RS256,使用公鑰/私鑰對進行簽名。
  4. call.respond 將權杖作為 JSON 物件發送給用戶端。

步驟 3:設定領域 (realm)

realm 屬性允許您設定存取受保護路由時要在 WWW-Authenticate 標頭中傳遞的領域。

kotlin
val myRealm = environment.config.property("jwt.realm").getString()
install(Authentication) {
    jwt("auth-jwt") {
        realm = myRealm
    }
}

步驟 4:設定權杖驗證器

verifier 函式允許您驗證權杖格式及其簽名:

  • 對於 HS256,您需要傳遞一個 JWTVerifier 執行個體來驗證權杖。
  • 對於 RS256,您需要傳遞 JwkProvider,它指定一個 JWKS 端點,用於存取驗證權杖所需的公鑰。在我們的案例中,發行者是 http://0.0.0.0:8080,因此 JWKS 端點位址將是 http://0.0.0.0:8080/.well-known/jwks.json
kotlin
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())
    }
}
kotlin
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 承載內容 (payload)

  1. validate 函式允許您對 JWT 承載內容執行驗證。此函式是必需的:如果您沒有配置它,提供程式初始化將拋出 IllegalArgumentException。請檢查 credential 參數,它代表一個 JWTCredential 物件並包含 JWT 承載內容。在下面的範例中,會檢查自訂 username 宣告的值。

    kotlin
    install(Authentication) {
        jwt("auth-jwt") {
            validate { credential ->
                if (credential.payload.getClaim("username").asString() != "") {
                    JWTPrincipal(credential.payload)
                } else {
                    null
                }
            }
        }
    }

    驗證成功時,傳回 JWTPrincipal

  2. challenge 函式允許您設定在驗證失敗時發送的回應。

    kotlin
    install(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 宣告的值和權杖到期時間。

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