[Spring] 김밥천국 배달앱 프로젝트 -3

JunWoo An·2023년 12월 7일
0

스파르타코딩클럽

목록 보기
32/46

지난 게시글의 ATK와 RTK구현에 이어 계속해서 jwt인증,인가 서비스개발을 지속하였다.

ATK와 RTK의 생성과 재발급을 구현해서 로그인기능을 구현을 했다면 다음은?

바로 로그아웃 이다.

statless의 특성을 지닌 JWT의 특성상 이미 발급한 토큰을 서버에서 직접 무효화 할수있는 방법은 없다. 그렇기 때문에 맨처음 저장되있는 RTK삭제와 잔존하는 ATK를 블랙리스트 저장소에 등록하여 무효화하는 방법을 구상했었다. 하지만 이과정에서 이미 RTK의 저장소가 존재하는데 ATK를 위한 저장소를 하나더 만들거나 RTK저장소의 엔티티의 필드를 구분하여 두 토큰을 전부 저장하는 방식등 여러방법을 고안했고 해당 방법에 대해 튜터님에게 자문을 구해 어떤방법이 좋을지에 대해 여쭤보았다.

하지만 놀랍게도 튜터님의 말씀은 두개의 저장소나 한개의 저장소도 아니였다. RTK삭제후 해당 토큰으로 발급된 ATK는 백엔드가 상관하지 않아도 된다는 말씀이였다. 구체적으로는 해당 ATK는 프론트에서 충분히 관리가 가능하기 때문에 굳이 백엔드에서 리소스를 낭비하면서 관리 할 필요가없다는것이다. 백엔드에서 최대한 모든걸 처리해야한다고 생각하고있던 나는 내 생각에 대해 다시 한번 생각해 볼 수 있는 기회가 되었다.

다음으로는 로그인,로그아웃 및 인증 시 예외처리에 대해 작성하였다.JWT 자체만으로도 Spring에서 제공하는 관련 Exception이 많은 만큼 발생할수있는 예외에 대해선 최대한 작성하기 위해 노력했다. 우선 컨트롤러와 서비스단에서 사용하는 Spring AOP를 사용한 Global Exception을 인증,인가 필터에서도 사용할려고 하였지만 해당 예외처리가 실행이 안되는 오류가 발생하였다.이에 대해 조사해 보니

Global Exception은 디스패처서블릿을 사용하여 예외처리를 실시하는데 필터단은 요청을 가장 먼저 받는 부분으로서 디스패처서블릿이 생성되기이전에 이미 로직이 실행이된다는것을 알게되었다. 그렇기 때문에 필터단은 직접 예외문을 만들어줘야 하기 때문에 예외처리문을 작성하였다.

예외문을 작성도중 아래와 같이 작성하였는데 해당 부분에서 특정상황에서 에러가 발생함을 발견했다.

인가필터

로그인시도 Postman


로그인시도때는 ATK없이 로그인을 한 후 ATK와RTK를 반환받는게 논리적으로 맞지만 해당 로직에서는 발생하면안될 예외가 발생하여 예외를 반환한 모습이다. 이것에 대해 튜터님에게 자문을 구해보았는데 내가 생각하기로는 Spring Security의 WebConfig설정에서 인증을 하지 않을 경로에 대해서는 인가 필터를 거치지않을것이라고 생각했지만 튜터님의 말씀으로는 모든 요청은 우선 필터를 걸치게 들어오게 됨으로 해당 ATK가 없는 요청이 들어올때 예외처리는 필터단이 아닌 컨트롤러 단에서 처리를 해야 된다는 말씀이였다.이를 통해 해당 예외처리문을 삭제 하였다.

해당 처리 후 인증이 필요한 요청처리에서 토큰이 없이 요청을 해보니 아래와같이 정상적인 결과를얻을수있었다.

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String accessTokenValue = jwtUtil.getJwtFromHeader(req);
        if (StringUtils.hasText(accessTokenValue)) {
            log.info(accessTokenValue);

            try {
                if (!jwtUtil.validateToken(accessTokenValue)) {
                    log.error("유효하지않은 AccesToken");
                    setResponse(res,ErrorCode.ACCESS_DENIED);
                    return;
                }

            } catch (ExpiredJwtException e) {
                // 만료된 accessToken 일경우 accessToken 재발급
                // 쿠키에서 리프레시 토큰가져와서 유효성 검사후  발급
                String refreshToken = jwtUtil.getTokenFromRequest(req);
                log.info(refreshToken);
                if (refreshToken.isEmpty()) {
                    log.error("쿠키에 RereshToken이 존재하지 않습니다.");
                    setResponse(res, ErrorCode.UNKNOWN_ERROR_NOT_EXIST_REFRESHTOKEN);
                    return;
                }

                // refrshToken 검증
                try {
                    if (!jwtUtil.validateToken(refreshToken)) {
                        log.error("유효하지않은 RefreshToken");
                        setResponse(res,ErrorCode.ACCESS_DENIED);
                        return;
                    }
                } catch (ExpiredJwtException exception) {
                    log.error("만료된 RefreshToken");
                    setResponse(res,ErrorCode.EXPIRED_TOKEN);
                    return;
                }

                // refreshToken DB조회
                if (!jwtUtil.checkTokenDBByToken(refreshToken)) {
                    log.error("DB에 해당 RefreshToken이 존재하지 않습니다.");
                    setResponse(res,ErrorCode.UNKNOWN_ERROR_NOT_EXIST_REFRESHTOKEN);
                    return;
                }

                // accessToken 재발급
                Claims user = jwtUtil.getUserInfoFromToken(refreshToken);
                String accessToken = jwtUtil.createAccessToken(user.getSubject(), UserRoleEnum.valueOf(user.get(AUTHORIZATION_KEY).toString()));

                // AccessToken 헤더에 저장
                res.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);

                res.setStatus(200);
                res.setCharacterEncoding("utf-8");
                PrintWriter writer = res.getWriter();
                writer.println("AccessToken이 재발급되었습니다. 다시 시도 해주세요.");

                return;
            }

            // 정상 동작일때
            Claims info = jwtUtil.getUserInfoFromToken(accessTokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }
    private void setResponse(HttpServletResponse response, ErrorCode errorCode) {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        response.setCharacterEncoding("utf-8");
        ErrorResponse errorResponse = new ErrorResponse(errorCode.getHttpStatus(), errorCode.getMessage());

        try {
            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
           } catch (IOException e) {
            e.printStackTrace();
        }

    }
profile
도전하는 사람

0개의 댓글