내일배움캠프 Spring 31일차 TIL

Skadi·2024년 2월 5일
0

스프링 숙련

JWT Token과 Spring Security가 적용된 스케쥴앱 서버 제작

JWT 토큰 RefreshToken 적용

  • 기존 JWT 토큰 흐름
    -(정상) API 호출 -> 액세스 토큰이 유효한가?(True) -> 정상 호출 -> 응답 반환
    -(오류) API 호출 -> 액세스 토큰이 유효한가?(False) -> 예외 호출
  • RefreshToken 흐름
    -(정상, 정상) API 호출 -> 액세스 토큰이 유효한가?(True) -> 정상 호출 -> 응답반환
    -(오류, 정상) API 호출 -> 액세스 토큰이 유효한가?(False) -> 리프레시 토큰이 유효한가?(True) -> 새로운 액세스 토큰 발급 -> 정상 호출 -> 응답 반환
    -(오류, 오류) API 호출 -> 액세스 토큰이 유효한가?(False) -> 리프레시 토큰이 유효한가?(False) -> 예외 발생 -> 응답 반환
  • 기존 JWT와의 차이
  1. 로그인 당시 발급하던 토큰(액세스 토큰)에 추가하여 리프레시 토큰을 발급
  2. 액세스 토큰의 유효 기간을 줄여서 보안성 향상
  3. 액세스 토큰이 만료되면 처음에 같이 발급받은 리프레시 토큰을 사용하여 유효할 경우 새로운 액세스 토큰 발급
  • @RestController에서의 JWT Refresh Token 사용
  1. 장점

    • @RestControllerAdvice에서 공통적인 에러처리가 가능하다.
    • 토큰 발급이 단순하고 쉽다.
  2. 단점

    • 토큰 유효성 검사가 필요한 API 요청에 대해서 모두 검사코드가 필요하다.
  • SpringSecurity + Filter를 이용한 JWT Refresh Token 사용
  1. 장점
    • 토큰 유효성 검사가 없는 요청에 대해서만 예외처리를 하고 나머지는 모두 토큰 검사를 자동으로 실행해 준다.
  2. 단점
    • 코드 작성이 어려우며 예외처리가 어렵다.
  • JwtAuthorizationFilter 일부
@Override
    protected void doFilterInternal(
        HttpServletRequest req, HttpServletResponse res, FilterChain filterChain)
        throws ServletException, IOException {

        String accessTokenValue = jwtUtil.getAccessTokenFromRequest(req);
        String claimToToken;

        if (StringUtils.hasText(accessTokenValue)) {
            // JWT 토큰 substring
            accessTokenValue = jwtUtil.substringToken(accessTokenValue);
            log.info(accessTokenValue);
            claimToToken = accessTokenValue;

            try{
                if (!jwtUtil.validateAccessToken(accessTokenValue)) {
                    jwtTokenError.messageToClient(res, 400, "토큰에 문제", "failed");
                    return;
                }
            }catch (ExpiredJwtException e) {
                logger.error("Expired JWT token, 만료된 JWT AccessToken 입니다.");
                String refreshTokenValue = jwtUtil.getRefreshTokenFromRequest(req);
                refreshTokenValue = jwtUtil.substringToken(refreshTokenValue);

                if (StringUtils.hasText(refreshTokenValue) && jwtUtil.validateRefreshToken(refreshTokenValue)){
                    String newAccessToken = jwtUtil.createAccessToken(jwtUtil.getUserInfoFromToken(refreshTokenValue).getSubject());
                    jwtUtil.addAccessTokenToCookie(newAccessToken, res);
                    claimToToken = jwtUtil.substringToken(newAccessToken);
                } else {
                    jwtTokenError.messageToClient(res, 400, "토큰에 문제", "failed");
                    return;
                }
            }


            Claims info = jwtUtil.getUserInfoFromToken(claimToToken);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                jwtTokenError.messageToClient(res, 400, "토큰에 문제", "failed");
                return;
            }
        } else if (req.getRequestURI().startsWith("/user/")) {
            filterChain.doFilter(req, res);
            return;
        } else {

            jwtTokenError.messageToClient(res, 400, "토큰에 문제", "failed");
            return;
        }

        filterChain.doFilter(req, res);
    }
  1. if (StringUtils.hasText(accessTokenValue)) : 엑세스 토큰의 존재 여부 확인
  2. if (!jwtUtil.validateAccessToken(accessTokenValue)) : 액세스 토큰의 검증
  3. catch (ExpiredJwtException e){ ... } : 토큰 오류 중 기간 만료에 대해서만 리프레시 토큰 접근
  4. if (StringUtils.hasText(refreshTokenValue) && jwtUtil.validateRefreshToken(refreshTokenValue)) : 리프레시 토큰 존재여부 확인 + 검증 후 새로운 엑세스 토큰 생성
  5. else if (req.getRequestURI().startsWith("/user/")) : 엑세스 토큰이 존재하지 않지만 /user/로 시작하는 요청의 경우 다음 필터로 패스
  6. else : 토큰도 없고 /user/의 요청도 아니기에 잘못된 요청으로 에러 발생

0개의 댓글