Credential Request

Credentialed Request는 쿠키, Authorization 헤더, TLS 인증서 등 사용자 인증 정보를 포함하여 Cross-Origin으로 보내는 요청입니다.

왜 별도 개념이 필요한가

[기본 동작 — Cross-Origin 요청에 인증 정보 미포함]

fetch('https://api.example.com/profile', {
    method: 'GET'
});

→ 브라우저는 기본적으로 Cross-Origin 요청에 쿠키를 포함하지 않음
→ 서버에 세션 쿠키가 전달되지 않음
→ 서버: "로그인 안 된 사용자" → 401 Unauthorized

[이유]
→ 쿠키를 무조건 보내면 CSRF 공격에 취약
→ 보안을 위해 기본적으로 차단
→ 명시적으로 허용해야만 인증 정보 포함 가능

Credentialed Request 활성화

클라이언트와 서버 양쪽 모두 설정이 필요합니다.

클라이언트 — credentials 설정

// fetch API
fetch('https://api.example.com/profile', {
    method: 'GET',
    credentials: 'include'  // ← 핵심: 인증 정보 포함
});

// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/profile');
xhr.withCredentials = true;  // ← 핵심
xhr.send();

// axios
axios.get('https://api.example.com/profile', {
    withCredentials: true  // ← 핵심
});

credentials 옵션 비교

옵션같은 출처다른 출처
omit쿠키 미포함쿠키 미포함
same-origin (기본값)쿠키 포함 O쿠키 미포함
include쿠키 포함 O쿠키 포함 O
same-origin (기본값):
https://example.com → https://example.com/api  → 쿠키 포함 O
https://example.com → https://api.example.com  → 쿠키 미포함 X

include:
https://example.com → https://example.com/api  → 쿠키 포함 O
https://example.com → https://api.example.com  → 쿠키 포함 O

서버 — CORS 응답 헤더 설정

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Credentials: true
[서버가 반드시 지켜야 하는 규칙]

1. Access-Control-Allow-Credentials: true 필수

2. Access-Control-Allow-Origin에 와일드카드(*) 사용 불가
   X Access-Control-Allow-Origin: *
   O Access-Control-Allow-Origin: https://frontend.com

3. Access-Control-Allow-Headers에 와일드카드(*) 사용 불가
   X Access-Control-Allow-Headers: *
   O Access-Control-Allow-Headers: Content-Type, Authorization

4. Access-Control-Allow-Methods에 와일드카드(*) 사용 불가
   X Access-Control-Allow-Methods: *
   O Access-Control-Allow-Methods: GET, POST, PUT, DELETE

→ 인증 정보를 포함하는 요청에는 정확한 출처/헤더/메서드를 명시해야 함
→ *은 "누구에게나 인증 정보를 보낸다"는 뜻이므로 보안상 차단

전체 통신 흐름

단순 요청 + Credential

브라우저                                    서버
   │                                        │
   │  GET /api/profile                      │
   │  Origin: https://frontend.com          │
   │  Cookie: sessionId=abc123  ← 쿠키 포함  │
   │ ───────────────────────────────────→    │
   │                                        │
   │  200 OK                                │
   │  Access-Control-Allow-Origin:          │
   │    https://frontend.com   ← * 불가      │
   │  Access-Control-Allow-Credentials:     │
   │    true                   ← 필수        │
   │  {"name": "홍길동", ...}               │
   │ ←───────────────────────────────────   │

Preflight + Credential

브라우저                                    서버
   │                                        │
   │  ① OPTIONS /api/profile               │
   │  Origin: https://frontend.com          │
   │  Access-Control-Request-Method: PUT    │
   │  Access-Control-Request-Headers:       │
   │    Content-Type                        │
   │ ───────────────────────────────────→    │
   │                                        │
   │  ② 200 OK                             │
   │  Access-Control-Allow-Origin:          │
   │    https://frontend.com                │
   │  Access-Control-Allow-Methods:         │
   │    GET, POST, PUT, DELETE              │
   │  Access-Control-Allow-Credentials:     │
   │    true                                │
   │  Access-Control-Max-Age: 3600         │
   │ ←───────────────────────────────────   │
   │                                        │
   │  ③ PUT /api/profile                   │
   │  Origin: https://frontend.com          │
   │  Cookie: sessionId=abc123              │
   │  Content-Type: application/json        │
   │ ───────────────────────────────────→    │
   │                                        │
   │  ④ 200 OK                             │
   │  Access-Control-Allow-Origin:          │
   │    https://frontend.com                │
   │  Access-Control-Allow-Credentials:     │
   │    true                                │
   │ ←───────────────────────────────────   │

Spring Boot 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(csrf -> csrf.disable());

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // X 와일드카드 사용 불가 (Credentialed Request에서)
        // config.setAllowedOrigins(List.of("*"));

        // O 정확한 출처 명시
        config.setAllowedOrigins(List.of("https://frontend.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
        config.setAllowCredentials(true);  // ← 핵심
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

다중 출처 허용 시

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();

    // 여러 출처 허용
    config.setAllowedOrigins(List.of(
        "https://frontend.com",
        "https://admin.frontend.com",
        "http://localhost:3000"       // 개발 환경
    ));

    config.setAllowCredentials(true);

    // 또는 패턴 사용
    // config.setAllowedOriginPatterns(List.of("https://*.frontend.com"));
    // allowedOriginPatterns는 Credentials: true와 함께 사용 가능

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

흔한 에러와 해결

에러 1: 와일드카드 + Credentials

X 에러 메시지:
"Access to fetch at 'https://api.example.com' from origin
'https://frontend.com' has been blocked by CORS policy:
The value of 'Access-Control-Allow-Origin' must not be '*'
when the request's credentials mode is 'include'."
// X 원인
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true);

// O 해결 — 정확한 출처 명시
config.setAllowedOrigins(List.of("https://frontend.com"));
config.setAllowCredentials(true);

// O 또는 패턴 사용
config.setAllowedOriginPatterns(List.of("*"));  // 패턴은 Credentials와 사용 가능
config.setAllowCredentials(true);

에러 2: Credentials 헤더 누락

X 에러 메시지:
"The value of 'Access-Control-Allow-Credentials' header
must be 'true' when the request's credentials mode is 'include'."
// X 원인 — 서버에서 Allow-Credentials 미설정
config.setAllowCredentials(false);

// O 해결
config.setAllowCredentials(true);

에러 3: 클라이언트 설정 누락

// X credentials 미설정 → 쿠키가 전송되지 않음
fetch('https://api.example.com/profile');

// O credentials: 'include' 설정
fetch('https://api.example.com/profile', {
    credentials: 'include'
});

CORS 요청 3가지 유형 전체 비교

항목Simple RequestPreflight RequestCredentialed Request
Preflight없음OPTIONS 발생조건에 따라 발생
인증 정보미포함미포함쿠키/Authorization 포함
Allow-Origin: *허용허용X 불가
Allow-Credentials불필요불필요true 필수
클라이언트 설정없음없음credentials: 'include'
서버 설정최소메서드/헤더 허용정확한 출처 + Credentials
[세 유형의 관계]

Credentialed Request는 독립 유형이 아니라
Simple / Preflight에 "인증 정보 포함"이 추가된 것

Simple + Credential:
→ GET (기본 헤더만) + credentials: 'include'
→ Preflight 없이 쿠키 포함 전송

Preflight + Credential:
→ POST (application/json) + credentials: 'include'
→ OPTIONS 확인 후 쿠키 포함 전송
[요청 분류 흐름]

                    ┌──────────────────────┐
                    │ credentials: include? │
                    └──────────┬───────────┘
                          │          │
                         No         Yes
                         │          │
                    ┌─────┴─────┐  Credentialed Request
                    │ 단순 요청?  │  (Allow-Origin: * 불가)
                    └─────┬─────┘
                     │         │
                    Yes        No
                     │         │
               Simple      Preflight
               Request     Request

면접 포인트

CSLOG에서 보기