Skip to content
Server Plugin

JSON Web 權杖

必需的依賴: io.ktor:ktor-server-auth, io.ktor:ktor-server-auth-jwt

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

原生伺服器
Ktor supports Kotlin/Native and allows you to run a server without an additional runtime or virtual machine.
支援: ✖️

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

Ktor 處理透過 Authorization 標頭以 Bearer 模式傳遞的 JWT,並允許您:

  • 驗證 JSON web token 的簽章;
  • 對 JWT 酬載執行額外驗證。

您可以在 Ktor 伺服器中的驗證和授權 章節中獲取有關 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 的簽章。請注意,驗證方式 取決於用於簽署 token 的演算法。
    • 對 JWT 酬載執行 額外驗證
  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 {
        // Configure jwt authentication
    }
}

您可以選擇指定一個 提供者名稱,該名稱可用於 驗證指定的路由

配置 JWT

在本節中,我們將看到如何在 Ktor 伺服器應用程式中使用 JSON web token。我們將展示兩種簽署 token 的方法,因為它們需要略微不同的 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 = "MIIBVQIBADANBgkqhki9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+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:生成 token

若要生成 JSON web token,您可以使用 JWTCreator.Builder。以下程式碼片段展示了如何為 HS256RS256 演算法執行此操作:

kotlin
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))
}
kotlin
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))
}
  1. post("/login") 定義了一個用於接收 POST 請求的驗證 路由
  2. call.receive<User>() 接收 作為 JSON 物件傳送的使用者憑證,並將其轉換為 User 類別物件。
  3. JWT.create() 使用指定的 JWT 設定生成 token,添加一個帶有接收到的使用者名稱的自訂 claim,並使用指定的演算法簽署 token:
    • 對於 HS256,使用共享密鑰簽署 token。
    • 對於 RS256,使用公鑰/私鑰對。
  4. call.respond token 作為 JSON 物件發送給客戶端。

步驟 3:配置 realm

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

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

步驟 4:配置 token 驗證器

verifier 函數允許您驗證 token 的格式及其簽章:

  • 對於 HS256,您需要傳遞一個 JWTVerifier 實例來驗證 token。
  • 對於 RS256,您需要傳遞 JwkProvider,它指定一個 JWKS 端點,用於存取用於驗證 token 的公鑰。在我們的案例中,發行者是 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 酬載

  1. validate 函數允許您對 JWT 酬載執行額外驗證。檢查 credential 參數,它代表一個 JWTCredential 物件並包含 JWT 酬載。在以下範例中,檢查了自訂 username claim 的值。

    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 claim 的值和 token 的過期時間。

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