이슈 모음 (6) - jwt와 interceptor

jinvicky·2024년 1월 18일
0
post-thumbnail

Intro

오늘도 신나게 고생한 이슈들을 정리해 보자.
주제는 jwt 인증 처리와 interceptor이다.

Interceptor CORS

난 분명히 초기에 CORS 설정을 WebMvcConfig에서 했었는데? Interceptor를 거치기만 하면 에러가 난다.... 왜?

@CrossOrigin

클래스 위에 @CrossOrigin을 추가하면 해결된다.

@Slf4j
@Component
@CrossOrigin
public class TestInterceptor implements HandlerInterceptor {

Interceptor에서 request, response 값 넘기기

Interceptor를 통해서 로그인 여부를 체크하고 싶은데
값을 interceptor에서 controller로 넘기거나, controller 결과값에 + postHandle에서 추가한 값을 더하는 방법은 불가능하다는 결론에 도달했다.

결론부터 말하면 RestController를 사용하거나 Controller + ResponseBody 어노테이션을 사용할 때 dto, 엔티티, String, long 등을 반환하게 되면 Interceptor에서 response를 꺼내 수정할 수 없다.

참고 https://medium.com/sjk5766/spring-interceptor에서-response-수정하기-5b6ea3a5a270

로그인 체크 이슈

기존에는 항상 /validate/token 엔드포인트를 만들어서
한번 호출하고 응답받고 다시 데이터를 가져오는 엔드포인트를 호출하는 식이었다.

댓글 리스트 가져오는 것을 예로 들면 아래처럼 짰다.

const fetchReplyList = () => {
    axios.get(HOST_URL + "/reply/list/" + props.idx).then((resp) => {
      if (resp.status === 200 && resp.data) {
        setReplyList(resp.data.data);
        console.log(resp.data.data);
      }
    });
  };

  useEffect(() => {
    AUTH_ITC.get(HOST_URL + "/validate/token").then((resp) => {
      if (resp.data.status === 200 || resp.data.staus === 205) {
        fetchReplyList(); // 200이 와야만 다시 fetch를 한다
      }
    });
  }, []);

문제가 많은 코드다.

  • 매번 status가 200인지 체크를 해야 한다
  • access token을 재발급한 경우 로컬스토리지에 저장하는 코드를 매번 추가해야 한다.
  • 요청을 2번 날리느라 시간이 더 걸린다.
  • 코드가 너무 쓸데없이 거추장스럽다.

그래서 백에서 interceptor를 추가해서 처리하고자 했다.
하지만 여기서 문제가 발생

지금까지 작성한 중구난방 api들을 어떻게 분류에서 interceptor에 적용할 것인가?

@Override
    public void addInterceptors(InterceptorRegistry registry) {

        List<String> URL_PATTERNS = Arrays.asList("/api/**");
        List<String> URL_EXCLUDE_PATTERNS = Arrays.asList("/api/process/kakao", "/css/**", "/images/**", "/js/**");

        registry.addInterceptor(authInterceptor)
                .addPathPatterns(URL_PATTERNS)
                .excludePathPatterns(URL_EXCLUDE_PATTERNS);
    }

URL_PATTERNS를 이용해서 경로를 등록할 때 어떻게 처리해야 할지 막막했고, 그걸 일일이 주석 작성하려니 골치가 아팠다.

그래서 난 front단에서 type 헤더값을 추가해서 publicprivate을 구분하기로 결정했다.
지금은 GET으로만 예를 들어보자.

export const REQUEST_GET = async (url: string, params: {}, callback: Function, type: string, redirect?: boolean) => {
  const resp = await axios
    .create({
      baseURL: HOST_URL,
      headers: {
        Authorization: `Bearer ${getAtk()}`,
        RefreshToken: getRtk(),
        type: type, // "private" 또는 "public"
      },
    })
    .get(url, params);

  if (resp.data.status === 200) {
    if (resp.data) callback(resp.data);
    if (resp.data.newAtk) setAccessToken(`${resp.data.newAtk}`); // atk 재발급
  } else {
    if (redirect)  {
      localStorage.setItem("referer", window.location.pathname);
      alert("로그인이 필요합니다.");
      window.location.href = "/sign/in";
    }
    // 415, 500, 505 ::login required
  }
};

이걸 interceptor에서 받아서 public이라면 인증 상관없이 데이터를 보내준다. (공통 페이지)

 if(!StringUtil.isEmpty(request.getHeader("type")) && request.getHeader("type").equals("public") ) return true;

private 페이지의 경우 ObjectMapper를 사용해서 에러가 발생하면 바로 response를 보내도록 설정했다.

interceptor는 boolean, void 리턴값을 가져서 바로 Map<String, Object>식으로 리턴값을 던질 수 없는 상황에서 고민을 많이 하다 이 방법을 골랐다.
AuthInterceptor.java

@Override
    public boolean preHandle(HttpServletRequest request , HttpServletResponse response, Object handler) throws Exception {
        if(!StringUtil.isEmpty(request.getHeader("type")) && request.getHeader("type").equals("public") ) return true;

        String authHeader = request.getHeader("Authorization");
        String rtkValue = request.getHeader("RefreshToken");

        if (isLoginRequired(authHeader, rtkValue)) { //로그인 필요한 경우
            sendObjMappingData(response, GlobalStatus.LOGIN_REQUIRED);
            return false;
        }

        if(!jwtTokenProvider.validateToken(authHeader.substring(7))) {
            Optional<MemberDto> dto = memberService.getByRtk(rtkValue);

            if (!dto.isPresent()) {
                sendObjMappingData(response, GlobalStatus.NOT_FOUND_USER);
                return false;
            } else { // 액세스 토큰 재발급
                Long memberId = dto.get().getId();
                String newAccessToken = jwtTokensGenerator.generateAtk(memberId);
                request.setAttribute("newAtk", newAccessToken);
                return true;
            }
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

이렇게 하면 호출이 간결해진다. 2번씩 요청을 보내지 않아도 된다.

REQUEST_GET("/cms/list", {}, (data) => setData(data.data), "public", false);

미해결

access token이 재발급되었을 때 front에 전달하는 방법을 interceptor 내부에서 해결하지 못해서 controller단에서 map에 신규 access token을 담는 코드를 추가해야 한다.

if(!StringUtil.isEmpty((String) request.getAttribute("newAtk"))) {
                map.put("newAtk", (String) request.getAttribute("newAtk"));
            }

좀 더 고민해볼 예정.

Git

https://github.com/CM-WORLD

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글