Jwt 토큰 재발급 로직 및 과정

박태현·2025년 3월 26일

기타

목록 보기
11/12

현재 Jwt를 이용한 로그인 방식을 사용하고 있기에, Jwt 토큰을 재발급 하는 로직이 필요

import axios from "axios";
import Cookies from "js-cookie";

const api = axios.create();

api.interceptors.request.use(
  (config) => {
    const accessToken = Cookies.get("authorization");

    if (accessToken) {
      config.headers.authorization = `Bearer ${accessToken}`;
    }

    return config;
  },
  (error) => Promise.reject(error)
);

api.interceptors.response.use(
  (response) => response,

  async (error) => {
    const originalRequest = error.config;

    if (
      error.response &&
      error.response.status === 401 &&
      !originalRequest._retry
    ) {
      console.log("Access token이 만료되었습니다. 재발급 시도 중...");

      originalRequest._retry = true;

      try {
        const refreshAuthorization = Cookies.get("refresh_authorization");

        console.log("재발급 요청 중 ..");
        console.log("refreshAuthorization 값:", refreshAuthorization);

        const response = await axios.post(
          `/api/reToken`,
          {},
          {
            headers: {
              refreshAuthorization: `Bearer ${refreshAuthorization}`,
            },
          }
        );

        console.log("재발급 요청 완료");

        // 새 Access Token 저장
        // 헤더의 토큰을 가져올 때는 response.headers[''] 형식을 지키고 반드시 소문자로 시작해야 함
        const newAccessToken = response.headers["authorization"];

        if (!newAccessToken) {
          console.error("새로운 Access Token을 가져오지 못했습니다.");
          return Promise.reject(error);
        }

        Cookies.set("authorization", newAccessToken, {
          secure: true,
          sameSite: "Strict",
        });

        api.defaults.headers.common["authorization"] = `Bearer ${newAccessToken}`;

        console.log("Access token이 성공적으로 재발급되었습니다.");

        // 기존 요청에 새 토큰을 업데이트
        originalRequest.headers.authorization = `Bearer ${newAccessToken}`;

        // 새로운 axios 인스턴스를 사용하여 요청을 다시 보냄
        return axios({...originalRequest, withCredentials: true });

      } catch (err) {
        console.error(
          "Refresh token이 만료되었거나 오류가 발생했습니다. 로그아웃 처리 필요."
        );
        return Promise.reject(err);
      }
    }

    return Promise.reject(error);
  }
);

export default api;

api로 요청을 보내는 경우 interceptor가 우선 요청을 보낸 후 만약 Jwt 토큰이 만료되게 됐다면

, 발생한 HttpServletResponse.*SC_UNAUTHORIZED* 에러를 받아서 Jwt 토큰을 재발급 하는 요청을 실행

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

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

        String requestURI = request.getRequestURI();

        if (requestURI.equals("/api/reToken")) {
            filterChain.doFilter(request, response);
            return;
        }

        String authorization = null;

        Cookie[] cookies = request.getCookies();

        // 쿠키의 유무
        if (cookies == null) {
            filterChain.doFilter(request, response);
            return;
        }

        // 쿠키에서 access token을 추출
        for (Cookie cookie : cookies) {
            if (cookie.getName().equals("authorization")) {
                authorization = cookie.getValue();
            }
        }

        // access token 유무
        if (authorization == null) {
            log.info("token is null");
            filterChain.doFilter(request, response);

            return;
        }

        try {
            jwtUtil.isExpired(authorization);
        } catch (ChatException e) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Token이 만료되었습니다.");
            return;
        }

        String username = null;
        try {
            username = jwtUtil.getUsername(authorization);
        } catch (ChatException e) {
            throw new ChatException(HttpStatus.BAD_REQUEST, ErrorCode.ACCESSTOKEN_IS_EXPIRED);
        }

        String role = jwtUtil.getRole(authorization);

        Member member = Member.builder()
                .username(username)
                .role(Role.valueOf(role))
                .build();

        Map<String, Object> attributes = new HashMap<>();

        CustomOAuth2User customOAuth2User = new CustomOAuth2User(member, attributes);

        Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2User, null, customOAuth2User.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}
String requestURI = request.getRequestURI();

if (requestURI.equals("/api/reToken")) {
	filterChain.doFilter(request, response);
	return;
}

재발급 하는 요청 또한 만료된 Jwt 토큰을 사용하기에 401 에러가 발생하게 되는데

, 이를 필터에서 그냥 넘기기 위해 위 코드 작성


Backend 코드
@PostMapping("/api/reToken")
	public ResponseEntity<?> reissueController(@RequestHeader("refreshAuthorization") String refreshAuthorization, HttpServletRequest request, HttpServletResponse response) {

	log.info("access token 재발급 시도 중 ..");
	return reissueService.reissue(refreshAuthorization, request, response);
}
@Slf4j
@RequiredArgsConstructor
@Service
public class ReissueService {

    private final JwtUtil jwtUtil;
    private final StringRedisTemplate redisTemplate;

    public ResponseEntity<?> reissue(String refreshAuthorization, HttpServletRequest request, HttpServletResponse response) {

        String refresh_token = refreshAuthorization.substring(7);

        try {
            jwtUtil.isExpired(refresh_token);
        } catch (ChatException e){
            throw new ChatException(HttpStatus.BAD_REQUEST, ErrorCode.REFRESHTOKEN_IS_EXPIRED);
        }

        String category = jwtUtil.getCategory(refresh_token);

        if (!category.equals("refresh")) {
            throw new ChatException(HttpStatus.BAD_REQUEST, ErrorCode.IS_NOT_REFRESHTOKEN);
        }

        String username = null;

        try {
            username = jwtUtil.getUsername(refresh_token);
        } catch (ChatException e) {
            throw new ChatException(HttpStatus.BAD_REQUEST, ErrorCode.ACCESSTOKEN_IS_EXPIRED);
        }

        String role = jwtUtil.getRole(refresh_token);

        String new_jwt = jwtUtil.createJwt("access", username, role, 1800000L); // 30분, 1800000L
        String new_refresh = jwtUtil.createJwt("refresh", username, role, 259200000L); // 3일

        redisTemplate.opsForValue().set(
                "refresh_token:" + username,
                new_refresh,
                259200000L,
                TimeUnit.MILLISECONDS
        );

        log.info("새로운 Access 토큰: " + new_jwt);

        Cookie new_refresh_token = CookieUtil.createDefaultCookie("refresh_authorization", new_refresh);

        response.setHeader("authorization", new_jwt);
        response.addCookie(new_refresh_token);

        return new ResponseEntity<>(HttpStatus.OK);
    }
}


profile
꾸준하게

0개의 댓글