본문 바로가기
자바와 코틀린

JWT

by pius712 2024. 11. 17.

 

 

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개 라이브러리가 유명한거 같다.

이번 실습은 스타가 더 많은 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
        }
    }   
}