지난번 포스팅에서 JWT 토큰의 유효성 검사 로직을 처리하기 위해서 Filter와 Interceptor 중 어떤 것을 사용해야하는지 알아보았습니다.
https://doreree.tistory.com/25
회원 API에 공통적으로 들어가있는 JWT 토큰의 유효성 검사 로직에 대한 중복 코드 문제를 Spring Interceptor를 이용해서 해결하려고 합니다.
아래는 중복 코드에 대한 예시입니다. 해당 코드는 API의 일부이며 이 외에도 다른 컨트롤러에 수 많은 중복 코드가 존재합니다.
// 미션 리스트
public ResponseEntity<List<UserMissionRes>> userMissionList(HttpServletRequest request){
String accessToken = headers.getFirst("Authorization");
if(accessToken == null){
throw new UnAuthorizedException("토큰 전달 방식에 오류");
}
String refreshToken = cookieHandler.getRefreshToken(request);
try{
authService.parseToken(accessToken);
}catch(Exception e){
accessToken = authService.reissueATK(refreshToken);
}
}
// 도전과제 리스트
public ResponseEntity<List<String>> userAssetList(@RequestHeader HttpHeaders headers, HttpServletRequest request){
String accessToken = headers.getFirst("Authorization");
if(accessToken == null){
throw new UnAuthorizedException("토큰 전달 방식에 오류");
}
String refreshToken = cookieHandler.getRefreshToken(request);
}
[ 인가 과정 설명 ]
1. 회원 인가를 위해 access Token을 HttpHeaders 객체로 header에 접근하여 가져옵니다.
2. accessToken에 대한 NPE 체크를 진행합니다.
3. refreshToken을 HttpOnly와 Secure 설정이 된 Cookie에서 가져오는 Handler를 실행합니다.
4. accessToken 유효성 검사 후 만료시에 refreshToken을 통해 accessToken 재발급을 진행합니다.
RefreshToken은 Cookie로 전달되며 보안을 위해 HttpOnly와 Secure 설정이 된 상태입니다.
HttpHeader에서 가져온 AccessToken의 유효성 검사 후 만료 시 RefreshToken을 통한 재발급 과정을 Interceptor를 통해 Controller에 요청이 전달되기 전에 가로채서 하나의 공통된 로직으로 처리합니다.
1. Spring Interceptor 생성하기
org.springframework.web.servlet의 HandlerInterceptor 이용
- HandlerInterceptor를 구현한 Interceptor 생성
- Spring Bean 주입 (Component Annotation)
@Component
public class JwtInterceptor implements HandlerInterceptor {
private final AuthService authService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("[preHandle]");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.debug("[postHandle]");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("[afterCompletion]");
}
}
2. Interceptor 등록
WebMvcConfig 설정에 addInterceptors 메소드를 구현하여 1에서 생성한 인터셉터를 추가합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/**/mission/list");
}
}
3. Interceptor
[ HttpHeaders → HttpServletRequest 변경 ]
Controller에 요청이 들어가기 전 Interceptor에서 accessToken의 유효성 검사가 진행되고 난 후 Controller에 전달하기 위하여 HttpServletRequest의 setAttribute를 통해 저장시켜놓습니다.
저장시켜놓은 accessToken을 Controller에서 꺼내 사용하기 위하여 HttpServletRequest 객체를 이용하기 때문에 HttpHeaders를 HttpServletRequest로 변경합니다.
// 변경 전
String accessToken = (String) headers.getHeader("Authorization");
// 변경 후
String accessToken = (String) request.getAttribute("Authorization");
[ AccessToken의 NPE ⇒ Interceptor에 추가 ]
// 모든 API에 들어가있는 Null Check
if(accessToken == null){
throw new UnAuthorizedException("토큰 전달 방식에 오류");
}
===============================================================
//변경 후
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("[preHandle]");
// ATK 유효성 검사
String accessToken = request.getHeader("Authorization");
if(accessToken == null){
throw new UnAuthorizedException("토큰 전달 방식에 오류");
}
// ATK 유효성 검사
return true;
}
[ AccessToken의 유효성 검사 ⇒ Interceptor에 추가 ]
// Cookie에서 Get RTK
String refreshToken = CookieHandler.getRefreshToken(request);
try{
authService.parseToken(accessToken);
}catch(Exception e){
accessToken = authService.reissueATK(refreshToken);
}
// 데이터 전달
request.setAttribute("Authorization", accessToken);
[ 최종 코드 ]
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.debug("[preHandle]");
// ATK 유효성 검사
String accessToken = request.getHeader("Authorization");
if(accessToken == null){
throw new UnAuthorizedException("토큰 전달 방식에 오류");
}
try{
authService.parseToken(accessToken);
}catch(Exception e){
// Cookie에서 Get RTK
String refreshToken = CookieHandler.getRefreshToken(request);
accessToken = authService.reissueATK(refreshToken);
}
// 데이터 전달
request.setAttribute("Authorization", accessToken);
return true;
}
마치며
리팩토링을 진행하면서 총 10개가 넘는 API에 대해 Controller와 Service 로직에 들어 있는 공통적인 부분을 Interceptor를 통해 해결할 수 있었습니다.
이 처럼 Spring에서 제공하는 Interceptor를 이용하면 과정 중에 발생하는 예외 처리도 가능하며 RefreshToken의 값도 만료 시 클라이언트에 401에러를 통해 재 로그인하도록 요청에 대한 Error를 반환하면서 그 다음 로직을 수행할 수 있습니다.
'BackEnd' 카테고리의 다른 글
서비스 배포로 확인하는 세션 기반 인증 방식 문제점 (0) | 2023.08.16 |
---|---|
세션 인증 방식과 토큰 인증 방식 (1) | 2023.07.13 |
[Spring] Interceptor 와 Filter의 개념 및 차이점 (0) | 2023.06.20 |
Message Queue (3) | 2023.04.27 |
티켓 예약 서비스의 대규모 트래픽 처리에 대한 고민 (1) | 2023.04.22 |