백엔드 프로젝트 환경 세팅 후 스터디룸 생성 기능을 구현하던 중 403 에러가 발생했습니다.
403 Forbidden은 클라이언트가 요청한 리소스에 대한 접근 권한이 없음을 나타내며, 이는 서버 측에서 액세스 권한이 거부되었음을 의미합니다.
이전 프로젝트에서는 이런 문제가 발생하지 않았었습니다. 그래서 이전 프로젝트와 비교해보니, 스프링 시큐리티의 기본 필터를 사용하지 않고 인터셉터를 적용했던 점이 다르다는 걸 확인할 수 있었습니다.
스프링 시큐리티와 기본 필터에 대해서는 지식이 부족했기 때문에 공부가 필요했습니다. 때마침 유데미에서 인도 선생님의 스프링 강의를 시청중이었기 때문에 강의 내용중 시큐리티 부분을 먼저 공부했고, 이를 통해 문제를 해결할 수 있었습니다. 이번 글에서는 그 과정에 대해서 적어보려고 합니다.
이전 프로젝트에서 기본 필터가 아닌 인터셉터를 적용했던 이유
그 당시에는 웹 개발을 처음 배울 때였습니다. 그렇기 때문에 6개월 동안 공부를 하고, 개인 프로젝트를 만들기 위해서는 선택과 집중이 필요했습니다.
따라서 필터 같은 경우 대부분의 회사에 이미 적용되어 있어 변경할 일이 많지 않고, Spring Security는 다른 부분에 비해 우선순위가 높지 않았기 때문에 기본 필터를 사용하지 않고 인터셉터를 적용했었습니다.
그럼 403 이슈의 원인과 해결방법을 파악하기 위해 Spring Security에 대해 알아보겠습니다.
스프링 시큐리티는 Spring 기반 에플리케이션의 인증, 권한, 인가 등을 담당하는 Spring 하위 프레임워크입니다.
Spring Security는 다음과 같은 순서대로 동작합니다.
Spring Security는 Filter Chain을 통해 다양한 기능을 제공해줍니다.
또한 기본적으로 적용되는 설정이 있는데, 이 기본 설정으로 인해 403 이슈가 발생했습니다.
Spring Filter Chain은 아래의 기능들을 제공합니다:
Spring Security는 POST, PUT 등의 업데이트 요청에 대해 요청 헤더의 일부로 CSRF 토큰을 제공하도록 하는 CSRF 필터를 기본적으로 사용합니다.
여기서 403 에러의 원인을 파악할 수 있습니다. 스터디룸 생성은 POST 요청이므로, 기본적으로 적용된 CSRF 필터에서 CSRF 토큰을 요구합니다. 하지만 CSRF 토큰을 제공하지 않았기 때문에, '권한이 없다'는 403 에러가 발생한 것입니다.
별도의 설정없이 서버를 켜본 결과, 필터 로그에서 CSRF 필터가 자동으로 사용 설정되어 있는 것을 확인할 수 있습니다.
문제를 해결하기 위해 CSRF 필터를 해제하기로 결정했습니다.
Spring Security는 CSRF를 방지하기 위해 기본적으로 동기화 토큰 패턴을 사용합니다. 이는 요청마다 토큰을 생성하여 검증하는 방식입니다.
각 요청에 대해 토큰이 생성되고 POST, PUT 같은 업데이트 요청에는 요청 헤더의 일부로 CSRF 토큰을 제공해야합니다.
그러나, 세션을 사용하지 않고, 상태를 저장하지 않는 REST API를 사용한다면 CSRF가 필요하지 않습니다. 악성 웹 사이트에서 이용할 수 있는 인증정보(브라우저에 있는 쿠키)가 없기 때문입니다.
이후에 리프레쉬 토큰을 브라우저에 쿠키를 이용해 저장할 예정입니다. 하지만 SameSite설정을 strict로 설정하여 쿠키는 해당 사이트로만 전송되게 할 것이기 때문에 CSRF 필터가 없어도 CSRF 공격을 막을 수 있습니다. 따라서 CSRF 필터를 해제하도록 하겠습니다.
필터를 해제하기 위해 먼저 Spring Security 라이브러리에서 SpringBootWebSecurityConfiguration에 기본으로 적용되어 있는 필터 defaultSecurityFilterChain을 찾아줍니다. (프로젝트 내에서 검색하면 쉽게 찾을 수 있습니다)
SpringBootWebSecurityConfiguration은 서블릿 애플리케이션을 보호하는 설정 클래스이며 defaultSecurityFilterChain은 웹 보안을 위한 기본 설정입니다.
하나씩 살펴보면 먼저 HttpSecurity가 인자로 있는 것을 볼 수 있습니다.
HttpSecurity는 필터 체인 설정에 유용한 클래스로, 특정 HTTP 요청에 대해 웹 기반 보안을 설정할 수 있습니다.
이어서 모든 요청을 인증하고,
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
폼 기반 인증을 사용하며,
http.formLogin(withDefaults());
기본인증을 사용하도록 설정되어 있습니다.
http.httpBasic(withDefaults());
그리고 마지막으로 이러한 기능의 기본적인 필터 체인을 생성해서 반환하게 됩니다.
return http.build();
이제 이 메서드를 활용하여 CSRF 필터 사용을 해제해 줄 것입니다.
SecurityConfig를 만들어 앞서 나온 defaultSecurityFilterChain을 오버라이딩 해줍니다.
1. @Bean을 붙여주고, 순서는 중요하지 않기 때문에 @Order은 지워줍니다. 또한 더 이상 기본 필터가 아니기 때문에 securityFilterChain으로 이름을 변경해줍니다.
2. 세션을 사용하지 않을 것이기 때문에 세션에 사용할 정책을 STATELESS로 설정해줍니다. 공식문서 참고
세션 정책
SessionCreationPolicy 코드에 들어가보면 4가지의 세션 정책을 확인할 수 있습니다.
- ALWAYS를 설정하면 세션을 항상생성
- NEVER → HTTP 세션을 생성하지 않지만 이미 있을 경우에는 사용
- IF_REQUIRED → 필요시에만 세션을 생성
- STATELESS → 세션을 사용하지 않음
이렇게 하면 CSRF 필터가 해제됩니다.
그런데 코드를 보시면 CSRF()부분에 빨간 줄이 그어져 있는 걸 볼 수 있습니다.
이는 해당 메서드가 deprecated 되었음을 의미하며, 더 이상 지원되지 않을 것이므로 사용을 자제해야 합니다.
따라서 공식문서를 참고하여 현재 버전에 맞는 코드로 바꿔주겠습니다. 공식문서 참고
공식문서에는 아래처럼 람다표현식을 이용하여 CSRF를 해제하도록 나와있습니다.
람다표현식을 메서드 참조로 변환하면 이렇게 만들 수도 있습니다.
앞서 말한 코드들을 반영하면 최종적으로 이런형태가 됩니다.
authorizeHttpRequests부분은 이후에 하려는 JWT를 이용한 인증작업이 구현이 안되어있어서 일단 지워뒀습니다.
마지막으로 서버를 실행해서 로그를 통해 CSRF 필터가 정말 해제되었는지 살펴보면 CSRF 필터가 사라진 것을 확인할 수 있습니다.
실제로 요청했을 때는 잘 작동하지만 테스트 환경에서는 실패하는 경우가 있을 수 있습니다. 제가 그랬거든요😂테스트를 실행하면 CSRF 필터를 해제했음에도 반영이 되지 않고 CSRF 토큰을 요구하는 걸 볼 수 있습니다.
이유는 테스트 환경에서는 별도의 설정 없이는 실제 애플리케이션의 설정을 그대로 사용하지 않기 때문입니다. 그래서 테스트 시에 특정 설정이 필요할 경우, 해당 설정 클래스를 @Import를 통해 명시적으로 가져와야 합니다.
@WebMvcTest(StudyRoomController.class)
@Import({SecurityConfig.class})
class StudyRoomControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private CreateStudyRoomService createStudyRoomService;
// 생략
이번 글을 통해 403 Forbidden 에러가 발생한 원인과 그 해결 방법에 대해 알아보았습니다. 스프링 시큐리티의 기본 설정 때문에 발생한 문제였으며, 이를 해결하기 위해 CSRF 필터를 해제하였습니다. 이 과정을 통해 스프링 시큐리티에 대해 좀 더 깊게 이해할 수 있었습니다.