Tiny Star

IT

[Spring Boot] Spring Security 추가 후 CORS/401/403 오류 해결

하얀지 2025. 6. 27. 00:41

 

멀쩡하던 서비스에서 SpringSecurity 의존성을 추가하면 갑자기 프론트 요청이 막힌다.

❌ fetch 요청 시 CORS 에러 발생
❌ 응답은 갔는데도 403 Forbidden
❌ 로그인하지 않았는데 401 Unauthorized

 

이번 글에서는 Spring Security 추가 이후 발생하는 대표적인 API 통신 문제들왜 발생하는지,

그리고 어떻게 해결할 수 있는지 정리해보겠습니다.

 


 

 

문제 상황

Spring Security를 추가한 직후, 아래와 같은 문제가 발생

증상 설명
CORS 오류 브라우저에서 요청 자체가 막힘 (preflight 실패)
401 Unauthorized Spring Security가 로그인 안 했다고 거부
403 Forbidden 응답은 갔지만 CORS 헤더 누락으로 브라우저가 거부

 

원인 분석

Spring Security는 다음과 같은 보안 기본 정책을 자동 적용합니다:

  1. 모든 요청은 인증된 사용자만 허용
  2. CORS 요청은 차단
  3. CSRF 보호 활성화 (기본)

이 설정으로 인해 프론트에서 API 요청이 다음과 같이 막히게 됩니다.

 

 

CORS? CSRF?

해결하기 전에 CORS와 CSRF가 뭔지 알고 가자.

CORS (Cross-Origin Resource Sharing)

CORS는 '출처(origin)'가 다른 웹 페이지에서 서버에 요청을 보낼 수 있게 허용하는 브라우저 보안 정책

 

예를 들어:

  • 프론트 주소: http://localhost:5173
  • 백엔드 주소: http://localhost:8080

이 두 주소는 포트가 다르기 때문에 출처가 다릅니다.
브라우저는 기본적으로 이런 요청을 보안상 차단합니다. 이걸 뚫어주기 위한 설정이 바로 CORS 설정입니다.

 

백엔드는 "브라우저가 허용해줄지 말지" 판단할 수 있는 신호(Header) 를 줘야 합니다.

  • 요청은 백엔드로 가더라도, 브라우저가 응답을 막을 수 있음
  • 백엔드에서 다음과 같은 응답 헤더를 명시해야 함
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true

 

 

CSRF (Cross-Site Request Forgery)

CSRF는 사용자의 인증 정보를 탈취해 악의적인 요청을 대신 보내는 공격 방식

 

예를 들어, 사용자가 로그인한 상태로 악성 사이트를 방문했을 때,
그 사이트가 사용자의 인증 쿠키를 이용해 백엔드에 의도하지 않은 요청을 보낼 수 있습니다.

 

공격 시나리오 예를들면 아래와 같습니다.

  1. 사용자가 news.com에 로그인한 상태
  2. 동시에 브라우저는 news.com의 세션 쿠키를 보유
  3. 사용자가 attacker.com에 접속
  4. 악성 폼이 자동으로 제출됨 → POST /api/user/delete
  5. 요청에는 news.com의 쿠키도 자동으로 포함됨
    (왜냐면 도메인과 경로가 일치하므로 브라우저가 알아서 붙여줌)
  6. 결과: news.com 서버는 "이 사용자는 로그인 상태구나"라고 오해하고,
    삭제 요청을 처리해버림 → ❌ 사용자 의도와 무관하게 요청 성공

 

Spring Security의 기본 방어:

  • Spring Security는 기본적으로 모든 요청에 CSRF 보호를 적용합니다.
  • 하지만 REST API + 프론트 분리 구조에서는, 브라우저 폼 기반이 아니고 CSRF 토큰을 사용하지 않기 때문에 보호를 꺼주는 것이 일반적입니다.
http.csrf(csrf -> csrf.disable());

JWT 기반, 토큰 기반 인증 시스템에서도 보통 CSRF는 꺼놓습니다.

 

 

 

해결 전략

1. 요청 인증 예외 설정

Spring Security는 모든 요청을 authenticated()로 처리하기 때문에,
회원가입, 로그인, 인증번호 요청 같은 비회원 요청은 명시적으로 permitAll() 예외 처리해야 합니다.

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/auth/**").permitAll()
    .anyRequest().permitAll() // 추후 인증 붙일 때는 authenticated()로 변경
);

 

 

2. CORS 설정 적용 (Security 필터 기준)

단순히 @CrossOrigin이나 WebMvcConfigurer만 써서는 충분하지 않습니다.
Spring Security의 필터 체인에도 CORS 설정을 명시해야 합니다.

http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

그리고 아래처럼 CORS 설정을 정확히 구성합니다.

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173")); // 프론트 도메인
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true); // withCredentials 요청 허용

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

 

 

3. CSRF 비활성화

프론트에서 별도로 CSRF 토큰을 사용하지 않는다면,
API 요청에서는 CSRF를 꺼주는 게 일반적입니다.

http.csrf(csrf -> csrf.disable());

 

 

4. 프론트 요청 설정도 맞춰주기

프론트에서는 반드시 아래와 같이 요청에 credentials: 'include' 옵션을 줘야
쿠키, 세션 등을 포함해서 요청할 수 있고, 백엔드의 allowCredentials(true)와도 호환됩니다.

fetch('http://localhost:8080/api/auth/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, nickname, password }),
  credentials: 'include', // ← 꼭 필요!
});

 

 

 

최종 Security 설정 예시

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().permitAll()
            );
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:5173"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);

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

 

top