저번에 '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);
}
}