interceptor를 사용하여 game api 자체에서 로그인을 체크하고 로그인 토큰의 유효기간이 종료되었다면 다시 재발급하여 진행할 수 있도록 처리해보려고 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) //interceptor 등록
.order(1) //우선도
.addPathPatterns("/**"); //사용될 url
// .excludePathPatterns() //제외될 url
}
}
우선 로그를 찍어보기 위해 LogInterceptor를 추가해보았다.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
//controller 실행 전
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uuid = UUID.randomUUID().toString();
log.info("pre log = {}", uuid);
HttpSession session = request.getSession();
session.setAttribute(LOG_ID, uuid); //session에 로그에 대한 랜덤 아이디 값을 저장해둠.
return true; //return이 false일 경우 다음 실행을 하지 않음, true여야 다음 interceptor or controller가 실행됨
}
//controller 실행 후 (예외 발생 시 실행되지 않음)
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HttpSession session = request.getSession(false);//session이 없다면 만들지 않음
String logId = (String)session.getAttribute(LOG_ID);
log.info("post log = {}", logId);
}
//요청이 완전히 종료된 후 (예외가 발생해도 실행 됨, 예외가 발생하면 ex로 예외를 처리해줄 수 있음)
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HttpSession session = request.getSession(false);//session이 없다면 만들지 않음
String logId = (String)session.getAttribute(LOG_ID);
log.info("after log = {}", logId);
}
}
LogInterceptor는 다음과 같이 session의 uuid로 랜덤한 값을 넣어 다음 실행부분에서 같은 값을 출력하는지에 대해 확인해 보려고 한다.
session은 web에서 한 요청에 대해 정보를 일정하게 가지고 넘어간다는 확신이 있지만 또한 session은 사용자수 = session 수 이므로 무분별하게 session의 많은 정보를 담으면 성능에 문제가 발생할 수 있으니 정말 필요한 데이터만 담도록 하자!
실행을 해보면 실제로 pre가 실행되고 controller 내 debug가 실행되었으며 이후 post와 after가 순서적으로 실행되는 것을 확인할 수 있었다.
HTTP 요청 -> WAS -> filter -> servlet -> interceptor -> controller
순서의 실행을 꼭 기억하자.
이제 로그인 토큰 처리를 이 안으로 넣어보자.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final WebClientConfig webClientConfig;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//log interceptor
registry.addInterceptor(new LogInterceptor()) //interceptor 등록
.order(1) //우선도
.addPathPatterns("/**") //사용될 url
.excludePathPatterns("/error")
;
//login token check interceptor
registry.addInterceptor(new TokenInterceptor(webClientConfig))
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/api/member/login", "/api/member/join", "/error")
;
}
}
LogInterceptor에 이어서 TokenInterceptor를 등록한다. 하지만 TokenInterceptor에서는 WebClinet를 사용해야 하므로 생성자에 주입해준다.
@Slf4j
@RequiredArgsConstructor
public class TokenInterceptor implements HandlerInterceptor {
private final WebClientConfig webClient;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Token Interceptor 실행");
//session 가져오기
HttpSession session = request.getSession(false);
//cookie 가져오기
Cookie[] cookies = request.getCookies();
String accessTokenId = "";
String refreshTokenId = "";
String accessToken = "";
String refreshToken = "";
ObjectMapper mapper = new ObjectMapper();
try {
log.info("access token 확인");
accessTokenId = findCookie(cookies, "access_token_id_cookie").getValue();
accessToken = session.getAttribute(accessTokenId).toString();
log.info("access token = {}", accessToken);
}catch (NullPointerException ne){
try{
log.info("access token 만료로 인한 refresh token 확인");
refreshTokenId = findCookie(cookies, "refresh_token_id_cookie").getValue();
refreshToken = session.getAttribute(refreshTokenId).toString();
log.info("refresh token = {}", refreshToken);
//refresh token 존재한다면 다시 access token 발급
ResponseEntity<String> result = webClient.webClient().post() //get 요청
.uri("/login-service/game/token/refresh") //요청 uri
.header("Authorization", String.format("Bearer %s", refreshToken))
.retrieve()//결과 값 반환
.toEntity(String.class)
.block();
//token 파싱
String body = result.getBody();
JwtToken jwtToken = mapper.readValue(body, JwtToken.class);
accessToken = jwtToken.getAccess_token();
accessTokenId = UUID.randomUUID().toString();
session.setAttribute(accessTokenId, accessToken); //session에 access_token 값을 저장
//쿠키도 새로 생성
Cookie accessTokenCookie = new Cookie("access_token_id_cookie", accessTokenId);
accessTokenCookie.setMaxAge(30*60); //단위는 초 , 30분으로 지정
accessTokenCookie.setPath("/");
accessTokenCookie.setHttpOnly(true); //http를 통해서만 쿠키가 보내짐
response.addCookie(accessTokenCookie);
}catch (NullPointerException ne2){
log.info("token 만료");
//token 없음으로 재 로그인 필요
Map<String, Object> map = new HashMap<>();
map.put("code", 401);
map.put("msg", "로그인 후 다시 시도해주세요.");
throw new ClientError(HttpStatus.UNAUTHORIZED, mapper.writeValueAsString(map));
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("Token Interceptor 종료");
}
//cookie 찾기
private Cookie findCookie(Cookie[] cookies, String findKey){
return Arrays.stream(cookies).filter(cookie -> cookie.getName().equals(findKey))
.findAny()
.orElse(null);
}
}
동일하게 Interceptor를 구현하며 access_token을 우선 있지는 체크한다.
access_token은 login-service에서 발급된 token의 유효기간인 30분만큼 cookie도 설정되어 있어서 cookie에 값이 없다면 null로 넘어온다.
access_token이 존재하지 않다면 refresh_token을 header에 넣고 재발급을 요청하며 session과 cookie에 각각 저장한다.
그럼 이제 테스트 해보자!!!!
//token check
@GetMapping("/tokenCheck")
public ResponseEntity<String> tokenCheck() throws JsonProcessingException {
return ResponseEntity.ok().build();
}
token 체크라는 controller를 생성하여 해당 접근시 token을 체크하도록 했다.
token의 데이터가 하나도 없는 경우
로그인 필요하다는 알림과 함께
로그인 창으로 리디렉션하도록 코드를 짜놨다.
import axios from '../axios/jayeon-axios'
import router from '@/router';
export default {
methods:{
//회원가입
test(){
axios.get('/member/tokenCheck')
.then(res => {
console.log(res);
if(res.status == '200'){
alert('정상실행');
}
})
.catch((err) => {
if(err.response.status == 401){
alert('로그인 후 다시 시도해 주세요.');
router.push({name:'login'});
}
console.log(err);
})
}
}
}
vue의 경우 해당 내용으로 짰기 때문에 다음과 같이 실행되었다.
정상 로그인 후 요청하게 된다면
다음과 같이 정상실행이 실행된다.
마지막으로 access_token이 만료되었을 경우 정상적으로 재발급 후 요청하는지 확인해보자.
이전의 요청에서 access_token의 쿠키를 삭제한 뒤 요청해보자.
access_token을 새로 발급한 뒤
이전의 요청과 같이 정상 실행하는 것을 확인할 수 있다.
이번 로그인 처리를 통해 filter와 interceptor의 개념에 대해 더 확실히 하고 갈 수 있었다. AOP 또한 다른 개념인데 이후에 AOP를 좀 더 확실히 학습하여 이 3개의 개념을 헷갈리지 않도록 해야겠다.