Spring security/02.JWT활용

[JWT]프로젝트 구성 및 토큰 확인

  • -

이제 스프링 Boot에서 JWT를 사용해서 토큰을 생성하는 방법에 대해서 알아보자.

프로젝트 구성

 

의존성

JWT를 자바에서 사용하기 위해서는 여러가지 라이브러리가 있는데 여기서는 jjwt라는 녀석을 사용해보자. 이를 위해 다음의 의존성을 추가한다. 이 녀석의 역할은 토큰은 base64로 인코딩 하거나 signature를 만들고 검증하는 역할등을 수행한다.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>

 

application.yml

application.properties에는 사용자 정의 속성으로 token의 만료 시간을 적어주자. 테스트를 위해 access token은 2분 refresh token은 4분의 시간을 설정했다.

custom:
  jwt:
    access-expmin: 2     # access 토큰 만료 시간: 2분
    refresh-expmin: 4    # refresh 토큰 만료 시간: 4분

토큰의 유효기간이 길어지면 자주 로그인할 필요가 없으니 사용자의 경험은 좋아지겠지만 그만큼 토큰 탈취에 대한 위험도는 높아진다. 일반적으로 access token은 15분~1시간 정도(짧게는 5분, 길게는 24시간까지) 설정하고 refresh token은 2주~1개월(짧게는 1일 길게는 1년까지)까지도 설정한다. 이는 어디까지나 일반적인 사항이고 보안과 사용자 경험을 고려하여 적절히 걸정한다.

 

JWTUtil 작성

 

전체적인 구성

JWTUtil은 application.properties에 선언된 속성을 사용하며 크게 토큰을 만드는 부분과 토큰을 검증하고 내용을 확인하는 부분으로 구성된다.

package com.doding.example.security.jwt;

import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;

@Component
@Slf4j
public class JWTUtil {
    private final SecretKey key;

    public JWTUtil(){
        key = Jwts.SIG.HS256.key().build(); // 랜덤 salt 활용한 암호 키 생성
    }
    
    public String createAuthToken(String userId, long expireMin){
        return create(userId, "authToken", expireMin);
    }

    private String create(String userId, String subject, long expireMin){
        return null;
    }

    public Map<String, Object> checkAndGetClaims(String jwt){
        return null;
    }
}

 

상세구현

이제 각각의 메서드들을 구현해보자. 자세한 설명은 주석으로 대체한다.

private String create(String userId, String subject, long expireMin){
    final JwtBuilder builder = Jwts.builder();
    // Payload 설정 - expire, claim(key-value 쌍) 등 정보 포함
    Date expireDate = new Date(System.currentTimeMillis() + 1000 * 60 * expireMin);
    if (userId != null) {
        builder.claim("user", userId);
    }
    
    // 직렬화 처리
    builder.subject(subject).expiration(expireDate).signWith(key);
    String jwt = builder.compact();
    log.debug("토큰 발행: {}", jwt);
    return jwt;
}

 

public Map<String, Object> checkAndGetClaims(String jwt){
    Jws<Claims> token = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt);
    log.trace("token: {}", token);
    return token.getPayload();
}

 

토큰 검증

이제 토큰이 잘 생성되고 적절히 validation 되는지 확인해보자.

@ExtendWith(SpringExtension.class)      // 설정 파일을 로드하지 않음
@TestPropertySource(properties = {      // yml 파일을 가져오지 않음
        "custom.jwt.access-expmin=2",
        "custom.jwt.refresh-expmin=4"
})
@Import({ JWTUtil.class })
@ActiveProfiles("test")
@Slf4j
public class JwtTokenTest {
    @Autowired
    JWTUtil util;

    @Test
    void 토큰이_생성되고_저장한_Claim들이_잘_조회되는가(@Value("${custom.jwt.access-expmin}") int expTime) {
        String userId = "member@quietjun.com";
        String token = util.create(userId, "auth", expTime);
        assertNotNull(token);
        Map<String, Object> claims = util.checkAndGetClaims(token);
        Assertions.assertEquals(claims.get("user"), userId);
        log.debug("token: {}", token);
    }
}


만약 형식은 적합하지만 유효기간이 지난 토큰을 사용하면 ExpiredJwtException이 발생한다. 이는 나중에 Refresh Token을 생성하는 근거가 된다.

@Test
void 유효기간이_지난_토큰을_사용하는_경우(@Value("${custom.jwt.access-expmin}") int expTime) {
  String userId = "member@quietjun.com";
  String token = util.createAuthToken(userId, -expTime - 1);
  Assertions.assertThrows(ExpiredJwtException.class, () -> util.checkAndGetClaims(token));
}

 

만약 잘못 형식이 잘못된 토큰을 이용하려고 하면 MalformedJwtException이 발생하고 훼손된 토큰을 사용하려는 경우 SignatureException이 발생한다.

@Test
void 잘못된_토큰을_사용하려는경우_예외발생() {
  String wrongFormatToken = "잘못된토큰";
  Assertions.assertThrows(MalformedJwtException.class, ()->{
    util.checkAndGetClaims(wrongFormatToken);
  });
  
  String invalidToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoibWVtYmVyQHF1aWV0anVuLmNvbSIsInN1YiI6ImF1dGhUb2tlbiIsImV4cCI6MTczMjUyMDgwMH0.As_DN2K_OrVUJjaMQpIM1bq-sdfQqpIgXR_modified";
  Assertions.assertThrows(SignatureException.class, ()->{
    util.checkAndGetClaims(invalidToken);
  });
}

 

jwt.io

JWT 토큰의 내용을 쉽게 확인해보기 위해서 jwt.io 사이트를 이용할 수도 있다. jwt.io의 디버거 탭(https://jwt.io/#debugger-io)에 출력된 토큰을 넣어보면 토큰을 생성할 때 넣었던 정보들을 확인할 수 있다.

https://jwt.io를 통한 토큰 검사

이때 header(빨강)와 payload(보라) 부분은 단순히 encoding 된 내용이어서 decoding을 통해서 바로 확인 가능하지만 signature(파랑) 부분은 서버의 secret 정보가 없으므로 확인할 수 없다.

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.