티스토리 뷰

개요

  • JWT(JSON Web Token)는 전통적인 Opaque Token과 반대되는 개념으로 제 3자가 토큰 문자열에서 주요 정보를 확인 가능한 것이 특징이다. (반면에 Opaque TokenUUID와 같이 의미 없는 랜덤 문자열을 의미하며, 주요 정보는 서버 사이드에 안전하게 격리된 저장소에 랜덤 문자열과 매칭하여 저장한다.)
  • JWT 도입의 장점은 토큰 관리에 있어 별도의 저장소가 필요하지 않아 진정한 의미의 상태 없는 마이크로서비스 구현이 가능하다는 것이고, 단점은 한번 발급한 토큰에 대한 일괄 만료 등의 직접적인 제어가 힘들다보니 보안을 위한 추가 절차의 설계가 필요하다는 것이다. (이에 대한 보완책으로 Access TokenJWT 방식으로 수명을 짧게 설정하고, Refresh TokenOpaque 방식으로 서버 사이드에서 제어하고 수명을 길게 설정하기도 한다.)
  • 이번 글에서는 Java 진영에서 가장 완성도가 높고 널리 쓰이고 JWT 라이브러리인 JJWT를 이용하여 JWT를 생성하고 유효성 검사를 수행하는 예를 설명하고자 한다.

사전 조건

  • 본 예제는 Spring Boot와 관계없지만, Spring Boot 기반 프로젝트에서 작성하여 이 방식을 추천한다. Spring Boot 기반 프로젝트 생성 방법은 본 블로그의 이 글을 참고한다.

build.gradle.kts 라이브러리 추가

  • JWT를 사용하기 위해 아래와 같이 JJWT 라이브러리를 프로젝트에 추가한다.
dependencies {
    implementation("io.jsonwebtoken:jjwt-api:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
}

JWT Key 생성

  • JWTSignature를 생성하는 알고리즘 방식을 HS256으로 설정할 경우, Secret Key가 요구된다. Secret Key는 서버 사이드에서만 존재해야 하며, 반드시 소스 코드가 아닌 운영체제 환경 변수 또는 보안 처리된 공간에 별도로 격리되어 보관되어야 한다. (Spring Boot에서 환경 변수를 불러오는 방법은 본 블로그의 이 글을 참고한다.)
// Secret Key를 기반으로 Key 생성
// Key 문자열 크기가 256 bits 이하일 경우 io.jsonwebtoken.security.WeakKeyException 발생
val key = Keys.hmacShaKeyFor("{secret-key}".toByteArray(StandardCharsets.UTF_8))

JWT 생성

  • 앞서 생성한 Secret Key를 기반으로 실제 유효한 JWT 문자열을 생성할 차례이다. (프로덕션 레벨에서의 생성 예는 회원이 username, password로 로그인에 성공한 시점이다.)
// 앞서 생성한 Key를 기반으로 JWT 생성
// header={"alg": "HS256"}, body={"sub": "5A697K8J0O6HoZgNK62wE5", "iat": 1637023146, exp: "1637030346"}, signature=bDQBRUnvs275dfF7VRWJqE-WGTlNo5TFYYkNpS_GNwQ
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI1QTY5N0s4SjBPNkhvWmdOSzYyd0U1IiwiaWF0IjoxNjM3MDIzMTQ2LCJleHAiOjE2MzcwMzAzNDZ9.bDQBRUnvs275dfF7VRWJqE-WGTlNo5TFYYkNpS_GNwQ
val jwt = Jwts.builder()
    // 회원 식별 문자열 저장 (sub)
    .setSubject("{user-id}")
    // 토큰 발급 일시 저장 (iat)
    .setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.of("Asia/Seoul")).toInstant()))
    // 토큰 만료 일시 저장 (exp)
    .setExpiration(Date.from(LocalDateTime.now().plusHours(2).atZone(ZoneId.of("Asia/Seoul")).toInstant()))
    // 시그너쳐 알고리즘 지정
    .signWith(key, SignatureAlgorithm.HS256)
    .compact()
  • 생성된 JWT 문자열의 Header, Payload는 누구나 내용을 확인할 수 있으므로 절대로 회원과 연관된 중요 정보를 payload에 담아서는 안된다. 위 예제에서는 최소한의 필수 정보인 회원 식별 문자열, 토큰 발급 일시, 토큰 만료 일시를 저장했다.
  • SignatureHeader, Payload, 마지막으로 서버 사이드에만 존재하는 Secret Key를 기반으로 생성된다. 따라서, 서버 사이드에서는 해당 JWT가 위조되지 않은 안전하게 생성된 토큰인지 확인할 수 있다.

JWT 유효성 검사

  • 앞서 생성한 JWT 문자열의 유효성을 검사할 차례이다. (프로덕션 레벨에서의 유효성 검사 예는 회원 권한이 요구되는 API 요청 시점이다.)
// JWT 문자열을 파씽하여 오브젝트로 변환
// 토큰 형식이 유효하지 않을 경우 io.jsonwebtoken.MalformedJwtException 예외 발생
// 토큰 만료시 io.jsonwebtoken.ExpiredJwtException 예외 발생
// 시그너쳐 미일치시 io.jsonwebtoken.security.SignatureException 예외 발생
val jwtObject = Jwts.parserBuilder()
    .setSigningKey(Keys.hmacShaKeyFor("{secret-key}".toByteArray(StandardCharsets.UTF_8)))
    .build()
    .parseClaimsJws(jwt)

// 파씽된 JWS 오브젝트 출력
// header={alg=HS256},body={sub=5A697K8J0O6HoZgNK62wE5, iat=1637023146, exp=1637030346},signature=bDQBRUnvs275dfF7VRWJqE-WGTlNo5TFYYkNpS_GNwQ
println(jwtObject)

// 회원 식별 문자열 출력
// 5A697K8J0O6HoZgNK62wE5
println(jwtObject.body.subject)

// 토큰 발급 일시 출력
// 2021-11-16T00:39:06
println(LocalDateTime.ofInstant(Instant.ofEpochMilli(jwsObject.body.issuedAt.time), ZoneId.of("Asia/Seoul")))

// 토큰 만료 일시 출력
// 2021-11-16T02:39:06
println(LocalDateTime.ofInstant(Instant.ofEpochMilli(jwsObject.body.expiration.time), ZoneId.of("Asia/Seoul")))

참고 글

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함