Spring Boot + Interceptor 구현

형석이의 성장일기·2023년 10월 9일
0

저번에 'Spring Boot + 카카오 로그인'을 구현하였는데, 이번엔 거기서 생긴 AccessToken을 관리하는 JwtInterceptor를 구현해보자

쉽게 말해서 Interceptor는 프론트엔드에서 오는 모든 요청에 대해 검증을 거치는 기능을 가지고 있다!

디렉토리 구조는 이렇다.

그리고 사실 이 인터셉터를 직접 구현하는 이유는 Spring Security를 써보려면 Filter와 Interceptor를 직접 구현해보지도 않고 바로 쓰는건 약간 위험해보인달까..? 나중에 면접에 왜 Spring Security를 썼냐고 물어보면 Filter와 Interceptor를 직접 구현해봤다고 하고 이어서 말을 한다면 더 설득력이 있을 것 같다.

전체적인 흐름은 이렇다.

mt/util/WebMvcConfig.java

package com.project.mt.util;

import com.project.mt.authentication.interceptor.JwtInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final JwtInterceptor jwtTokenInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        System.out.println("인터셉터 등록");
        registry.addInterceptor(jwtTokenInterceptor).addPathPatterns("/**").excludePathPatterns("/api/auth/kakao");
    }
}

여기서

registry.addInterceptor(jwtTokenInterceptor).addPathPatterns("/**").excludePathPatterns("/api/auth/kakao")

이 부분의 의미는 인터셉터를 등록하고, 모든 요청에 대해 인터셉터를 거치는데 /api/auth/kakao 가 붙은 요청만 제외한다는 의미!

로그인을 하거나 회원가입할 때는 당연히 유저가 AccessToken을 가지고있지 않겠죠..? 그러니까 인터셉터를 거치면 안된다는 뜻입니다.


mt/authentication/interceptor/JwtInterceptor.java

package com.project.mt.authentication.interceptor;

import com.project.mt.authentication.domain.AuthTokensGenerator;
import com.project.mt.authentication.infra.JwtTokenProvider;
import com.project.mt.exception.ErrorCode;
import com.project.mt.exception.RestApiException;
import com.project.mt.member.domain.Member;
import com.project.mt.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RequiredArgsConstructor
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;
    private final MemberRepository memberRepository;
    private final AuthTokensGenerator authTokensGenerator;

    /**
     * Http 요청이 들어온 경우, 가장 처음 만나는 메서드
     * 여기서 AccessToken 이 유효한지, 유효하지 않다면 RefreshToken 은 유효한지, 검증해야 함
     *
     * 1) AccessToken 이 유효한 경우 : 그냥 return true
     * 2) AccessToken 이 유효하지 않아서 RefreshToken 이 유효한지 검증했는데 유효한 경우 : 다시 AccessToken 발급해주고 그거 리턴
     * 3) AccessToken, RefreshToken 둘 다 유효하지 않은 경우
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception, RestApiException {
        // React와의 연동으로 인한 CORS 정책 판단 조건
        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            return true;
        }

        // 인가 요청 여부확인
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

        // JWT 여부 확인
        String accessToken = "";
        try {
            accessToken = authorization.replaceAll("Bearer ", "");
        } catch (NullPointerException e) {
            throw new RestApiException(ErrorCode.UNAUTHORIZED_REQUEST);
        }

        Long memberIdx = authTokensGenerator.extractMemberId(accessToken);
        Member member = memberRepository.findMemberByMemberIdx(memberIdx).orElseThrow(() -> new RestApiException(ErrorCode.MEMBER_NOT_FOUND));

        // AccessToken 이 유효한 경우
        if (jwtTokenProvider.vaildAccessToken(accessToken)) { // AccessToken 이 유효한 경우
            if (!jwtTokenProvider.vaildRefreshToken(member.getRefreshToken())) {// RefreshToken 은 만료된 경우
                String newRefreshToken = authTokensGenerator.generateRefreshToken(memberIdx.toString()); // RefreshToken 재발급
                memberRepository.saveRefreshToken(newRefreshToken, memberIdx);
            }
            return true;
        } else {
            if (jwtTokenProvider.vaildRefreshToken(member.getRefreshToken())) { // RefreshToken 이 유효한지 검증했는데 유효한 경우
                String newAccessToken = authTokensGenerator.generateAccessToken(memberIdx.toString()); // AccessToken 재발급
                System.out.println("RefreshToken은 만료되지 않았으므로 새로운 AccessToken을 발급함 " + newAccessToken);
                response.setHeader("newAccessToken", newAccessToken);
                return true;
            }
        }

        // AccessToken, RefreshToken 둘 다 유효하지 않은 경우 -> 재 로그인 필요
        throw new RestApiException(ErrorCode.VALID_TOKEN_EXPIRED);
    }
}
profile
이사중 .. -> https://gudtjr2949.tistory.com/

0개의 댓글