오늘도 신나게 고생한 이슈들을 정리해 보자.
주제는 jwt 인증 처리와 interceptor이다.
난 분명히 초기에 CORS
설정을 WebMvcConfig에서 했었는데? Interceptor
를 거치기만 하면 에러가 난다.... 왜?
클래스 위에 @CrossOrigin
을 추가하면 해결된다.
@Slf4j
@Component
@CrossOrigin
public class TestInterceptor implements HandlerInterceptor {
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를 한다
}
});
}, []);
문제가 많은 코드다.
그래서 백에서 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
헤더값을 추가해서 public
과 private
을 구분하기로 결정했다.
지금은 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"));
}
좀 더 고민해볼 예정.