api server login 구현 [3] interceptor를 사용하여 로그인 체크

최준호·2022년 4월 30일
0

game

목록 보기
8/14
post-thumbnail

interceptor를 사용하여 game api 자체에서 로그인을 체크하고 로그인 토큰의 유효기간이 종료되었다면 다시 재발급하여 진행할 수 있도록 처리해보려고 한다.

🔨interceptor 사용 설정

@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

순서의 실행을 꼭 기억하자.

이제 로그인 토큰 처리를 이 안으로 넣어보자.

🔨login interceptor 생성

@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 모두 만료

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의 쿠키를 삭제한 뒤 요청해보자.

access_token을 새로 발급한 뒤

이전의 요청과 같이 정상 실행하는 것을 확인할 수 있다.

이번 로그인 처리를 통해 filter와 interceptor의 개념에 대해 더 확실히 하고 갈 수 있었다. AOP 또한 다른 개념인데 이후에 AOP를 좀 더 확실히 학습하여 이 3개의 개념을 헷갈리지 않도록 해야겠다.

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글