JWT 세팅하기(TokenProvider, UserDetailsService)

Nicky·2024년 3월 15일
0
post-thumbnail

이번 포스팅은 JWT 토큰 관련 클래스에 대해 다루겠다.

의존성 추가

우선 다음의 의존성을 추가하자.

	// JWT
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

TokenProvider

TokenProvider는 토큰 관련 기능을 제공하는 클래스이다.
주요 기능으로 다음과 같은 것이 있다.

  • 토큰 생성: 사용자 인증 정보를 바탕으로 토큰 생성.
  • 토큰 검증: 서버로 전송된 토큰의 유효성 검증
  • 토큰 파싱: 토큰에 포함된 사용자 정보(권한 등) 추출

생성자

토큰의 비밀키 값, access & refresh 토큰의 유효기간을 주입받는 생성자를 준비하자.

주입받은 비밀 키로부터 안전한 HMAC SHA 키를 생성한다.

@Component
public class TokenProvider {

    private final Key jwtSecretKey;
    private final Long accessTokenExpiration;
    private final Long refreshTokenExpiration;
    
    public TokenProvider(@Value("${jwt.secret}") String secretKey,
                         @Value("${jwt.access.expiration}") String accessTokenExpiration,
                         @Value("${jwt.refresh.expiration}") String refreshTokenExpiration) {
        this.jwtSecretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
        this.accessTokenExpiration = Long.valueOf(accessTokenExpiration);
        this.refreshTokenExpiration = Long.valueOf(refreshTokenExpiration);
    }

generateToken

JWT 토큰 발급을 하는 메서드이다.

Access 토큰의 Subject에는 사용자의 식별을 위한 유일 값(예시는 이름 사용)을 담아, 이후 인가 과정에서 사용자 정보를 찾는데 사용한다.

Refresh 토큰의 경우 오직 access 토큰의 재발급을 위해 존재하기 때문에 별도로 사용자 정보를 담지 않는다!

    // 토큰 생성
    public TokenData generateToken(UserDetails userDetails) {
        long now = (new Date()).getTime();

        // access 토큰 생성
        String accessToken = Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setExpiration(new Date(now + accessTokenExpiration))
                .signWith(jwtSecretKey, SignatureAlgorithm.HS256)
                .compact();

        // refresh 토큰 생성
        String refreshToken = Jwts.builder()
                .setExpiration(new Date(now + refreshTokenExpiration))
                .signWith(jwtSecretKey, SignatureAlgorithm.HS256)
                .compact();
        return new TokenData(accessToken, refreshToken);
    }

validateToken

토큰 검증 메서드이다.

토큰의 복호화 단계시 발생하는 예외들에 대한 처리를 해준다.

    // 토큰 검증
    public void validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(jwtSecretKey).build().parseClaimsJws(token);
        } catch (MalformedJwtException e) {
            throw new MalformedJwtException(MALFORMED_SIGNATURE.getMessage());
        } catch (ExpiredJwtException e) {
            throw new SecurityException(EXPIRED_ACCESS_TOKEN.getMessage());
        } catch (UnsupportedJwtException e) {
            throw new UnsupportedJwtException(UNSUPPORTED_TOKEN.getMessage());
        } catch (SignatureException e) {
            throw new SignatureException(UNMATCHED_SIGNATURE.getMessage());
        }
    }
}

parseClaims

토큰의 복호화 메서드이다.

토큰 검증 이후에 access 토큰으로부터 subject 값을 가져오기 위해 사용된다.
(토큰 검증 이후에 사용할 것이므로 유효기간 만료 익셉션은 무시한다.)

    // 토큰 복호화
    public Claims parseClaims(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(jwtSecretKey)
                    .parseClaimsJws(token)
                    .getBody();
        // 토큰 만료시에도 클레임 반환
        } catch (ExpiredJwtException e){
            return e.getClaims();
        }
    }

UserDetailsService

인증, 인가 단계에서 사용할 메서드들을 UserDetailsService을 통해 구현해보았다.

getAccessTokenResponse

주입받은 사용자 정보(UserDetails)로 토큰을 발급하고,
Refresh 토큰은 Redis에, Access 토큰은 클라이언트으로의 응답을 위해 DTO로 보낸다.

    // Access 토큰 DTO 반환
    public AccessTokenResponse getAccessTokenResponse(CustomUserDetails userDetails) {
        // 토큰 생성
        TokenData tokenData = tokenProvider.generateToken(userDetails);
        setRefreshTokenInRedis(userDetails.getUsername(), tokenData);
        return new AccessTokenResponse(tokenData.accessToken());
    }

refresh 토큰 저장은 사용자 식별을 위한 값을 key 값으로, refresh 토큰 문자열을 value값으로 저장하고 유효기간을 정해준다.

    // redis에 refresh 토큰 저장
    @Transactional
    private void setRefreshTokenInRedis(String username, TokenData tokenData){
        String refreshToken = tokenData.refreshToken();
        redisService.setRefreshToken(username, refreshToken, Long.valueOf(refreshTokenExpiration));
    }

refreshAccessToken

토큰의 재발급 메서드이다.
이전 포스팅에 따라, redis에서 refresh 토큰이 조회되면 토큰 재발급을 해주고,
만약 refresh 토큰 또한 만료되면 익셉션을 던져 인증(로그인) 재요청을 보낼 것이다.

    public AccessTokenResponse refreshAccessToken(String accessToken) {
        // redis에서 refresh 토큰 조회
        String userName = tokenProvider.parseClaims(accessToken).getSubject();
        String refreshToken = redisService.getRefreshToken(userName);
        // 조회 실패시 예외 처리
        if (refreshToken == null) {
            throw new SecurityException(EXPIRED_REFRESH_TOKEN.getMessage());
        }
        // 조회 성공시 토큰 재발급
        CustomUserDetails userDetails = loadUserByUsername(userName);
        return getAccessTokenResponse(userDetails);
    }

다음 포스팅부터 본격적으로 Spring Security의 인증, 인가 로직을 구현해보겠다.

profile
코딩 연구소

0개의 댓글

관련 채용 정보