refreshToken 을 안전하게 생성
refreshToken 은 서버에서 Set-Cookie: HttpOnly; Secure; SameSite=Strict 로 클라이언트 전송하는 방법을 설명합니다.
Cookie 생성 유틸리티 클래스
import jakarta.servlet.http.Cookie;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
@Component
public class CookieUtil {
public ResponseCookie createRefreshTokenCookie(String refreshToken, long maxAgeInSeconds) {
return ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true) // JS 접근 방지
.secure(true) // HTTPS 환경에서만 전송
.path("/") // 모든 경로에서 쿠키 전송
.maxAge(maxAgeInSeconds)
.sameSite("Strict") // CSRF 공격 방어
.build();
}
}
컨트롤러에서 쿠키 전송하기
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final CookieUtil cookieUtil;
public AuthController(CookieUtil cookieUtil) {
this.cookieUtil = cookieUtil;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// 1. 사용자 인증 로직 (생략)
// 2. 토큰 생성
String accessToken = "access-token-string";
String refreshToken = "refresh-token-string";
// 3. Refresh Token을 담은 쿠키 생성
ResponseCookie cookie = cookieUtil.createRefreshTokenCookie(refreshToken, 7 * 24 * 60 * 60);
// 4. Access Token은 바디에, Refresh Token은 헤더(쿠키)에 설정
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new LoginResponse(accessToken));
}
}
클라이언트로부터 쿠키 읽기
@PostMapping("/refresh")
public ResponseEntity<?> refresh(
@CookieValue(name = "refreshToken") String refreshToken) {
// 1. 유효성 검증 및 새로운 Access Token 생성 로직
if (isValid(refreshToken)) {
String newAccessToken = "new-access-token";
return ResponseEntity.ok(new LoginResponse(newAccessToken));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
application-dev.yml, application-prd.yml 에서 Secure 처리
Secure(true) 설정 시, http://localhost 환경에서는 쿠키가 저장되지 않을 수 있습니다.
application-dev.yml
auth:
cookie:
secure: false
same-site: Lax # 로컬 개발 시에는 Strict보다 Lax가 편리할 수 있습니다.
application-prd.yml
auth:
cookie:
secure: true
same-site: Strict
@Component
public class CookieUtil {
@Value("${auth.cookie.secure}")
private boolean isSecure;
@Value("${auth.cookie.same-site}")
private String sameSite;
public ResponseCookie createRefreshTokenCookie(String refreshToken, long maxAgeInSeconds) {
return ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(isSecure) // yml 설정값 적용
.path("/")
.maxAge(maxAgeInSeconds)
.sameSite(sameSite) // yml 설정값 적용
.build();
}
}
주의사항
-
CORS 설정: 프론트엔드와 백엔드 도메인이 다를 경우, 백엔드 CORS 설정에서 allowCredentials(true)를 반드시 활성화해야 쿠키가 주고받아집니다.
-
Local 테스트: Secure(true) 설정 시, http://localhost 환경에서는 쿠키가 저장되지 않을 수 있습니다. 개발 환경에서는 Secure(false)로 운영하거나 HTTPS 대행 프록시를 사용하세요.
-
Set-Cookie 헤더 처리: 크로스 오리진 환경인 경우, 브라우저가 Set-Cookie 헤더를 처리하려면 프론트엔드에서 요청 시 credentials: ‘include’ (또는 axios의 withCredentials: true)를 설정해야 합니다.
Refresh Token Rotation
해커가 브라우저에 까지도 접근 가능한 상황이 되었다는 가정하에,
Access Token 을 재발할 할때마다 Refresh Token 또한 재발급하는 방법입니다.
Refresh Token 을 정상적인 사용자가 사용하여 Access Token 을 재발급하면,
탈취된 Refresh Token 은 무효화됩니다.
반대로 해커가 먼저 재발급을 받았다면, 정상적인 사용자가 로그인 시도하였을 때 모든 토큰을 무효화 함으로서 지속적인 피해를 차단시킬 수 있습니다.
마지막 유효 토큰 + 디바이스 ID 만 보관함으로써, 서버 저장 공간을 효율적으로 운용할 수 있다.
한가지 고려할 것은 한번에 3개의 호출이 발생했을 때, 호출도중 토큰 갱신이 발생하면 오류가 발생한다.
토큰 갱신이후에도 최소 10초정도는 이전 토큰을 유효하게 처리해 줘야 하는 로직이 필요하다.