Bootcamp/Implementation

CORS와 CSRF

개발자 오리 2026. 7. 14. 23:13

💡  오늘 학습 키워드

  • CORS
  • CSRF

 

 

🎯  학습 내용 정리

Spring Security를 처음 적용하면 가장 먼저 작성하는 설정 중 하나가 다음 코드이다.

http
    .csrf(AbstractHttpConfigurer::disable)
    .cors(Customizer.withDefaults());

많은 개발자가 이 설정을 관례처럼 복사하여 사용하지만, 왜 필요한지 정확히 이해하지 못하는 경우가 많다.

특히 JWT 기반 인증에서는 대부분 CSRF를 비활성화하는데, 이를 "JWT를 사용하니까 무조건 CSRF를 끄면 된다."라고 이해하는 것은 정확하지 않다.

 

CSRF인증 정보를 브라우저가 자동으로 전송하는 방식과 관련된 공격이고, CORS브라우저의 동일 출처 정책(Same-Origin Policy) 으로 인해 발생하는 제약을 해결하기 위한 정책이다.

 

 

CORS와 CSRF는 서로 다른 문제를 해결한다

이름이 비슷하여 함께 설명되는 경우가 많지만 목적은 전혀 다르다.

항목 CORS CSRF
목적 다른 Origin 요청 허용 여부 사용자의 의도하지 않은 요청 방지
발생 원인 브라우저의 Same-Origin Policy 브라우저의 자동 인증 정보 전송
해결 방법 Access-Control-Allow-Origin CSRF Token 검증

 

즉,

  • CORS는 브라우저가 요청을 막는 문제
  • CSRF는 브라우저가 요청을 대신 보내는 문제

를 해결한다.

 

 

1. CORS

브라우저는 기본적으로 동일 출처 정책(Same-Origin Policy) 을 적용한다.

 

예를 들어

Frontend

http://localhost:3000

 

Backend

http://localhost:8080

는 서로 다른 Origin이다.

 

Frontend에서 다음과 같이 API를 호출하면

fetch("http://localhost:8080/users");

브라우저는 서버가 허용하지 않았다면 요청 결과를 차단한다.

이는 서버가 요청을 거부하는 것이 아니라 브라우저가 응답을 차단하는 것이다.

 

 

CORS는 어떻게 동작할까?

브라우저는 요청을 보내기 전에 서버가 해당 Origin을 허용하는지 확인한다.

서버는 다음과 같은 Header를 응답에 포함한다.

Access-Control-Allow-Origin: http://localhost:3000

 

Spring Security에서는 다음과 같이 설정한다.

@Bean
CorsConfigurationSource corsConfigurationSource() {

    CorsConfiguration configuration =
            new CorsConfiguration();

    configuration.setAllowedOrigins(
            List.of("http://localhost:3000")
    );

    configuration.setAllowedMethods(
            List.of("GET", "POST", "PUT", "DELETE")
    );

    configuration.setAllowedHeaders(
            List.of("*")
    );

    configuration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration("/**", configuration);

    return source;
}

 

그리고 SecurityConfig에서 활성화한다.

http.cors(Customizer.withDefaults());

 

 

 

 

모든 요청이 바로 서버로 전달되는 것은 아니다.

PUT, DELETE, Authorization Header 등을 사용하는 경우 브라우저는 먼저 OPTIONS 요청을 보낸다.

이를 Preflight Request라고 한다.

OPTIONS /users

Origin: http://localhost:3000

Access-Control-Request-Method: PUT

 

서버가 허용하면 실제 요청을 보낸다.

OPTIONS
  ↓
200 OK
  ↓
PUT 요청

 

 

 

2. CSRF

CSRFCross Site Request Forgery의 약자사용자가 의도하지 않은 요청을 보내도록 만드는 공격이다.

 

예를 들어 사용자가 쇼핑몰에 로그인한 상태라고 가정한다.

브라우저에는 Session Cookie가 저장되어 있다.

사용자가 악성 사이트를 방문하면

<form action="https://shop.com/pay">

<input type="hidden">

</form>

브라우저는 Session Cookie를 자동으로 함께 전송한다.

결국 사용자는 의도하지 않았지만 결제가 수행될 수도 있다.

 

 

Spring Security는 CSRF를 어떻게 막을까?

Spring Security는 CSRF Token을 사용한다.

사용자에게 랜덤 Token을 발급하고 요청마다 함께 전송하도록 한다.

Client
  ↓
CSRF Token
  ↓
POST 요청
  ↓
Server
  ↓
Token 검증

Token이 일치하지 않으면 요청을 거부한다.

 

 

그렇다면 JWT에서는 왜 CSRF를 비활성화할까?

여기서 중요한 점은 JWT를 사용한다고 해서 무조건 CSRF가 발생하지 않는 것은 아니다.

CSRF의 핵심 조건은 브라우저가 인증 정보를 자동으로 전송하는가이다.

 

예를 들어 Access Token을 Authorization Header에 저장하는 경우

Authorization: Bearer xxx

 

브라우저는 Header를 자동으로 전송하지 않는다.

JavaScript가 직접 추가해야 한다.

따라서 일반적인 CSRF 공격이 성립하기 어렵다.

이러한 구조에서는 다음과 같이 설정하는 것이 일반적이다.

http.csrf(AbstractHttpConfigurer::disable);

 

 

Refresh Token을 Cookie에 저장하면 어떻게 될까?

실무에서는 다음과 같은 구조를 자주 사용한다.

  • Access Token → Authorization Header
  • Refresh Token → HttpOnly Cookie

이 경우 Refresh Token Cookie는 브라우저가 자동으로 전송한다.

따라서 Refresh Token 재발급 API는 CSRF 공격 대상이 될 수 있다.

이를 방지하기 위해 다음과 같은 방법을 함께 사용한다.

  • SameSite=Lax 또는 Strict
  • Origin 검증
  • Refresh Token Rotation
  • CSRF Token 적용(필요한 경우)

즉, JWT를 사용하더라도 Cookie 기반 인증 정보가 있다면 CSRF를 다시 고려해야 한다.

 

 

 

JWT 기반 인증에서 자주 사용하는 SecurityConfig 설정은 다음과 같다.

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {

    http
            .csrf(AbstractHttpConfigurer::disable)

            .cors(Customizer.withDefaults())

            .sessionManagement(session ->
                    session.sessionCreationPolicy(
                            SessionCreationPolicy.STATELESS
                    )
            );

    return http.build();
}

이 설정은 다음과 같은 의미를 가진다.

  • Session 사용 안 함
  • CSRF 비활성화
  • CORS 활성화
  • Stateless 인증

 

 

실무에서 자주 하는 실수를 알아보자.

 

1. CORS와 CSRF를 같은 개념으로 이해하는 경우

둘은 완전히 다른 문제를 해결한다.

  • CORS → 브라우저의 보안 정책
  • CSRF → 인증 정보를 악용하는 공격

 

2. allowedOrigins("*")와 allowCredentials(true)를 함께 사용하는 경우

다음 설정은 허용되지 않는다.

configuration.setAllowedOrigins(List.of("*"));
configuration.setAllowCredentials(true);

브라우저 명세상 Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true 는 함께 사용할 수 없다.

Credential(쿠키, 인증 정보)을 허용한다면 구체적인 Origin을 지정해야 한다.

 

3. JWT를 사용하니까 무조건 CSRF를 꺼도 된다고 생각하는 경우

이는 가장 흔한 오해이다.

판단 기준은 JWT 사용 여부가 아니라 브라우저가 인증 정보를 자동으로 전송하는지 여부이다.

Access Token을 Authorization Header로 전달하는 경우에는 일반적으로 CSRF 위험이 낮지만, Refresh Token을 Cookie에 저장하는 경우에는 CSRF 방어 전략을 함께 고려해야 한다.

 

4. OPTIONS 요청을 허용하지 않는 경우

Preflight Request가 차단되면 실제 API 요청도 수행되지 않는다.

다음과 같이 OPTIONS 요청을 허용하거나 CORS 설정이 정상적으로 적용되었는지 확인해야 한다.

http.authorizeHttpRequests(auth -> auth
        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
        .anyRequest().authenticated()
);

 

 

 

 

전체적인 흐름을 정리해보면 다음과 같다.

Browser
  ↓
Origin 확인
  ↓
CORS 검사
  ↓
JWT Filter
  ↓
Authentication
  ↓
Authorization
  ↓
Controller

 

CSRF가 필요한 경우

POST
  ↓
CSRF Token
  ↓
Token 검증
  ↓
Controller

 

 

CORS와 CSRF는 이름은 비슷하지만 서로 다른 문제를 해결한다.

CORS는 브라우저의 동일 출처 정책으로 인해 발생하는 제약을 해결하기 위한 설정이며, CSRF는 브라우저가 인증 정보를 자동으로 전송하는 특성을 악용한 공격을 방어하기 위한 메커니즘이다.

JWT 기반 인증에서 Access Token을 Authorization Header로 전달하는 경우에는 일반적으로 CSRF 위험이 낮기 때문에 csrf().disable()을 사용하는 경우가 많다. 하지만 Refresh Token을 HttpOnly Cookie에 저장하는 구조라면 브라우저가 쿠키를 자동으로 전송하므로 CSRF 방어 전략을 함께 고려해야 한다.

또한 CORS는 클라이언트와 서버 간의 통신을 허용하기 위한 정책이므로, 프런트엔드와 백엔드가 서로 다른 Origin에서 동작하는 환경이라면 반드시 적절한 CORS 설정을 적용해야 한다.

 

 

📚  한줄 정리

CORS는 브라우저의 요청을 허용하기 위한 정책이고, CSRF는 브라우저의 자동 인증 전송을 악용한 공격을 방어하는 메커니즘이며, JWT 환경에서는 토큰의 종류보다 인증 정보를 전달하는 방식이 CSRF 적용 여부를 결정한다.