[Spring Security] 6. 토큰 재발급 기능 구현하기

전유림·2024년 3월 13일
0

Spring Security

목록 보기
7/8

엑세스 토큰이 만료되었을 때 리프레시 토큰을 사용하여 재발급하는 작업은 사용자가 로그인할 때와 마찬가지로 인증 프로세스의 일부라고 생각했다. 따라서 사용자 정의 필터를 통해 작업을 처리할 수 있도록 구현하였다.

@RequiredArgsConstructor
public class TokenReissueFilter extends OncePerRequestFilter {

    private final RequestMatcher reissueRequestMatcher;
    private final ObjectMapper objectMapper;
    private final TokenAuthenticationService tokenAuthenticationService;
    private final TokenReissueSuccessHandler successHandler;
    private final TokenReissueFailureHandler failureHandler;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {
        if (!isReissueRequest(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        String refreshToken = getRefreshToken(request);
        if (refreshToken == null) {
            unsuccessfulAuthentication(request, response, NOT_TOKEN);
        }

        try {
            RefreshToken redisRefreshToken = tokenAuthenticationService.validateRefreshToken(refreshToken);
            request.setAttribute(REDIS_REFRESH_TOKEN_ATTRIBUTE, redisRefreshToken);
            successfulAuthentication(request, response);
        } catch (ExpiredJwtException e) {
            unsuccessfulAuthentication(request, response, EXPIRED_TOKEN);
        } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
            unsuccessfulAuthentication(request, response, INVALID_TOKEN);
        }
    }

    private boolean isReissueRequest(HttpServletRequest request) {
        return reissueRequestMatcher.matches(request);
    }

    public String getRefreshToken(HttpServletRequest request) {
        try {
            ReissueRequest reissueRequest = objectMapper.readValue(request.getReader(), ReissueRequest.class);
            return reissueRequest.refreshToken();
        } catch (IOException e) {
            throw new CustomException(INVALID_REQUEST);
        }
    }

    private void successfulAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        successHandler.onAuthenticationSuccess(request, response, null);
    }

    private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            ErrorCode errorCode) throws IOException {
        String errorMessage = errorCode.getMessage();
        AuthenticationException exception = new CustomAuthenticationException(errorMessage);
        failureHandler.onAuthenticationFailure(request, response, exception);
    }
}

해당 필터는 OncePerRequestFilter를 상속하여 각 요청당 한 번씩만 실행되도록 보장한다.

필터는 요청이 재발급 요청인지 확인하고, 리프레시 토큰을 검증하여 새로운 엑세스 토큰을 발급하거나 실패할 경우 적절한 오류 응답을 처리한다.

  1. isReissueRequest 메서드를 호출하여 현재 요청이 재발급 요청인지 확인한다. 만약 재발급 요청이 아니라면, 다음 필터로 요청을 전달한다.
  2. getRefreshToken 메서드를 호출하여 요청에서 리프레시 토큰을 추출한다.
  3. TokenAuthenticationService 클래스의 validateRefreshToken 메서드를 통해 추출한 리프레시 토큰에 대한 유효성을 검사한다.
  4. 검증에 성공하면, 검증된 리프레시 토큰을 요청의 속성으로 설정하고 successfulAuthentication 메서드를 통해 TokenReissueSuccessHandler 클래스의 onAuthenticationSuccess 메서드를 호출하여 성공 처리를 진행한다.
  5. 검증에 실패하면, unsuccessfulAuthentication 메서드를 통해 TokenReissueFailureHandler 클래스의 onAuthenticationFailure 메서드에 ErrorCode를 전달하여 실패 처리를 진행한다.


리프레시 토큰 유효성 검증은 위와 같이 진행하였다.

  1. 요청된 리프레시 토큰에서 UUID 값을 추출하여 레디스에서 해당하는 RefreshToken 객체를 가져온다. 존재하지 않을 경우 예외가 발생한다.
  2. 요청된 리프레시 토큰과 레디스에서 가져온 리프레시 토큰이 일치하지 않을 경우에도 예외가 발생한다.
  3. 검증에 성공했을 경우 RefreshToken 객체를 반환한다.

참고로 RefreshToken은 위와 같은 정보를 담고 있다.
리프레시 토큰이 탈취될 가능성을 고려하여 클레임에 사용자의 개인정보를 담지 않았다. 따라서 액세스 토큰 재발급을 위해 필요한 정보들을 RefreshToken 객체에 담아 레디스에 저장하도록 설계하였다.



리프레시 토큰 검증 성공 후 토큰 재발급 로직은 다음과 같다.

실패 시 예외 처리 로직은 다음과 같다.

@RequiredArgsConstructor
public class TokenReissueConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final ObjectMapper objectMapper;
    private final TokenAuthenticationService tokenAuthenticationService;

    @Override
    public void configure(HttpSecurity builder) {
        TokenReissueFilter tokenReissueFilter = new TokenReissueFilter(
                reissueRequestMatcher(),
                objectMapper,
                tokenAuthenticationService,
                tokenReissueSuccessHandler(),
                tokenReissueFailureHandler()
        );
        builder.addFilterBefore(
                tokenReissueFilter,
                UsernamePasswordAuthenticationFilter.class
        );
    }

    private AntPathRequestMatcher reissueRequestMatcher() {
        return new AntPathRequestMatcher(REISSUE_URL_PATTERN, DEFAULT_HTTP_METHOD);
    }

    public TokenReissueSuccessHandler tokenReissueSuccessHandler() {
        return new TokenReissueSuccessHandler(objectMapper, tokenAuthenticationService);
    }

    public TokenReissueFailureHandler tokenReissueFailureHandler() {
        return new TokenReissueFailureHandler(objectMapper);
    }
}

Spring Security의 SecurityConfigurerAdapter를 확장하여 구현된 TokenReissueConfig 클래스이다. 위와 같이 TokenReissueFilter 필터를 구성하고 UsernamePasswordAuthenticationFilter 필터 앞에 위치 시킨다.

이후 아래와 같이 SecurityConfig 클래스에서 필터 체인에 등록한다.

0개의 댓글