JWT는 한번 발급되면 만료 전 까지 삭제할 수 없다.
토큰 재발급의 경우 다양한 방식으로 구현될 수 있으며, 해당 포스팅에서 구현된 방식은 여러 방식 중 하나이다.
access token은 발급 후 서버에 저장되지 않고 해당 토큰으로 사용자 권한을 인증하는 특징이 있다.
만약 access token이 탈취되면 토큰이 만료되기 전까지 해당 토큰을 갖고 있다면 누구나 권한 인증이 가능하다는 문제점이 있어 이를 보완하기 위해 access token의 만료 기간을 짧게 주는 방법이 있다.
하지만 사용자 측면에서는 토큰이 만료될때 마다 다시 로그인을 하여 토큰을 발급 받아야하는 불편함이 발생한다.
이를 해소하기 위해 refresh token을 사용하는데, access token에 비해 보다 더 긴 유효 기간으로 발급되며 권한을 부여하는게 아닌 access token을 재발급 하기 위해서만 사용된다는 특징이 있다.
refresh token에도 문제점은 있다. 바로 무상태라는 특징으로 인해 access token과 마찬가지로 탈취 당할 위험이 있다.
만약 refresh token을 탈취 당했다면 해당 refresh token을 통해 access token을 재발급 받을 수 있게 된다.
하여 최초 로그인 시 refresh token을 DB 또는 Redis에 저장을 하는데 유저의 정보나, 요청이 온 ip와 함께 저장하고 재발급 요청 시 저장된 유저의 정보나 ip를 비교하여 재발급 여부를 결정한다.
- access token 생성
/**
* Access 토큰을 생성하여 반환
* @param authentication
* @return access token
*/
public String generateAccessToken(Authentication authentication) {
// user 구분을 위해 Claims에 User 고유값인 email값을 넣음(UserDetailsServiceImpl 에서 email로 세팅)
Claims claims = Jwts.claims().setSubject(authentication.getName());
log.info("authentication.getName() : {}", authentication.getName());
Date now = new Date();
Date expiresIn = new Date(now.getTime() + access_token_expire_time);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiresIn)
.signWith(SignatureAlgorithm.HS256, access_token_secret_key)
.compact();
}
- refresh token 생성
/**
* Refresh 토큰을 생성하여 반환
* @param authentication
* @return refresh token
*/
public String generateRefreshToken(Authentication authentication) {
Claims claims = Jwts.claims().setSubject(authentication.getName());
Date now = new Date();
Date expiresIn = new Date(now.getTime() + refresh_token_expire_time);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiresIn)
.signWith(SignatureAlgorithm.HS256, refresh_token_secret_key)
.compact();
}
- 로그인 시 토큰 저장
- 사용자의 email 과 password로 인증과정에서 사용하는 UsernamePasswordAuthenticationToken 객체를 생성하여 인증 처리를 한다.
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDto.getEmail(),
loginDto.getPassword()
)
);
String refresh_token = jwtTokenProvider.generateRefreshToken(authentication);
TokenDto tokenDto = new TokenDto(
jwtTokenProvider.generateAccessToken(authentication),
refresh_token
);
// Redis에 저장 - 만료 시간 설정을 통해 자동 삭제 처리
redisTemplate.opsForValue().set(
authentication.getName(),
refresh_token,
refresh_token_expire_time,
TimeUnit.MILLISECONDS
);
// DB에 Refresh토큰 있는지 확인
Optional<RefreshToken> refreshToken = refreshTokenRepository.findByEmail(loginDto.getEmail());
if(refreshToken.isPresent()) {
refreshTokenRepository.save(refreshToken.get().updateToken(refresh_token));
}else {
RefreshToken newToken = new RefreshToken(refresh_token, loginDto.getEmail());
refreshTokenRepository.save(newToken);
}
- 토큰 만료
- 토큰이 만료 되었다면 refresh token으로 access token 재발급 요청
/**
* Refresh 토큰을 검증
* @param token
* @return
*/
public boolean validateRefreshToken(String token) {
try {
Jwts.parser().setSigningKey(refresh_token_secret_key).parseClaimsJws(token);
return true;
} catch (JwtException e) {
// MalformedJwtException | ExpiredJwtException | IllegalArgumentException
throw new CustomException("Error on Refresh Token", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// Refresh Token 검증
if (!jwtTokenProvider.validateRefreshToken(refresh_token)) {
throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
}
// Refresh Token 에서 username 가져온다.
Authentication authentication = jwtTokenProvider.getAuthenticationByRefreshToken(refresh_token);
// Redis에서 저장된 Refresh Token 값을 가져온다.
String redisSaveRefreshToken = redisTemplate.opsForValue().get(authentication.getName());
if (!jwtTokenProvider.validateRefreshToken(redisSaveRefreshToken)) {
throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
}
// 토큰 재발행
String new_refresh_token = jwtTokenProvider.generateRefreshToken(authentication);
TokenDto tokenDto = new TokenDto(
jwtTokenProvider.generateAccessToken(authentication),
new_refresh_token
);
// RefreshToken Redis에 업데이트
redisTemplate.opsForValue().set(
authentication.getName(),
new_refresh_token,
refresh_token_expire_time,
TimeUnit.MILLISECONDS
);
// DB에 저장된 Refresh Token 값을 가져온다.
RefreshToken findRefreshToken = refreshTokenRepository.findByEmail(authentication.getName())
.orElseThrow(() -> new IllegalArgumentException("해당 RefreshToken은 없습니다."));
System.out.println(findRefreshToken.getId());
if (!jwtTokenProvider.validateRefreshToken(findRefreshToken.getRefreshToken())) {
throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
}
refreshTokenRepository.save(findRefreshToken.updateToken(new_refresh_token));