Repository 테스트 코드 작성을 위해 테스트 DB로 H2 를 사용하기 위해 설정을 하던 중 문제가 발생했다.
문제
application.yaml 을 환경 별로 분리하고 h2 관련 설정을 한 후 어플리케이션을 실행하니 다음과 같은 에러가 나온다.
rg.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'securityFilterChain' defined in class path resource [com/sparta/todo/config/SecurityConfig.class]: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception with message: 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=[/]}.
For each MvcRequestMatcher, call MvcRequestMatcher#setServletPath to indicate the servlet path.
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:875) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:828) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:492) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1416) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:597) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:520) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:973) ~[spring-beans-6.0.13.jar:6.0.13]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:950) ~[spring-context-6.0.13.jar:6.0.13]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:616) ~[spring-context-6.0.13.jar:6.0.13]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.1.5.jar:3.1.5]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:738) ~[spring-boot-3.1.5.jar:3.1.5]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:440) ~[spring-boot-3.1.5.jar:3.1.5]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) ~[spring-boot-3.1.5.jar:3.1.5]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.1.5.jar:3.1.5]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.1.5.jar:3.1.5]
at com.sparta.todo.ToDoListApplication.main(ToDoListApplication.java:10) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
에러로그를 좀더 보니 다음과 같은 에러가 스택 트레이스에 나타났다.
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).
해당 에러에 대해서 조금 알아보니 원인을 알아낼 수 있었다.
원인
발생 원인은
org.h2.server.web.JakartaWebServlet 과 org.springframework.web.servlet.DispatcherServlet 두 개의 Servlet이 Servlet Context에 등록되어 어떤 Servlet을 사용해야하는지 명시되어 있지 않아 발생한 것이다.
현재 구현한 SecurityConfig 코드는 다음과 같다.
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final JwtProvider jwtProvider;
private final RedisUtils redisUtils;
private final CustomUserDetailService userDetailService;
private final AuthenticationConfiguration authenticationConfiguration;
private final ObjectMapper objectMapper;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf(AbstractHttpConfigurer::disable);
// FormLogin 비활성
http.formLogin(AbstractHttpConfigurer::disable);
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests(authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers(WHITE_LIST_URL).permitAll() //허용 url 리스트
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
http
.exceptionHandling(handle ->
handle
.authenticationEntryPoint(authenticationEntryPoint())
);
// 필터 관리
http.addFilterBefore(jwtAuthenticationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtProvider, redisUtils, objectMapper);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtProvider, userDetailService, redisUtils);
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new CustomAuthenticationEntryPoint(objectMapper);
}
private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**",
"/api/v1/member/signup",
"/v1/api-docs/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"};
}
여기서 에러가 발생한 부분은 다음코드이다.
.requestMatchers(WHITE_LIST_URL).permitAll() //허용 url 리스트
코드를 보면 RequestMatcher를 사용하지 않고 String 배열을 인자로 넘겨주고 있다.
그래서 해당부분에서 에러가 발생이 되는 것이다.
해결
해결방법은 String[]으로 넘겨주는 인자들을 MvcRequestMatcher[]로 전달해서 MvcRequestMatcher 사용을 명시해주면 된다.
구현 순서는 다음과 같다.
1. HandlerMapptingIntrospector 빈을 주입 받고, MvcRqeustMatcher.Builder를 통해
인스턴스를 생성하고 빈으로 등록하기
@Bean
public MvcRequestMatcher.Builder mvcRequestMatcherBuilder(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}
2. MvcReqeustMatcher.Builder 인스턴스를 주입받아 String[] 인자를 MvcRequesetMatcher[] 로 매핑하기
필자는 메소드로 분리해놓았습니다.
private MvcRequestMatcher[] whiteListMapToMvcRequestMatchers(MvcRequestMatcher.Builder mvc) {
return Stream.of(WHITE_LIST_URL).map(mvc::pattern).toArray(MvcRequestMatcher[]::new);
}
3. 필터체인의 requestMatchers에 매핑한 MvcRequestMatcher[]로 전달하기
.requestMatchers(this.whiteListMapToMvcRequestMatchers(mvc)).permitAll() //허용 url 리스트
설정 후 실행하면 정상 동작하는 것을 확인할 수 있다.
시큐리티 설정 전체코드도 남깁니다.
package com.sparta.todo.config;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.todo.jwt.JwtAuthenticationFilter;
import com.sparta.todo.jwt.JwtAuthorizationFilter;
import com.sparta.todo.jwt.JwtProvider;
import com.sparta.todo.security.CustomAuthenticationEntryPoint;
import com.sparta.todo.security.CustomUserDetailService;
import com.sparta.todo.util.RedisUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
@RequiredArgsConstructor
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final JwtProvider jwtProvider;
private final RedisUtils redisUtils;
private final CustomUserDetailService userDetailService;
private final AuthenticationConfiguration authenticationConfiguration;
private final ObjectMapper objectMapper;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
// CSRF 설정
http.csrf(AbstractHttpConfigurer::disable);
// FormLogin 비활성
http.formLogin(AbstractHttpConfigurer::disable);
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests(authorizeHttpRequests ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers(this.whiteListMapToMvcRequestMatchers(mvc)).permitAll() //허용 url 리스트
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
http
.exceptionHandling(handle ->
handle
.authenticationEntryPoint(authenticationEntryPoint())
);
// 필터 관리
http.addFilterBefore(jwtAuthenticationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
private MvcRequestMatcher[] whiteListMapToMvcRequestMatchers(MvcRequestMatcher.Builder mvc) {
return Stream.of(WHITE_LIST_URL).map(mvc::pattern).toArray(MvcRequestMatcher[]::new);
}
@Bean
public MvcRequestMatcher.Builder mvcRequestMatcherBuilder(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtProvider, redisUtils, objectMapper);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter(jwtProvider, userDetailService, redisUtils);
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new CustomAuthenticationEntryPoint(objectMapper);
}
private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**",
"/api/v1/member/signup",
"/v1/api-docs/**",
"/v2/api-docs",
"/v3/api-docs",
"/v3/api-docs/**",
"/swagger-resources",
"/swagger-resources/**",
"/configuration/ui",
"/configuration/security",
"/swagger-ui/**",
"/webjars/**",
"/swagger-ui.html"};
}
'TIL' 카테고리의 다른 글
2023.12.05 TIL - 팀 프로젝트 시작 (1) | 2023.12.05 |
---|---|
2023.12.04 TIL - JaCoCo 플러그인 적용 (feat. 테스트 커버리지확인 플러그인) (0) | 2023.12.04 |
2023.11.30 TIL - 테스트의 범위.. (0) | 2023.11.30 |
2023.11.29 TIL - 단위테스트와 Mockito (0) | 2023.11.29 |
2023.11.28 TIL - OAuth란? (0) | 2023.11.28 |