[ STUDY ]/Spring Boot

[ JWT를 활용한 Spring Security ] Access Token 재발급

김강니 2024. 9. 25. 03:17

5. AccessToken이 만료되었을때 - 만료응답을 클라이언트에 반환

//토큰이 유효하지 않을 경우
if (!jwtService.validateToken(jwt, userDetails.getUsername())) {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access token expired");
    return;
}

 

만료된 토큰을 받은 서버는 HTTP상태 코드와 함께 "Access token expired"메세지를 클라이언트에 반환

 

 

6. AccessToken이 만료되었을때 - 만료응답을 받은 클라이언트

api.interceptors.response.use(
  response => response,
  async error => {
    if(error.response.status === 401 && error.response.data === "Access token expired"){
      console.log("토큰 만료");

      const refreshToken = document.cookie.replace(
        /(?:(?:^|.*;\s*)refresh_token\s*\=\s*([^;]*).*$)|^.*$/,
        "$1"
      ); // 쿠키에서 Refresh Token을 가져옴(정규식)
      const accessToken = localStorage.getItem('token'); // access Token

      // refresh, access token을 서버로 보내서 재발급 받은 acccessToken 저장
      const newAccessToken = await refreshTokenAxios(accessToken, refreshToken);

      // 실패했던 요청을 다시 시도
      error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
      return axios(error.config);
    }
    return Promise.reject(error);
  }
);

 

만료 응답을 받은 클라이언트는 서버로부터 access token을 새로 발급받아야한다.

이때 쿠키에 저장된 refresh token을 가져와서 access token과 함께 서버로 보낸다.

 

7. AccessToken이 만료되었을때 - 재발급을 위한 refresh, access token을 받은 서버

public String refreshAccessToken(String accessToken, String refreshToken) {
    // Access token 정보
    String userEmailFromAccess = extractUsername(accessToken);
    Long userIdFromAccess = extractUserId(accessToken);

    // DB에 refreshToken이 저장되어있는지 확인
    User user = userRepository.findByRefreshToken(refreshToken)
            .orElseThrow(() -> new IllegalArgumentException("User not found"));

    // 같은 사용자의 token인지 확인
    if(!userEmailFromAccess.equals(user.getEmail()) || !userIdFromAccess.equals(user.getUserId())) {
        throw new IllegalArgumentException("Tokens do not match for the same user");
    }

    // refresh token 만료 확인
    if(isTokenExpired(refreshToken)) {
        throw new IllegalArgumentException("Refresh token is expired");
    }

    return Jwts.builder()
            .setSubject(user.getEmail())
            .claim("role", user.getUserType())
            .claim("userId", user.getUserId())
            .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME))
            .signWith(key)
            .compact();
}

 

 

1. 서버는 클라이언트로부터 refresh token과 access token을 받는다.

2. access token에서 사용자 이메일, id를 추출한다.

3. refresh token을 이용해 DB에서 해당 사용자가 저장되어있는지 확인한다.
    -> 일치하는 사용자가 없다면 예외발생

4. access token에서 추출한 정보와 refresh token의 정보가 일치하는지 확인한다.
    -> 사용자 정보가 일치하지않는다면 예외발생

5. 사용자 정보까지 일치했다면 refresh token의 만료여부를 검사한다.
    -> refresh token이 만료되었다면 예외발생

6. 만약 모든 단계를 통과했다면 access token 재발급!

 

 

컨트롤러에서의 예외처리

@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequestDTO refreshTokenRequestDTO) {
    try {
        String newAccessToken = jwtService.refreshAccessToken(refreshTokenRequestDTO.getAccessToken(), refreshTokenRequestDTO.getRefreshToken());
        return ResponseEntity.ok(Map.of("access_token", newAccessToken));
    } catch (IllegalArgumentException e) {
        if (e.getMessage().equals("Refresh Token is expired")) {
            return ResponseEntity.status(401).body("Refresh Token expired");
        } else if (e.getMessage().equals("Tokens do not match for the same user")) {
            return ResponseEntity.status(403).body("Tokens do not match for the same user");
        } else {
            return ResponseEntity.status(400).body("Invalid Token");
        }
    }
}

 

상황 예외 메시지(응답 메시지) HTTP 상태 코드
refresh token이 만료 Refresh Token is expired 401(Unauthorized)
access, refresh token 사용자 불일치 Tokens do not match for the same user 403(Forbidden)
기타(유효하지 않은 토큰) Invalid Token 400(BadRequest)

 

 

8. AccessToken이 만료되었을때 - Access Token 재발급

try {
    const response = await api.post("/api/token/refresh", { accessToken, refreshToken });
      const newAccessToken = response.data.access_token;
      localStorage.setItem("token", newAccessToken);
      error.config.headers['Authorization'] = `Bearer ${newAccessToken}`;
      return api(error.config);
    } catch (refreshError) {
    if (refreshError.response.status === 401 && refreshError.response.data.message === "Refresh token is expired") {
      // 로그아웃 처리 및 로그인 페이지로 리다이렉트
      handleLogout();
    } else {
      console.error("토큰 재발급 중 오류 발생:", refreshError);
      return Promise.reject(refreshError);
    }
    }

 

refresh도 정상 상태라면 정상적으로 accesstoken을 재발급 받은 후 실패한 요청 처리

만약에 refresh token이 만료된 상태라면 로그인페이지로 리다이렉트 및 모든 데이터 로그아웃 처리!

 

 


++ cookie는 서버에서 지우는거였음..?

// 쿠키 삭제를 위한 설정
Cookie refreshTokenCookie = new Cookie("refresh_token", null); // 쿠키의 값을 null로 설정
refreshTokenCookie.setPath("/"); // 쿠키의 경로를 지정
refreshTokenCookie.setMaxAge(0); // 쿠키의 만료 시간을 0으로 설정하여 즉시 삭제
refreshTokenCookie.setHttpOnly(true); // HttpOnly 속성 설정
refreshTokenCookie.setSecure(true); // HTTPS 환경에서만 전송 (옵션)

response.addCookie(refreshTokenCookie); // 응답에 쿠키 추가 (삭제 요청)

이렇게 했는데 맞는건가....

 

시큐리티는 아무래도 따로 프젝파서 다시 연습해야할거같음....

그래도 어찌저찌 햇다..!!