Skip to content
Server Plugin

JSON 웹 토큰

필수 의존성: io.ktor:ktor-server-auth, io.ktor:ktor-server-auth-jwt

코드 예시: auth-jwt-hs256, auth-jwt-rs256

네이티브 서버
Ktor는 Kotlin/Native를 지원하며 추가 런타임이나 가상 머신 없이 서버를 실행할 수 있게 해줍니다.
지원: ✖️

JSON 웹 토큰 (JWT)은 당사자 간에 정보를 JSON 객체로 안전하게 전송하는 방법을 정의하는 개방형 표준입니다. 이 정보는 공유 비밀( HS256 알고리즘 사용) 또는 공개/개인 키 쌍(예: RS256)으로 서명되므로 검증되고 신뢰할 수 있습니다.

Ktor는 Authorization 헤더를 통해 Bearer 스키마를 사용하여 전달되는 JWT를 처리하며 다음과 같은 기능을 제공합니다:

  • JSON 웹 토큰의 서명 확인
  • JWT 페이로드에 대한 추가 유효성 검사 수행

Ktor의 인증 및 권한 부여에 대한 일반 정보는 Ktor 서버의 인증 및 권한 부여 섹션에서 얻을 수 있습니다.

의존성 추가

JWT 인증을 활성화하려면 빌드 스크립트에 ktor-server-authktor-server-auth-jwt 아티팩트를 포함해야 합니다:

Kotlin
Groovy
XML

JWT 인증 흐름

Ktor의 JWT 인증 흐름은 다음과 같습니다:

  1. 클라이언트가 특정 인증 경로로 서버 애플리케이션에 자격 증명을 포함한 POST 요청을 보냅니다. 아래 예시는 JSON으로 전달된 자격 증명이 포함된 HTTP 클라이언트 POST 요청을 보여줍니다:
    HTTP
    POST http://localhost:8080/login
    Content-Type: application/json
    
    {
      "username": "jetbrains",
      "password": "foobar"
    }
  2. 자격 증명이 유효하면, 서버는 JSON 웹 토큰을 생성하고 지정된 알고리즘으로 서명합니다. 예를 들어, 특정 공유 비밀을 사용하는 HS256 또는 공개/개인 키 쌍을 사용하는 RS256일 수 있습니다.
  3. 서버는 생성된 JWT를 클라이언트에 보냅니다.
  4. 클라이언트는 이제 Authorization 헤더에 Bearer 스키마를 사용하여 JSON 웹 토큰을 전달하여 보호된 리소스에 요청을 보낼 수 있습니다.
    HTTP
    GET http://localhost:8080/hello
    Authorization: Bearer {{auth_token}}
  5. 서버는 요청을 수신하고 다음 유효성 검사를 수행합니다:
    • 토큰의 서명을 확인합니다. 검증 방식은 토큰 서명에 사용된 알고리즘에 따라 달라집니다.
    • 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 웹 토큰을 사용하는 방법을 살펴보겠습니다. 토큰을 확인하는 방식이 약간 다르기 때문에 토큰 서명에 대한 두 가지 접근 방식을 시연할 것입니다:

  • 지정된 공유 비밀을 사용하는 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'"
}

비밀 정보는 구성 파일에 일반 텍스트로 저장해서는 안 됩니다. 이러한 매개변수를 지정하는 데 환경 변수를 사용하는 것을 고려하십시오.

다음과 같은 방식으로 코드에서 이러한 설정에 액세스할 수 있습니다:

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 웹 토큰을 생성하려면 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 설정으로 토큰을 생성하고, 수신된 사용자 이름으로 사용자 정의 클레임을 추가하고, 지정된 알고리즘으로 토큰에 서명합니다:
    • HS256의 경우, 공유 비밀이 토큰 서명에 사용됩니다.
    • RS256의 경우, 공개/개인 키 쌍이 사용됩니다.
  4. call.respond는 토큰을 JSON 객체로 클라이언트에 전송합니다.

단계 3: Realm 구성

realm 속성을 사용하면 보호된 경로에 액세스할 때 WWW-Authenticate 헤더에 전달될 realm을 설정할 수 있습니다.

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

단계 4: 토큰 검증기 구성

verifier 함수를 사용하면 토큰 형식과 서명을 확인할 수 있습니다:

  • HS256의 경우, 토큰을 확인하기 위해 JWTVerifier 인스턴스를 전달해야 합니다.
  • RS256의 경우, 토큰을 확인하는 데 사용되는 공개 키에 액세스하기 위한 JWKS 엔드포인트를 지정하는 JwkProvider를 전달해야 합니다. 이 경우, 발급자는 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 페이로드에 대한 추가 유효성 검사를 수행할 수 있습니다. JWTCredential 객체를 나타내고 JWT 페이로드를 포함하는 credential 매개변수를 확인합니다. 아래 예시에서는 사용자 정의 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.")
        }
    }
}