0. 서론
최근 Spring Boot 3.x + Spring Security 6.x로 프로젝트를 시작하면서 기존에 잘 사용하던 `.requestMatchers("/login", "/api/**")` 구문이 갑자기 다음과 같은 에러를 발생시키기 시작했다!
This method cannot decide whether these patterns are Spring MVC patterns or not.
If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).
This is because there is more than one mappable servlet in your servlet context:
{org.h2.server.web.JakartaWebServlet=[/h2-console/*], org.springframework.web.servlet.DispatcherServlet=[/]}.
해석해보자면...
SpringMVC 패턴인지 아닌지 알수없다.
만약 Spring MVC 엔드포인트라면 MvcRequestMatcher를 사용하라. 아니면, AntPathRequesMatcher를 사용해주세요.
서블릿 컨텍스트 안에 하나의 서블릿보다 더 많은 매칭가능한 서블릿이 존재합니다:
JakartaWebServlet 과 DispatcherServlet.
1. SpringSecurity6.x 버전부터는 requestMatcher를 명시해주어야 한다.
URL 경로는 이제 MVC 기반인지, 서블릿 기반인지 정확히 구분해서 써줘!
- SpringSecurity팀
SpringSecurity5.x까지도 그냥 requestMatchers("/", "/login") 이런 식으로 막 사용해도 작동했다. 그이유는 SpringSecurity가 알아서 `아~ MVC 패턴 경로겠지` 하고 추측해서 매칭해주었기 때문이다.
SpringSecurity5.x 시절,
http
.authorizeHttpRequests()
.requestMatchers("/login", "/user/**").permitAll()
이렇게만 작성해줘도,
- 내부에서 AntPathRequestMatcher를 자동으로 생성했다.
- 경로가 Spring MVC 컨트롤러의 매핑이든 정적 리소스든 크게 구분하지 않고 느슨히 모두 허용했다.
2. 그런데 왜 이제와서 문제가 되었을까?
예를들어, SpringSecurity 서비스에서 h2-console 데이터베이스를 사용한다고 했을때,
http
.authorizeHttpRequests()
.requestMatchers("/h2-console/**").permitAll()
스프링 시큐리티는 헷갈리게 된다.
“이 요청이 DispatcherServlet을 타는 거야?
아니면 H2 콘솔 서블릿으로 바로 가는 거야?”
이때, DispatcherServlet은 Spring MVC의 핸들러 매핑(컨트롤러)으로 연결되는 서블릿이고,
H2 콘솔 서블릿은 Spring과 무관하게 직접 HTML UI를 서블릿에서 처리하는 완전 별개 서블릿이다.
DispatcherServlet의 경우,
@GetMapping("/chat")
public String chatPage() {
return "chat";
}
DispatcherServlet은 요청이 들어오면, 어떤 @Controller 메서드에 보낼지 결정하는 역할을 한다.
위 코드와 같은 Controller 의 요청을 받아 처리한다. 즉, Spring MVC 의 핵심 서블릿이다.
처리 흐름은 다음과 같다.
요청 → DispatcherServlet → HandlerMapping → @Controller
H2 콘솔 서블릿의 경우,
자체적으로 html 기반 웹 ui를 띄워주는 Spring의 외부 라이브러리이다,
Spring MVC 구조와 전혀 무관하다. 왜냐하면 Controller도, ViewResolver도 사용하지 않는다.
처리 흐름은 다음과 같다.
요청 → org.h2.server.web.WebServlet → 내부 HTML 처리
3. 그러니까, 헷갈리는 건 알겠는데, 5.x버전에서는 문제가 없었잖아.
실은 보안상 구멍이 존재했다. 스프링 시큐리티팀은 이 구멍을 발견하고 6.x버전부터 업데이트했다.
스프링5.x에서는, 모두 AntPathRequestMatcher로 처리되었다.
// 이런 requestMatchers 가 등록되면,
.requestMatchers("/chat", "/h2-console/**")
// 내부에서는 아래와 같이 변환함
new AntPathRequestMatcher("/chat")
new AntPathRequestMatcher("/h2-console/**")
- 이 matcher는 단순히 URL 경로만 보고 매칭했다.
- 어떤 서블릿이 받는지는 아예 고려하지 않았다.
- 그래서 DispatcherServlet이든 H2든 그냥 URL만 일치하면 통과시킨것이다 !!!
AntPathRequestMatcher 의 경우,
단순히 요청 URI 가 매칭되면 전부 다 ! 배칭된다. 해당 경로에 컨트롤러가 존재하든 말든!!!
그러니까, 존재하지 않는 경로도 인증 필터 체인을 타게되는 것이다.
예를들어, 현재 내 서비스에 /api/v1/users 라는 getMapping 만 지정해놓았다고 해보자.
requestMatchers 에다가 /api/** 이렇게 등록을 해놓으면,
/api/v1/users 라는 uri 뿐만이 아니라, /api/foo/bar 와 같이 전혀 등록하지 않은 의미없는 경로에 대해서도 인증 필터를 타게 되는 것이다!
실은 탈필요없다, 왜냐하면 만들어놓은 컨트롤러가 아니니까!!
그러나 MvcRequestMatcher 의 경우,
Spring MVC 에 실제 매핑된 컨트롤러의 경로와 일치해야만 매칭이 된다.
심지어, PathVariable 이 존재하는 컨트롤러 경로까지 추가할 수 있다.
없는 경로는 인증 필터 자체가 타지 않는다~!
이제 /api/foo/bar 와 같이 존재하지 않는 컨트롤러 경로에 대해서는 인증 필터가 반응하지 않게 될 것이다.
4. SpringSecurity 팀이 6.1버전부터 서블릿 종류를 명시하도록 한 이유.
- 실제 존재하는 컨트롤러에만 보안 필터를 적용하도록
- 보안 설정 범위와 실제 API 경로가 100% 일치하는 구조로 유지하도록
- 필터에서 토큰 검증 등 무거운 처리를 피하도록
예시
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
HandlerMappingIntrospector introspector) throws Exception {
// MvcRequestMatcher builder 생성 + DispatcherServlet 경로 명시
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector)
.servletPath("/"); // Spring MVC 기준으로 필터링
http
.csrf().disable()
.headers().frameOptions().disable() // H2 콘솔 띄우기 위함
.authorizeHttpRequests(auth -> auth
// Spring MVC 컨트롤러 경로 - MvcRequestMatcher
.requestMatchers(mvc.pattern("/chat")).permitAll()
.requestMatchers(mvc.pattern("/api/v1/users")).authenticated()
// 정적 자원이나 외부 서블릿 경로 - AntPathRequestMatcher
.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/css/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/js/**")).permitAll()
// 그 외는 인증 필요
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public HandlerMappingIntrospector handlerMappingIntrospector() {
return new HandlerMappingIntrospector();
}
}
결론.
MvcRequestMatcher: Spring MVC 핸들러(@Controller, @RestController)에 매핑된 경로를 보호한다.
AntPathRequestMatcher: 정적 리소스, 외부 서블릿(H2 등) 경로를 허용한다.
혼합해서 매칭 방식을 달리 사용해야한다.
경로마다 처리 대상이 다르기 때문이다.
'Framework > Spring' 카테고리의 다른 글
제어의 역전(IoC, DI, AOP, Bean) (0) | 2025.04.20 |
---|---|
[SAML 2.0] IdP 와 SP 인증을 직접 구현해보자. (3) - SP 설정 : React편 (0) | 2025.01.30 |
[Spring MVC] WebMvcConfigurer로 cross-origin 설정을 해도 cors 오류가 나는 이유 (304 응답에서) (0) | 2025.01.30 |
[SAML 2.0] `SP-Initiated SSO 방식` vs `IdP-Initiated SSO 방식` (0) | 2025.01.29 |
[Servlet] ServletRegistrationBean : Spring 이 Servlet 을 다루는 방법 (0) | 2025.01.07 |