JWT 개념정리
JWT(JSON Web Token) 은 ID token 으로 주로 사용되지만, 텍스트 기반의 general purpose 메시징 포맷이다. 즉, 인증이 아닌 데이터를 주고 받는 목적으로도 사용될 수 있는 메시지 포맷이다. 물론 메시징 포맷으로써 JWT 를 사용하기도 하지만, 일반적으로 id token 으로 많이 사용되고 있다.
구성요소
- payload : 전달하고자하는 데이터
- header: payload 와 메시지에 대한 메타 데이터로 JSON Object 이다.
- signature: 토큰의 서명
- header, payload 와 시크릿을 통해 생성된다.
Claim ? Sub ?
이번 블로그를 작성하게 된 것이 사실은 이 두 용어를 헷갈려서 찾아보고 정리했기 때문이다.
payload 는 대부분 JSON 형태로 호출하는 user 나, system 의 신원 정보를 담아둔다. 이 경우 payload 는 Clams 라고 부르게 된다.
claim 에는 몇가지 표준 스펙으로 정의된 필드들이 있다.
- iss: issuer 를 나타내는 것으로 토큰을 발행한 주체를 나타낸다.
- sub: 토큰을 받은 대상을 나타낸다.
- aud: jwt 를 받은 수신자를 나타낸다. 예를들어, oauth 를 통해서 인증을 하는 경우, oauth client 가 aud 가 될 수 있다.
- exp: 토큰의 만료 기한이다
즉, claim 은 JWT 가 id 토큰으로써 동작할 때 나타내는 신원정보를 의미하고 sub 는 claim 의 표준 스펙 필드 중 하나일 뿐이다. 🙂
토큰의 보안
위에서 설명한 claims 는 번역하면 신원에 대한 “주장” 으로, 이것을 검증해야할 필요가 있다.
일반적으로 토큰에 서명을 하거나, 토큰을 암호화하는 방식으로 진행된다.
토큰을 서명한다음, 이 서명을 해독하는 방식으로 토큰의 위변조를 판단할 수 있는 것이다.
토큰 서명부위를 signature 라고 한다.
실습 - 구현
java 에서는 찾아보니, 2개 라이브러리가 유명한거 같다.
- java-jwt : https://github.com/auth0/java-jwt
- jjwt: https://github.com/jwtk/jjwt?tab=readme-ov-file#what-is-a-json-web-token
이번 실습은 스타가 더 많은 jjwt 로 진행하고자 한다.
jjwt 는 빌더 패턴을 사용하여, jwt 를 발급하거나 파싱할 수 있도록 한다.
의존성 설치
jjwt 라이브러리와 jjwt 의 payload 파싱을 위한 jackson 라이브러리를 설치한다.
dependencies {
implementation("io.jsonwebtoken:jjwt:0.12.6")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.6")
토큰 생성
Jwts.builder() 를 통해 토큰 생성을 할 수 있다. 예제에서는 간단하게, userId 만 paylaod 에 포함하였다.
- signWith(key): 토큰을 서명하는데 사용된다. secret key 생성시 여러가지 알고리즘을 사용할 수 있으나, 예제에서는 HMAC-SHA-256 알고리즘으로 생성한 키로 서명한다.
- claims: claims 에 추가적인 정보를 넣을 수 있다.
- 표준 claim 의 경우, subject(), issuer(), expiration() 와 같은 메서드를 제공한다.
- compact(): 토큰을 url-safe 한 표준 방식으로 serialize 한다.
@Component
class JwtUtil(
@Value("\\${jwt.secret}")
private val secretKey: String,
@Value("\\${jwt.expiration}")
private val expirationTime: Long,
@Value("\\${jwt.issuer}")
private val issuer: String,
) {
val signingKey = Keys.hmacShaKeyFor(secretKey.toByteArray())
fun generateToken(userId: Long): String {
val claims: Claims = Jwts
.claims()
.add("userId", userId.toString())
.build()
return Jwts.builder()
.claims(claims)
.signWith(signingKey)
.issuer(issuer)
.subject(userId.toString())
.expiration(Date(System.currentTimeMillis() + expirationTime))
.compact()
}
}
토큰 파싱
토큰은 크게 아래의 방식으로 많이 사용된다. 따라서 이 기능들을 구현하고자 한다.
- 토큰 검증
- 토큰 유효시간 검증
- 토큰에서 유저 정보 조회
class JwtUtil(
@Value("\\${jwt.secret}")
private val secretKey: String,
@Value("\\${jwt.expiration}")
private val expirationTime: Long,
@Value("\\${jwt.issuer}")
private val issuer: String
) {
val signingKey = Keys.hmacShaKeyFor(secretKey.toByteArray())
/**
* 토큰 유효성 검증 + 만료일 검증
* */
fun validateToken(token: String): Boolean {
val claims = getClaims(token) ?: return false
return !claims.expiration.before(Date())
}
/**
* 토큰 파싱: userId 반환
* */
fun getUserId(token: String): Long {
val claims = getClaims(token) ?: throw RuntimeException("invalid token")
return claims.get("userId", String::class.java)?.toLong()
?: throw RuntimeException("invalid token")
}
/**
* 토큰 파싱: 토큰 검증 + 클레임 반환
* */
private fun getClaims(token: String): Claims? {
return try {
Jwts.parser()
.json(JacksonDeserializer(jacksonObjectMapper()))
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.payload
} catch (e: Exception) {
null
}
}
}
'자바와 코틀린' 카테고리의 다른 글
코루틴 - 코루틴 스레드 양보와 실행 스레드 (0) | 2024.09.17 |
---|---|
코루틴 - 중단함수 (0) | 2024.09.17 |
코루틴 - 구조화된 동시성 (2) | 2024.09.17 |
코루틴 - 코루틴 빌더와 Job (0) | 2024.09.17 |
코루틴 - CoroutineDispatcher (0) | 2024.09.17 |