Spring Security에서 애플리케이션 보안을 구성하는 영역은 인증과 인가 두 가지이다. Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행한다. 그리고 인가 과정에서는 해당 리소스에 접근 권한이 있는지를 확인한다.
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Spring Security는 인증 처리 과정을 서블릿이 제공하는 필터를 기반으로 처리한다.
Filter 구조는 위와 같다. FilterChain과 SecurityFilterChain으로 나뉘어지는데, FilterChain은 기존 WAS의 필터 체인이고 오른쪽의 SecurityFilterChain은 스프링 시큐리티에서 사용하는 필터 체인이다.
이때 DelegationgFilterProxy는 무엇일까? SecurityFilterChain이라는 기존 필터 체인과 분리된 필터 체인이 존재하는데, HTTP 요청을 가져와야 하기 때문에 특별한 필터(DelegationgFilterProxy)를 FilterChain에 추가하여 SecurityFilterChain과 연결시켜 주는 역할을 한다.
요청은 ApplicationFilterChain의 순서대로 전달되다가 DelegatinFilterProxy에서 SecurityFilterChain으로 전달되어 그 내부 필터들을 순서대로 지난다. 그리고 다시 ApplicationFilterChain의 나머지 필터들로 전달된다.
정리하자면 다음과 같다.
FilterChain: 기존 WAS의 필터 체인
SecurityFilterChain: 스프링 시큐리티에서 사용하는 필터 체인으로, 인증과 인가에 대한 필터들이 정의되어 있다.
DelegatingFilterProxy: WAS 필터에서 SecurityFilterChain을 호출하기 위한 프록시 패턴의 필터
SecurityFilterChain은 @Configuration
어노테이션을 붙인 설정 정보 클래스를 통해 스프링 빈으로 등록할 수 있다. 그리고 다음과 같이 SecurityFilterChain에 필터들을 등록할 수 있다.
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtTokenFilter jwtTokenFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable) // 보안 관련 필터 등록과 설정
.authorizeHttpRequests(authHttp -> authHttp // 권한 관련 필터 등록과 설정
.requestMatchers("/login").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtTokenFilter, AuthorizationFilter.class); // 커스텀 필터 등록
return http.build();
}
}
SecurityFilterChain은 여러 개 존재할 수 있다. 그리고 경로에 따라 다른 SecurityFilterChain을 거치도록 할 수 있다. 예를 들어 위의 그림처럼 /api/**
경로에 대한 SecurityFilterChain과 나머지 경로(/**
) 에 대한 SecurityFilterChain을 다르게 설정하면, 각 경로에 따라 서로 다른 필터 체인을 통과하게 된다.
스프링 부트에서 Filter를 등록하는 방법은 다음과 같다.
Filter
인터페이스를 구현하고, @Component
를 붙여서 필터를 등록하는 방법
@Configuration
을 붙인 설정 정보 클래스를 만들고, 내부에 FilterRegistrationBean
을 반환하는 메서드를 통해 필터를 등록하는 방법
중요한 점은 이렇게 등록한 필터는 SecurityFilterChain이 아닌 WAS의 FilterChain에 등록된다는 것이다.
예제로 살펴보자. 다음과 같이 (간접적으로) Filter
인터페이스를 구현하고 @Component
어노테이션을 붙여서 필터로 등록한 JwtFilter 클래스가 있다. 이때 JwtFilter는 SecurityFilterChain이 아닌 WAS의 FilterChain에 등록된다.
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {...}
}
특정 경로에 대한 요청은 SecurityFilterChain을 타지 않게 하기 위해 다음과 같이 web.ignoring().requestMatchers();
를 설정했다.
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtExceptionFilter jwtExceptionFilter;
private final JwtFilter jwtFilter;
/*
* Spring Security 필터 설정
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authHttp ->
authHttp.requestMatchers(POST, "/join", "/login").permitAll())
.addFilterBefore(jwtFilter, AuthorizationFilter.class) // JwtFilter는 SecurityChainFilter가 아닌, FilterChain에 등록된다.
;
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring()
.requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
};
}
}
그런데 JwtFilter가 SecurityFilterChain이 아닌 FilterChain에 등록되었기 때문에 해당 경로로 접근했을 때, 여전히 JwtFilter가 적용된다.
따라서 JwtFilter를 @Component
를 붙여 WAS의 FilterChain에 등록할 것이 아니라, 다음과 같이 SecurityFilterChain에 등록해야 한다. 그러면 web.ignoring().requestMatchers();
이 제대로 적용되어, 해당 경로로 접근했을 때 JwtFilter가 적용되지 않는다.
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {...}
}
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtExceptionFilter jwtExceptionFilter;
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authHttp ->
authHttp.requestMatchers(POST, "/join", "/login").permitAll())
.addFilterBefore(new JwtFilter(jwtTokenProvider), AuthorizationFilter.class) //JwtFilter가 SecurityChainFilter에 등록된다.
;
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring()
.requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
};
}
}
WAS의 FilterChain에 등록
addFilterBefore(jwtFilter, AuthorizationFilter.class)
Spring Security의 SecurityChainFilter에 등록
addFilterBefore(new JwtFilter(jwtTokenProvider))
SecurityFilterChain으로 등록한 JwtFilter에서 발생한 예외를 처리하기 위한 JwtExceptionFilter가 있다. 그리고 이 필터를 SecurityFilterChain의 앞단에 위치시켜야 SecurityFilterChain에서 발생한 예외를 처리할 수 있다.
만약 다음과 같이 JwtExceptionFilter를 @Component
를 통해 FilterChain에 등록하면, JwtFilter를 포함한 SecurityFilterChain이 먼저 동작하고, 그 뒤로 FilterChain에 등록된 JwtExceptionFilter가 동작한다. 따라서 SecurityFilterChain에서 예외가 발생하면, JwtExceptionFilter가 호출되지 않는다.
@Component
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (JwtException e) {
...
}
}
}
따라서 다음과 같이 JwtExceptionFilter를 @Component
를 통해 FilterChain에 등록하는 것이 아니라, .addFilterBefore()
를 통해 SecurityFilterChain에 등록해야 한다. 이렇게 되면 JwtExceptionFilter와 JwtFilter이 순서대로 SecurityFilterChain에 등록된다.
public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (JwtException e) {
...
}
}
}
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
/*
* Spring Security 필터 설정
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authHttp ->
authHttp.requestMatchers(POST, "/join", "/login").permitAll())
.addFilterBefore(new JwtFilter(jwtTokenProvider), AuthorizationFilter.class) //JwtFilter를 SecurityChainFilter에 등록
.addFilterBefore(new JwtExceptionFilter(), JwtFilter.class) //JwtExceptionFilter를 SecurityChainFilter에 등록
;
return http.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring()
.requestMatchers(POST, "/join", "/login"); // 해당 경로는 security filter chain을 생략
};
}
}
Reference