본문 바로가기
공부/프로그래밍

[springboot, jwt] jwt 로 토큰 생성, 유효시간 관리 하기

by demonic_ 2021. 4. 5.
반응형

이번에는 jwt를 이용한 로그인 인증을 만들려 한다. 일전에 @EnableAuthorizationserver deprecated 되면서 찾던 중 jwt가 있어 이걸 활용하기로 했다(DB로 토큰유효성 확인도 안해서 더 나은거 같기도 하고...)

 

암튼 시작.

 

JwtManager 를 만들어서 JWT를 통해 토큰을 생성, 토큰 검증 하는 클래스를 만들 것이다. 그전에 다음 dependency를 먼저 추가하자.(gradle 기준)

...
    implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
...

Jwt를 관리할 매니저를 만들것이다. 의존성을 없이 기능만 하는 클래스를 만들어 어디서든 활용할 수 있도록 할 것이다.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.security.Key;
import java.util.Date;

public class JwtManager {
  // 30분
  private long ACCESS_TOKEN_VALIDATiON_SECOND = 60 * 30;

  // 1개월
  private long REFRESH_TOKEN_VALIDATiON_SECOND = 60 * 60 * 24 * 30;

  private String secretKey;

  public JwtManager(String secretKey) {
    this.secretKey = secretKey;
  }
  public JwtManager(
      String secretKey
      , Long accessTokenValidationSecond
      , Long refreshTokenValidationSecond
  ) {
    this.secretKey = secretKey;
    this.ACCESS_TOKEN_VALIDATiON_SECOND = accessTokenValidationSecond;
    this.REFRESH_TOKEN_VALIDATiON_SECOND = refreshTokenValidationSecond;
  }

  private Key getSigninKey(String secretKey) {
    byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
    return Keys.hmacShaKeyFor(keyBytes);
  }

  // 토큰 해석
  public Claims validTokenAndReturnBody(String token) {
    try {
      return Jwts.parserBuilder()
          .setSigningKey(getSigninKey(secretKey))
          .build()
          .parseClaimsJws(token)
          .getBody();
    } catch(ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
      e.printStackTrace();
      throw new InvalidParameterException("유효하지 않은 토큰입니다");
    }
  }

  // 유저id 조회
  public String getName(String token) {
    return validTokenAndReturnBody(token).get("username", String.class);
  }

  // 토큰 만료
  private Boolean isTokenExpired(String token){
    Date expiration = validTokenAndReturnBody(token).getExpiration();
    return expiration.before(new Date());
  }

  // access token 생성
  public String generateAccessToken(String username) {
    return doGenerateToken(username, ACCESS_TOKEN_VALIDATiON_SECOND * 1000l);
  }

  // refresh token 생성
  public String generateRefreshToken(String username) {
    return doGenerateToken(username, REFRESH_TOKEN_VALIDATiON_SECOND * 1000l);
  }

  // accessToken 유효시간 알림(second)
  public Long getValidationAccessTokenTime(){
        return ACCESS_TOKEN_VALIDATiON_SECOND;
  }

  private String doGenerateToken(String username, Long expireTime) {
    Claims claims = Jwts.claims();
    claims.put("username", username);

    return Jwts.builder()
        .setClaims(claims)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + expireTime))
        .signWith(getSigninKey(secretKey), SignatureAlgorithm.HS256)
        .compact();
  }
}

 

생성자를 보면 2개가 있는데 하나는 secretKey만 받는걸로, 하나는 accessToken과 refreshToken의 유효시간(초단위)을 조절하는 것이다. 보통 accessToken 30분이나 1시간 정도로 설정해두면 된다.

 

그럼 jwtManager를 테스트해보자. 두 테스트 모두 유효시간을 1초로 두고 테스트했다.

@ExtendWith(SpringExtension.class)
public class TestJwtSample {
  // secret key가 짧으면 에러가 난다
  private final String secretKey = "secretKey-test-authorization-jwt-manage-token";


  @Test
  @DisplayName("토큰 정상 발급")
  void successTest() {
    // given
    JwtManager jwtManager = new JwtManager(secretKey, 1l, 60l);

    // when
    String username = "testuser-1";
    String accessToken = jwtManager.generateAccessToken(username);

    Claims claims = jwtManager.validTokenAndReturnBody(accessToken);
    System.out.println("claims = " + claims);
    String findUsername = claims.get("username", String.class);

    // then
    assertThat(username).isEqualTo(jwtManager.getName(accessToken));
    assertThat(username).isEqualTo(findUsername);
  }


  @Test
  @DisplayName("토큰유효시간 over")
  void expireTokenTest() throws InterruptedException {
    // given
    JwtManager jwtManager = new JwtManager(secretKey, 1l, 60l);
    String username = "testuser-1";
    String accessToken = jwtManager.generateAccessToken(username);

    // 2초 딜레이
    Thread.sleep(2000l);

    // when
    InvalidParameterException ex = assertThrows(
        InvalidParameterException.class
        , () -> jwtManager.validTokenAndReturnBody(accessToken));

    // then
    assertThat(ex.getMessage()).isEqualTo("유효하지 않은 토큰입니다");
  }
}

테스트 통과 된 것을 확인할 수 있다.

생성된 토큰을 해석하면 다음과 같은 값을 가지고 있다

claims = {username=testuser-1, iat=1617413941, exp=1617413942}

 

claims 안에 다른 정보도 추가할 수 있는데 그럴수록 토큰이 길어지니까 최소값만 넣는 것을 추천한다.

 

 

참고로 secretKey 글자가 작으면 다음같은 에러가 발생한다. 그러니 충분한 길이로 만들어주자.

io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.

at io.jsonwebtoken.security.Keys.hmacShaKeyFor(Keys.java:96)
at com.procyan.api_solve_mate.JwtManager.getSigninKey(JwtManager.java:41)
at com.procyan.api_solve_mate.JwtManager.doGenerateToken(JwtManager.java:90)
at com.procyan.api_solve_mate.JwtManager.generateAccessToken(JwtManager.java:70)

 

 

끝.

 

반응형

댓글