Spring Security CSRF · CORS · CSP 완전 정리

Spring Security에서 가장 자주 혼동되는 세 가지 웹 보안 메커니즘 — CSRF 토큰, CORS 정책, Content-Security-Policy — 의 개념 차이와 설정 방법을 실전 코드와 함께 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 폼 로그인의 인증 흐름과 SecurityFilterChain 설정을 살펴봤습니다. 이번 글에서는 웹 보안을 다룰 때 가장 자주 혼동되는 세 가지 개념, CSRF · CORS · CSP를 각각 명확히 정의하고 Spring Security에서 어떻게 설정하는지 실전 코드로 정리합니다.

세 가지 보안 메커니즘 한눈에 보기

CSRF, CORS, CSP는 이름이 비슷하지만 방어하는 공격 벡터가 완전히 다릅니다.

CSRF · CORS · CSP 보안 메커니즘 비교

  • CSRF: 악성 사이트가 이미 로그인된 사용자의 세션을 이용해 서버에 요청을 위조하는 공격을 막습니다. 서버가 발급한 비밀 토큰을 매 요청에 포함하게 해서, 토큰을 모르는 외부 사이트의 요청을 거부합니다.
  • CORS: 브라우저의 동일 출처 정책(Same-Origin Policy)으로 인해 교차 출처 요청이 막히는 상황을 서버가 명시적으로 허용하는 메커니즘입니다. 보안을 약화시키는 것이 아니라 “허용 목록을 명시”하는 방식으로 동작합니다.
  • CSP: HTTP 응답 헤더로 브라우저에게 “이 페이지에서는 어떤 출처의 리소스만 로드·실행을 허용한다”고 알려줍니다. XSS 등 인젝션 공격의 피해를 브라우저 레벨에서 최소화합니다.

CSRF 설정

폼 기반 앱 — 기본값 유지

Spring Security는 기본적으로 CSRF 보호가 활성화되어 있습니다. 별도 설정 없이 formLogin()을 사용하면 CSRF 필터가 자동으로 작동합니다. Thymeleaf를 사용하면 th:action만 써도 CSRF 토큰이 hidden 필드로 자동 삽입됩니다.

// 폼 기반 앱: 별도 설정 불필요 (기본값 활성화)
http.csrf(Customizer.withDefaults())

REST API — 비활성화

JWT 등 토큰 기반 인증을 사용하는 Stateless REST API는 세션 쿠키를 사용하지 않으므로 CSRF 공격 대상이 아닙니다. 이 경우 명시적으로 비활성화합니다.

http
  .csrf(AbstractHttpConfigurer::disable)
  .sessionManagement(s -> s
      .sessionCreationPolicy(SessionCreationPolicy.STATELESS))

CookieCsrfTokenRepository — SPA + 쿠키

React/Vue 같은 SPA에서 CSRF 토큰을 쿠키로 전달하고 JavaScript가 읽어서 헤더에 담아 보내는 패턴을 사용할 때는 CookieCsrfTokenRepository를 사용합니다.

http.csrf(csrf -> csrf
    .csrfTokenRepository(
        CookieCsrfTokenRepository.withHttpOnlyFalse()))

withHttpOnlyFalse()를 써야 JavaScript에서 쿠키를 읽을 수 있습니다. 프론트엔드는 X-XSRF-TOKEN 헤더에 쿠키 값을 담아 전송합니다.

CORS 설정

CorsConfigurationSource 빈 등록

Spring Security 6에서 CORS를 설정하는 가장 권장되는 방식은 CorsConfigurationSource 빈을 등록하고 SecurityFilterChain에서 .cors(Customizer.withDefaults())를 호출하는 것입니다.

CSRF · CORS 코드 설정

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With"));
    config.setAllowCredentials(true);
    config.setMaxAge(3600L);  // Preflight 캐시 시간(초)

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

setAllowCredentials(true) 설정 시 setAllowedOrigins("*")(와일드카드)는 사용할 수 없습니다. 반드시 구체적인 출처를 명시해야 합니다.

SecurityFilterChain에 CORS 연결

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .cors(Customizer.withDefaults())  // CorsConfigurationSource 빈 자동 사용
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .build();
}

@CrossOrigin 어노테이션과의 차이

컨트롤러 또는 메서드에 @CrossOrigin을 달아도 CORS가 동작하지만, 이 방식은 MVC 레이어에서만 처리되고 Security 필터 체인보다 늦게 실행됩니다. 전역 CORS 정책은 CorsConfigurationSource를 통해 Security 레벨에서 관리하는 것이 권장됩니다.

CSP 설정

기본 CSP 헤더 추가

http.headers(headers -> headers
    .contentSecurityPolicy(csp ->
        csp.policyDirectives(
            "default-src 'self'; " +
            "script-src 'self' https://cdn.jsdelivr.net; " +
            "style-src 'self' 'unsafe-inline'; " +
            "img-src * data:; " +
            "frame-ancestors 'none'")))

default-src 'self'는 기본적으로 현재 출처의 리소스만 허용합니다. 개별 지시어로 세밀하게 재정의할 수 있습니다.

주요 CSP 지시어

지시어역할
default-src명시되지 않은 모든 소스의 기본값
script-srcJavaScript 허용 출처
style-srcCSS 허용 출처
img-src이미지 허용 출처
connect-srcfetch/XHR/WebSocket 허용 출처
frame-ancestors이 페이지를 iframe에 삽입 허용 출처
report-uri위반 보고 엔드포인트

보고 전용 모드 (Content-Security-Policy-Report-Only)

정책을 먼저 차단 없이 테스트하고 싶을 때는 보고 전용 모드를 사용합니다. 실제 차단은 하지 않고 위반 내용을 report-uri로 전송합니다.

csp.policyDirectives("default-src 'self'; report-uri /csp-report")
   .reportOnly(true)

프로덕션 배포 전 이 모드로 충분히 테스트한 후 reportOnly(false)로 전환하는 것이 권장됩니다.

보안 헤더 전체 설정

Spring Security는 CSRF·CORS·CSP 외에도 다양한 보안 헤더를 기본 제공합니다.

http.headers(headers -> headers
    .xssProtection(xss -> xss.headerValue(
        XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
    .contentTypeOptions(Customizer.withDefaults())
    .frameOptions(frame -> frame.sameOrigin())
    .httpStrictTransportSecurity(hsts -> hsts
        .includeSubDomains(true)
        .maxAgeInSeconds(31536000))
    .contentSecurityPolicy(csp ->
        csp.policyDirectives("default-src 'self'")))
  • X-XSS-Protection: 구형 브라우저의 반사형 XSS 필터 활성화
  • X-Content-Type-Options: nosniff: MIME 타입 추측 방지
  • X-Frame-Options: 클릭재킹 방지 (DENY 또는 SAMEORIGIN)
  • Strict-Transport-Security: HTTPS 강제 (HSTS)

정리 — 어떤 상황에서 무엇을 끄나?

상황CSRFCORSCSP
전통적인 MVC + Thymeleaf유지 (기본값)불필요 (동일 출처)권장
REST API + JWT비활성화필요 (SPA 분리)권장
REST API + 세션 쿠키유지필요권장
SPA (React/Vue) + 쿠키CookieCsrf필요권장

CSRF는 세션 쿠키 기반 인증에만 의미 있습니다. JWT 헤더 인증은 브라우저가 자동으로 첨부하지 않으므로 위조 요청이 애초에 불가능합니다.


지난 글: Spring Security 폼 로그인 구현

다음 글: Spring Security 메서드 보안 — @PreAuthorize · @PostAuthorize


읽어주셔서 감사합니다. 😊