마이의 개발 블로그

[Spring] Spring Security의 securityFilterChain 구성 시 antMatchers() -> requestMatchers()로 변경하는 과정에서 발생했던 문제 해결 과정 본문

개발지식/Spring

[Spring] Spring Security의 securityFilterChain 구성 시 antMatchers() -> requestMatchers()로 변경하는 과정에서 발생했던 문제 해결 과정

개발자마이 2023. 8. 21. 03:55
반응형

배경

JWT 보안 처리를 위한 필터체인 securityFilterChain를 구성 중 일부 코드가 동작하지 않아 동작하도록 고치는 과정에서 연속적으로 맞닥뜨리게 된 문제들이 있었다. 이를 해결하기 위해 꼬박 하루 이상의 시간을 사용했는데, 국내외를 막론하고 생각보다 문제에 대한 답을 제시해주는 포스트가 없기도 했고, 내가 평소에 개발을 하며 어떤 식으로 문제를 살펴보고 해결하는지를 기록으로 남겨두는 것도 좋을 것 같아 글을 쓰게되었다.

문제 1 - requestMatchers 메서드의 String 타입 사용 불가로 인한 컴파일 오류

메시지: cannot resolve method 'requestMatchers(String)'

 

기존에 사용된 antMatchers 메서드가 deprecated되어 requestMatchers로 변경하여 사용해야했다. 그러나 어떤 이유에선지 requestMatchers가 String타입을 인자로 사용할 수 없다는 메시지가 출력되며 컴파일을 할 수 없었다. 사용 예제를 찾아보거나 오류 메시지를 검색해봐도 어떤 예제에서는 String타입을 인자로 사용하기도 했던 것이 동작한다는 사람들이 있어 혼란만 가중될 뿐이였다.

 

공식 문서에서 antMatchers의 deprecation 관련 검색을 수행하니(https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html) 힌트를 얻을 수 있었는데, 일단 antMatcher가 deprecated된 Spring Security 5.8 에서는 requestMatchers의 인자로 String 타입이 사용 가능했다. 현재 버전에 아직 antMatcher가 사용 가능한 점으로 미루어보아 requestMatchers를 제대로 사용하기 위한 버전이 너무 낮은 것 같아 공식 문서(https://docs.spring.io/spring-security/reference/getting-spring-security.html)를 참고하여 Spring Security와 그에 대응하는 Spring 버전을 업그레이드했다. 일단 이렇게 하니 String 타입 관련 오류 메시지는 사라졌다.

 

의존성은 이미 추가되어있었고 아래와 같이 버전 pom.xml의 properties 태그에 버전을 명시하여 업그레이드를 진행했다.

<properties>
   <java.version>17</java.version>
   <spring-security.version>6.1.2</spring-security.version>
   <spring.version>6.0.11</spring.version>
</properties>

-> 요약: Spring Security, Spring 버전 업그레이드 후 String 타입을 requestMatchers() 메서드의 파라미터로 사용 가능하게 됨

문제2 - 버전업에 따른 requestMatchers 메서드의 사용 방법 변경

메시지: This method cannot decide whether these patterns are Spring MVC patterns or not

 

requestMatchers의 String 타입 사용으로 인한 컴파일 오류는 해결되었다. 그러나 어플리케이션 실행 후 위와 같은 메시지가 출력되었다. requestMatchers의 파라미터로 사용하려는 패턴(경로)가 Spring MVC의 패턴인지 아닌지를 이 메서드에서 결정할 수 없다는 뜻으로, 관련 내용을 찾아보니 requestMatchers의 인자를 String 타입이 아닌 별도의 클래스 타입으로 지정해줘야했다(https://spring.io/security/cve-2023-34035).

 

이 문제를 위해 해야할 일이 두 가지 있었는데, 하나는 Spring Security의 버전을 업그레이드하는 것이고, 나머지 하나는 파라미터로 MvcRequestMatcher 또는 AntPathRequestMatcher로 변경하는 것이였다. Spring Security 버전은 이미 업그레이했고 남은 건 파라미터의 변경인데 Spring MVC패턴이면 MvcRequestMatcher를, 그 외에는 AntPathRequestMatcher 클래스를 사용하면 된다고 안내되어 있어 코드를 변경했다(나는 AntPathRequestMatcher). 또한 버전업 이후에 deprecated된 2개 메서드를 고쳐야했는데 사용 방법이 좀 다르다보니 공식문서를 참고하여 코드를 수정했다.

 

-> 요약: requestMatchers 메서드의 파라미터 타입을 AntPathRequestMatcher로 변경, deprecated된 메서드(jwt(), frameOptions()) 고치기

문제3 - 단순한 오타

이제 오류 메시지 없이 원하는 경로로 요청을 보낼 수 있게 되었다. 그러나 요청은 가는데 이상하게도 서버에서 401 Unauthorized 코드가 응답으로 오고 있었다. 처음에는 이전에 겪었던 문제와 마찬가지로 preflight request를 스프링 시큐리티 필터체인에서 걸러내기 때문이라고 생각해 OPTIONS 메서드 관련 코드를 계속 만져보았으나 문제를 해결해주지는 못했다. 나중에 알고보니 내가 토큰 발급을 위해 body에 넣었던 username과 password에 오타가 있었다. 포스트맨에 username을 usename으로 잘못 적어놓고서는 한 시간동안 애꿎은 백엔드 코드만 만지작거리고 있었는데 나중에 알고나서 얼마나 황당하던지. 예전부터 느끼는 건데 역시 야간 코딩은 여러 모로 좋지 않다는 생각이 든다.

 

-> 요약: 토큰 발급을 위한 request body에 오타 고침

Note

정리해놓고 보면 별 것 아닌 것 같아도 단순 검색으로 바로 해결될 문제가 아니다보니 생각보다 문제 해결에 많은 시간이 소요되었다. 그 과정에서 프레임워크 내부의 코드들도 들여다 보고 공식 문서도 찬찬히 볼 수 있는 좋은 경험이였지만 소요된 시간이 아까운 건 어쩔 수 없는 것 같다. 이번 문제의 핵심은 '동작한다고 알려진 코드들을 무조건 신뢰할 수 없다'는 점을 간과했다는 것이다. 때로는 사용하는 기술이나 언어가 어느 시점, 어느 버전의 것인지에 따라 공식 문서를 참고하여 기술의 변화를 추적할 필요도 있다는 점을 알게되었다.


기존 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, HandlerMappingIntrospector introspector) throws Exception {

    return httpSecurity
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/authenticate").permitAll()
                    .requestMatchers(PathRequest.toH2Console()).permitAll() // h2-console is a servlet and NOT recommended for a production
                    .requestMatchers(HttpMethod.OPTIONS,"/**")
                    .permitAll()
                    .anyRequest()
                    .authenticated())
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.
                    sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(
                    OAuth2ResourceServerConfigurer::jwt)
            .httpBasic(
                    Customizer.withDefaults())
            .headers(header -> {header.
                    frameOptions().sameOrigin();})
            .build();
}

변경한 코드

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, HandlerMappingIntrospector introspector) throws Exception {
    
    return httpSecurity
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(new AntPathRequestMatcher("/authenticate")).permitAll()
                    .requestMatchers(new AntPathRequestMatcher(PathRequest.toH2Console().toString())).permitAll() // h2-console is a servlet and NOT recommended for a production
                    .requestMatchers(new AntPathRequestMatcher("/**", "OPTIONS")).permitAll()
                    .anyRequest()
                    .authenticated())
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.
                    sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .httpBasic(
                    Customizer.withDefaults())
            .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
            .build();
}

 

반응형
Comments