Interceptor 를 통해 회원 API 리팩토링하기

지난번 포스팅에서 JWT 토큰의 유효성 검사 로직을 처리하기 위해서 Filter와 Interceptor 중 어떤 것을 사용해야하는지 알아보았습니다.

https://doreree.tistory.com/25

 

[Spring] Interceptor 와 Filter의 개념 및 차이점

JWT를 이용한 회원 인증같은 공통된 로직을 처리하기 위해 Spring에서 어떤 것을 사용해야하는지 의문이 들었습니다. Spring에서는 공통된 작업을 처리하여 중복 코드를 제거할 수 있도록 많은 기능

doreree.tistory.com

 

회원 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 이용

  1. HandlerInterceptor를 구현한 Interceptor 생성
  2. 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를 반환하면서 그 다음 로직을 수행할 수 있습니다.