개발 환경에서 스프링 부트 프로젝트를 진행하다 보면, 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 보호 동작 방식:
- CSRF 토큰 발급: 서버는 클라이언트에게 CSRF 토큰이라는 고유한 값을 발급합니다. 이 토큰은 일반적으로 GET 요청에 대한 응답 시 쿠키(
XSRF-TOKEN
이름의 쿠키) 또는 HTML 폼 내의 숨겨진 필드(_csrf
이름의 필드) 형태로 전달됩니다. - 요청 시 토큰 포함: 클라이언트는 데이터를 변경하는 요청(POST, PUT, DELETE 등)을 서버로 보낼 때, 이전에 받은 CSRF 토큰을 함께 포함하여 전송해야 합니다. 토큰은 HTTP 헤더(
X-CSRF-TOKEN
) 또는 폼 데이터(_csrf
파라미터) 형태로 전송될 수 있습니다. - 토큰 검증: 서버는 요청과 함께 전송된 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 보호 기능을 활성화하고 올바른 방식으로 토큰을 처리하는 습관을 들이는 것이 좋습니다.