Spring Security - Refresh Token 을 이용한 JWT 재발급 (reissue)

이유석·2024년 11월 22일
1

Spring-Security

목록 보기
9/10
post-thumbnail

Refresh Token 을 이용한 Access Token 재발급

  • JWT는 유효기간이 지나 무효화되기 전까지, JWT 내부 정보를 이용할 수 있습니다.
    → JWT 는 탈취가 쉬움 → 탈취당하게 되면, 보안적 피해가 발생합니다.

  • 이에 대한 해결책은, Access Token 유효기간을 짧게 한다 입니다.

  • Access Token의 유효기간을 짧게 설정 후,
    Refresh Token을 활용하여 Access Token 을 갱신하게 합니다.
    그렇게 되면 Access Token 을 탈취당해도 상대적으로 피해를 줄일 수 있습니다.

  • Refresh Token의 통신 빈도가 적기는 하지만 탈취 위험에서 완전히 벗어난 것은 아니다.

    • 해결 방법 : Refresh Token Rotation
      Refresh Token Rotation은 클라이언트가 Access Token를 재요청할 때마다 Refresh Token도 새로 발급받는 것이다.
    • 이렇게 되면 탈취자가 가지고 있는 Refresh Token은 더이상 만료 기간이 긴 토큰이 아니게 된다. 따라서 불법적인 사용의 위험은 줄어든다.

JWT 재발급 처리 순서

  1. 클라이언트에서 /user/re-issue(서버 Access Token 재발급 API)으로 Http Cookie 에 RefreshToken 을 넣어서 요청한다. (key : refreshToken, value : RefreshToken 값)
  2. 요청이 들어오면 JwtAuthFilter 를 거친다.
  3. Access Token 재발급 요청에 대해서는 인증이 필요한 요청이 아니므로 토큰 정보가 필요하지 않다. → JWT 토큰 검증 Pass
    • 설정 파일을 통해 permitAll() 로 설정하였음
    • RefreshToken 에 대한 검증은 서비스 단에서 수행함
  4. AccessToken 재발급 API 에서 재발급 비즈니스 로직이 수행된다.
    • RefreshToken 검증
    • 최초 로그인한 IP 와 재발급을 요청한 IP의 일치 확인
    • JWT (AccessToken 및 RefreshToken) 재발급 및 반환

JWT 재발급 구현

JWT 재발급 API

@RequiredArgsConstructor
@RequestMapping("/user")
@RestController
public class UserAuthAPI {

    private final UserAuthService userAuthService;

    @PostMapping("/re-issue")
    public ResponseEntity<?> reissue(HttpServletRequest httpServletRequest, @CookieValue(value = "refreshToken") String refreshTokenReq) {
        return ResponseEntity.ok(userAuthService.reissue(httpServletRequest, refreshTokenReq));
    }
}

UserAuthService JWT 재발급

@RequiredArgsConstructor
@Service
public class UserAuthService {

		private final JwtProvider jwtProvider;
    
    private final RefreshTokenRedisRepository refreshTokenRedisRepository;

		... 
		
    public TokenInfoResponse reissue(HttpServletRequest httpServletRequest, String refreshTokenReq) {
        // 1. 쿠키에서 받아온 refresh token 검증
        if (StringUtils.hasText(refreshTokenReq) && jwtProvider.validateToken(refreshTokenReq)) {
						// 2. refresh token 타입 검증
            jwtProvider.validateTokenType(refreshTokenReq, JwtProvider.TYPE_REFRESH);
            
            // 3. redis에 refreshToken 존재 여부 확인
            Optional<RefreshToken> optionalRefreshToken = refreshTokenRedisRepository.findByRefreshToken(refreshTokenReq);
            if (optionalRefreshToken.isPresent()) {
                RefreshToken refreshToken = optionalRefreshToken.get();
                // 4. 최초 로그인한 ip 와 같은지 확인 (처리 방식에 따라 재발급을 하지 않거나 메일 등의 알림을 주는 방법이 있음)
                String currentIpAddress = NetworkUtil.getClientIp(httpServletRequest);
                if (refreshToken.getIp().equals(currentIpAddress)) {
                    // 5. Redis 에 저장된 RefreshToken 정보를 기반으로 JWT 생성
                    TokenInfoResponse response = jwtProvider.generateToken(refreshToken.getId(), refreshToken.getAuthorityList());

                    // 6. Redis RefreshToken update
                    refreshTokenRedisRepository.save(RefreshToken.builder()
                            .id(refreshToken.getId())
                            .ip(currentIpAddress)
                            .authorityList(response.authorityList())
                            .refreshToken(response.refreshToken())
                            .build());

                    return response;
                }
            }
        }

        throw new CustomCommonException(UserErrorCode.INVALID_REFRESH_TOKEN);
    }
}
profile
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.

0개의 댓글