[태그:] 스프링

  • 스프링 부트 POST 전송 시 403 Forbidden 에러, 왜 그럴까?

    스프링 부트 POST 전송 시 403 Forbidden 에러, 왜 그럴까?

    개발 환경에서 스프링 부트 프로젝트를 진행하다 보면, POST 요청 시 갑자기 403 Forbidden 에러를 마주하고 당황스러울 때가 있습니다. 특히 로컬 환경에서 잘 되던 기능이 갑자기 안 될 때 더욱 답답하죠. 이 글에서는 스프링 부트에서 흔히 발생하는 POST 전송 시 403 에러의 증상, 원인, 그리고 명확한 해결 방법까지 자세히 알아보겠습니다.

    증상: POST 요청 시 403 Forbidden 에러 발생

    • 웹 페이지에서 폼을 통해 데이터를 전송하려고 시도했지만, 서버로부터 403 Forbidden 응답을 받습니다.
    • JavaScript (fetch, XMLHttpRequest 등)를 사용하여 POST 요청을 보냈지만, 마찬가지로 403 Forbidden 에러가 발생합니다.
    • API 테스트 도구(Postman, Insomnia 등)를 사용하여 POST 요청을 보냈을 때도 403 Forbidden 에러가 나타납니다.
    • 브라우저 개발자 도구의 네트워크 탭을 확인하면, 해당 POST 요청의 상태 코드가 403으로 표시되고, 응답 본문에 CSRF 관련 메시지(자세한 내용은 설정에 따라 다를 수 있음)가 포함될 수 있습니다.

    원인: 스프링 시큐리티의 CSRF 보호 기능 활성화

    이러한 403 Forbidden 에러가 발생하는 가장 흔한 이유는 바로 스프링 부트의 기본 보안 기능인 CSRF(Cross-Site Request Forgery) 보호가 활성화되어 있기 때문입니다.

    CSRF(사이트 간 요청 위조)란?

    CSRF는 웹 애플리케이션의 취약점 중 하나로, 사용자가 자신의 의지와는 다르게 악의적인 요청을 서버로 보내도록 유도하는 공격입니다. 스프링 시큐리티는 이러한 공격으로부터 사용자를 보호하기 위해 CSRF 방어 기능을 기본적으로 활성화합니다.

    CSRF 보호 동작 방식:

    1. CSRF 토큰 발급: 서버는 클라이언트에게 CSRF 토큰이라는 고유한 값을 발급합니다. 이 토큰은 일반적으로 GET 요청에 대한 응답 시 쿠키(XSRF-TOKEN 이름의 쿠키) 또는 HTML 폼 내의 숨겨진 필드(_csrf 이름의 필드) 형태로 전달됩니다.
    2. 요청 시 토큰 포함: 클라이언트는 데이터를 변경하는 요청(POST, PUT, DELETE 등)을 서버로 보낼 때, 이전에 받은 CSRF 토큰을 함께 포함하여 전송해야 합니다. 토큰은 HTTP 헤더(X-CSRF-TOKEN) 또는 폼 데이터(_csrf 파라미터) 형태로 전송될 수 있습니다.
    3. 토큰 검증: 서버는 요청과 함께 전송된 CSRF 토큰이 서버가 발급한 토큰과 일치하는지 검증합니다. 토큰이 일치하지 않으면 해당 요청을 악의적인 요청으로 판단하고 403 Forbidden 에러를 반환합니다.

    로컬 환경에서 403 에러가 발생하는 이유:

    로컬 환경에서 개발 및 테스트를 진행할 때, 종종 CSRF 토큰을 제대로 챙겨서 요청에 포함시키지 않는 경우가 많습니다. 특히 다음과 같은 상황에서 403 에러가 발생하기 쉽습니다.

    • 순수 HTML 폼 사용: Thymeleaf, JSP 등의 템플릿 엔진을 사용하지 않고 직접 HTML 폼을 작성하면서 CSRF 토큰 필드를 추가하지 않은 경우.
    • JavaScript를 이용한 비동기 요청: JavaScript (fetch, XMLHttpRequest 등)를 사용하여 POST 요청을 보내면서 CSRF 토큰을 수동으로 가져와 헤더나 데이터에 포함시키지 않은 경우.
    • API 테스트 도구 사용: Postman, Insomnia 등의 API 테스트 도구를 사용하여 POST 요청을 보내면서 CSRF 토큰 관련 설정을 하지 않은 경우.

    해결 방법: CSRF 토큰을 올바르게 처리하기

    POST 요청 시 발생하는 403 Forbidden 에러를 해결하기 위해서는 요청에 CSRF 토큰을 올바르게 포함시켜야 합니다.

    1. Thymeleaf와 같은 템플릿 엔진 사용 시:

    Thymeleaf를 사용하는 경우, <form> 태그 내에서 th:action 속성을 사용하면 스프링 시큐리티가 자동으로 CSRF 토큰을 숨겨진 필드로 추가해줍니다. 별도의 작업 없이 폼을 통해 POST 요청을 보내면 CSRF 보호를 받을 수 있습니다.

    <form th:action="@{/your-endpoint}" method="post">
        <button type="submit">Submit</button>
    </form>
    

    2. 순수 HTML 폼 사용 시:

    순수한 HTML 폼을 사용하는 경우에는 서버로부터 CSRF 토큰을 받아와서 폼 내에 숨겨진 필드로 직접 추가해야 합니다. 템플릿 엔진에 따라 토큰에 접근하는 방식이 다를 수 있지만, 일반적으로 다음과 같은 형태로 추가할 수 있습니다.

    <form action="/your-endpoint" method="post">
        <input type="hidden" name="_csrf" value="${_csrf.token}">
        <button type="submit">Submit</button>
    </form>
    

    주의: ${_csrf.token} 부분은 템플릿 엔진(JSP, Mustache 등)에 따라 실제 토큰 값을 출력하는 문법으로 변경해야 합니다.

    3. JavaScript를 이용한 비동기 요청 시:

    JavaScript를 사용하여 POST 요청을 보내는 경우에는 다음과 같은 방법으로 CSRF 토큰을 요청에 포함시킬 수 있습니다.

    • HTTP 헤더에 포함: 서버로부터 CSRF 토큰을 얻어와서 (XSRF-TOKEN 쿠키 값 또는 meta 태그 등에서 추출) 요청 헤더에 X-CSRF-TOKEN 이름으로 추가합니다.
    fetch('/your-api-endpoint', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': getCsrfToken() // CSRF 토큰을 얻는 함수
        },
        body: JSON.stringify({ /* 요청 데이터 */ })
    })
    .then(response => { /* 응답 처리 */ });
    
    function getCsrfToken() {
        const metaTag = document.querySelector('meta[name="_csrf"]');
        return metaTag ? metaTag.getAttribute('content') : '';
        // 또는 쿠키에서 'XSRF-TOKEN' 값을 읽어오는 로직 구현
    }
    
    • 폼 데이터에 포함: 폼 데이터를 구성하여 요청을 보내는 경우, _csrf 파라미터와 함께 CSRF 토큰 값을 포함시킵니다.
    const formData = new FormData();
    formData.append('someData', 'value');
    formData.append('_csrf', getCsrfToken());
    
    fetch('/your-api-endpoint', {
        method: 'POST',
        body: formData
    })
    .then(response => { /* 응답 처리 */ });
    

    4. API 테스트 도구 사용 시:

    Postman, Insomnia 등의 API 테스트 도구를 사용하는 경우에는 다음과 같이 CSRF 토큰을 설정해야 합니다.

    • 쿠키 설정: 웹 브라우저를 통해 해당 애플리케이션에 접속하여 CSRF 토큰이 담긴 쿠키(XSRF-TOKEN)를 확인하고, API 테스트 도구의 쿠키 설정에 해당 쿠키를 추가합니다.
    • 헤더 설정: X-CSRF-TOKEN 헤더를 추가하고, 쿠키에서 얻은 CSRF 토큰 값을 헤더 값으로 설정하여 요청을 보냅니다.

    5. CSRF 보호 비활성화 (개발 환경 또는 특정 상황에서만 권장):

    개발 환경이나 특별한 이유로 CSRF 보호를 일시적으로 비활성화해야 하는 경우에는 application.yml 파일에 다음과 같이 설정할 수 있습니다.

    spring:
      security:
        csrf:
          enabled: false
    

    또는 자바 설정 클래스에서 HttpSecurity를 통해 비활성화할 수 있습니다.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    public class SecurityConfig {
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
                // ... 다른 보안 설정 ...
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
            return http.build();
        }
    }
    

    주의: CSRF 보호를 비활성화하는 것은 보안상 취약점을 만들 수 있으므로, 프로덕션 환경에서는 절대로 비활성화하지 않는 것을 권장합니다.

    마무리

    스프링 부트에서 POST 요청 시 발생하는 403 Forbidden 에러는 대부분 CSRF 보호 기능이 활성화되어 있고, 요청에 CSRF 토큰이 제대로 포함되지 않았기 때문에 발생합니다. 이 글에서 설명된 증상, 원인, 그리고 다양한 해결 방법을 통해 개발 환경에서 겪는 어려움을 해결하고, 더 나아가 CSRF에 대한 이해를 높이는 데 도움이 되셨기를 바랍니다. 보안은 중요한 문제이므로, 가능한 한 CSRF 보호 기능을 활성화하고 올바른 방식으로 토큰을 처리하는 습관을 들이는 것이 좋습니다.