그 동안 프로젝트를 진행하면 주로 자바를 활용하여 JWT를 발급하였습니다. 이번에는 코틀린으로 JWT 토큰을 발급해 볼 것 이며 jjwt도 0.12.x로 업그레이드 되면서 수정된 문법을 익혀볼 것 입니다.
전체 코드는 GitHub에서 확인하실 수 있습니다.
// build.gradle.kts
val jjwtVersion: String = "0.12.5"
dependencies {
// ...
implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")
}
오늘자 기준(24.3.3)으로 최신 버젼은 0.12.5
입니다. 위와 같이 build.gradle.kts
에 해당 내용을 추가합니다.
JWT 관련 환경 변수들을 가져와야 합니다. yml
에서 자동완성 기능을 사용하기 위해 ConfigurationProperties
을 활용합니다.
@ConfigurationProperties(prefix = "jwt")
data class JwtProperties(
val secret: String,
val accessTokenExpireTime: Long,
val refreshTokenExpireTime: Long,
)
위와 같이 yml
에서 사용하고 값을 주입을 받습니다.
기본 값이 없으므로 따로 지정하지 않는다면 컴파일 단계에서 실패하게 됩니다.
jwt:
secret: 7JWI64WV7Z... // 긴 값
access-token-expire-time: 30
refresh-token-expire-time: 60
JWT 암호화 알고리즘으로 HS512를 사용할 것 이기 때문에 매우 긴 값이 필요합니다.
위와 같이 yml
파일에 값을 넣어주도록 합니다.
현재 시간 단위는 "초(second)"입니다.
코드의 전체 부분입니다.
import com.seikim.kotlinjwt.auth.exception.AuthErrorCode
import com.seikim.kotlinjwt.auth.exception.AuthException
import com.seikim.kotlinjwt.auth.properties.JwtProperties
import com.seikim.kotlinjwt.member.domain.Member
import io.jsonwebtoken.*
import io.jsonwebtoken.io.Decoders
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
@Component
class TokenProvider(
private val jwtProperties: JwtProperties
) {
private val key: SecretKey by lazy {
val keyBytes = Decoders.BASE64.decode(jwtProperties.secret)
SecretKeySpec(
keyBytes,
Jwts.SIG.HS512.key().build().algorithm
)
}
companion object {
private const val BEARER_TYPE: String = "Bearer"
}
fun generateAccessToken(member: Member): String {
val now: Long = Date().time
val accessToken: String = generateToken(
member,
now,
expireTime = jwtProperties.accessTokenExpireTime
)
return accessToken
}
fun generateRefreshToken(member: Member): String {
val now: Long = Date().time
val refreshToken: String = generateToken(
member,
now,
expireTime = jwtProperties.refreshTokenExpireTime
)
return refreshToken
}
private fun generateToken(member: Member, now: Long, expireTime: Long): String {
val token: String = Jwts.builder()
.subject(member.email)
.claim("memberId", member.id)
.signWith(key)
.expiration(Date(now + (1000L * 60 * expireTime)))
.compact()
return "$BEARER_TYPE $token"
}
fun parseToken(token: String): Int {
validateBearer(token)
val jws = token.replace(BEARER_TYPE, "")
.trim()
try {
val claimsJws: Jws<Claims> = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jws)
return claimsJws.payload["memberId"] as Int
} catch (e: ExpiredJwtException) {
throw AuthException(AuthErrorCode.EXPIRED_JWT)
} catch (e: JwtException) {
throw AuthException(AuthErrorCode.INVALID_JWT)
}
}
private fun validateBearer(token: String) {
if (token.contains(BEARER_TYPE)) {
return
}
throw AuthException(AuthErrorCode.INVALID_BEARER_TYPE)
}
}
private val key: SecretKey by lazy {
val keyBytes = Decoders.BASE64.decode(jwtProperties.secret)
SecretKeySpec(
keyBytes,
Jwts.SIG.HS512.key().build().algorithm
)
}
JWT 0.12.x 로 넘어오면서 바뀐 부분 중 하나입니다. 이전에는 미리 정의하지 않고 알고리즘을 이후에 넣어줬지만 현재는 키를 정의할 때 미리 알고리즘을 넣어줍니다.
lazy
를 사용하여 객체가 생성된 후 값이 주입되도록 작성하였습니다. 이렇게하여 yml
에 정의해둔 값을 가져올 수 있습니다.
private fun generateToken(member: Member, now: Long, expireTime: Long): String {
val token: String = Jwts.builder()
.subject(member.email)
.claim("memberId", member.id)
.signWith(key)
.expiration(Date(now + (1000L * 60 * expireTime)))
.compact()
return "$BEARER_TYPE $token"
}
현재
spring security
를 사용하지 않아 권한과 관련된 부분은 없습니다.
JWT 토큰을 생성하는 부분입니다. 토큰을 생성할 때 필요한 값과 현재 시간, 만료 시간을 넘겨 받아 토큰을 생성합니다.
만약 원하는 값을 더 넣으려면 claim
와 같이 key-value 형식으로 넣을 수 있습니다.
fun parseToken(token: String): Int {
validateBearer(token)
val jws = token.replace(BEARER_TYPE, "")
.trim()
try {
val claimsJws: Jws<Claims> = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(jws)
return claimsJws.payload["memberId"] as Int
} catch (e: ExpiredJwtException) {
throw AuthException(AuthErrorCode.EXPIRED_JWT)
} catch (e: JwtException) {
throw AuthException(AuthErrorCode.INVALID_JWT)
}
}
private fun validateBearer(token: String) {
if (token.contains(BEARER_TYPE)) {
return
}
throw AuthException(AuthErrorCode.INVALID_BEARER_TYPE)
}
토큰을 검증하고 원하는 값을 가져오는 부분입니다.
Bearer
이 재대로 적용되어 있는지 검증하며, 만료 시간이 지났는지, 잘못된 토큰인지 파악합니다.
사용자의 인증 정보를 받아 정상적인 인증이라면 로그아웃을 진행하는 컨트롤러를 실행시킨 결과 정상적으로 응답하였습니다.
JWT
를 자바가 아닌 코틀린으로 발급하면서 별 다른 어려움은 없었습니다.
문법이 0.12.x 로 넘어오면서 익숙하지 않아 헷갈린 부분이 있지만 공식 문서를 찾아보며 해결을 하였습니다.
물론 아직 개인적인 아쉬움은 존재합니다.