이번 포스팅은 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
는 토큰 관련 기능을 제공하는 클래스이다.
주요 기능으로 다음과 같은 것이 있다.
토큰의 비밀키 값, 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);
}
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);
}
토큰 검증 메서드이다.
토큰의 복호화 단계시 발생하는 예외들에 대한 처리를 해준다.
// 토큰 검증
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());
}
}
}
토큰의 복호화 메서드이다.
토큰 검증 이후에 access 토큰으로부터 subject 값을 가져오기 위해 사용된다.
(토큰 검증 이후에 사용할 것이므로 유효기간 만료 익셉션은 무시한다.)
// 토큰 복호화
public Claims parseClaims(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSecretKey)
.parseClaimsJws(token)
.getBody();
// 토큰 만료시에도 클레임 반환
} catch (ExpiredJwtException e){
return e.getClaims();
}
}
인증, 인가 단계에서 사용할 메서드들을 UserDetailsService
을 통해 구현해보았다.
주입받은 사용자 정보(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));
}
토큰의 재발급 메서드이다.
이전 포스팅에 따라, 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의 인증, 인가 로직을 구현해보겠다.