refreshToken 을 안전하게 생성

By | 2026년 2월 20일
Table of Contents

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초정도는 이전 토큰을 유효하게 처리해 줘야 하는 로직이 필요하다.

답글 남기기