[Java] 다중 필터 환경에서 주의 할 점(필터 중복 호출)

Filter(필터)란?

특정한 신호에서 원하지 않는 신호를 차단하거나 원하는 신호만 통과시키는 기능을 하는 장치나 그러한 과정.

즉 요청에 대해 무언가 핸들링이 필요할 때 사용한다.

현재 작업중인 사이드 프로젝트 에서는 유저와 관리자가 존재하며, 유저는 AWS Cognito 를 이용하여 발급 받은 JWT를 사용하고 관리자는 서버에서 생성 한 JWT 를 사용한다.

각 토큰 발급 방식에 따라 암호화 키, 만료시간 등 차이가 있기 때문에 Security 환경 구성을 인증 방식에 따라 별도로 구성해야 하며, 각 구성에 따라 다른 Filter 를 사용해야 한다.

문제가 없다고 생각헀지만, /actuator 관련 된 요청에 권한설정을 작업하다 보니, Security 설정이 제대로 동작하지 않았고, 원인을 분석해보니 특정 엔드포인트에서만 작동하게 처리했다고 생각했던 Security 환경 구성에 문제가 있었다.

그냥 필터에서 shouldNotFilter 를 Override 하여 필터 호출은 되도 필터링을 막을 수는 있겠지만, 나의 의도는 설정하는 엔드포인트에서는 무조건 내가 의도한 필터만 호출하게 하는 것이기 때문에 좀 더 복잡스럽지만 돌아가는 방향을 선택했다.

이 글은 해당 내용에 대해 해결하는 과정을 기록하는 포스팅이다.


다중 필터 호출 원인

심플하다. 커스텀 필터를 등록 할 때 @Component 어노테이션을 사용하면 Bean 으로 등록되는데, 이럴 경우 antMatchers(…)에 파라미터로 전달된 패턴에 대해 security filter chain이 생략되는 것은 맞다. 하지만 이때 다른 필터를 빈으로 등록하게 되면 해당 필터가 security filter chain에 포함되는게 아니라 default filter chain에 포함되기 때문에 그대로 필터가 적용되는 것이다.

기존 Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Configuration
@Order(1)
@PropertySource(value = "classpath:application.yml")
@RequiredArgsConstructor
public class AdminSecurityConfig {
	
	private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
	private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
	private final JwtRequestFilter jwtRequestFilter;

	@Bean
	protected SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
		
		http
			.antMatcher("/voc/answer")
			.authorizeRequests()
			.antMatchers(HttpMethod.POST, "/voc/answer").hasAnyRole("ADMIN", "ADMIN2")
			.and().cors();
		http.csrf().disable(); 
		http.headers()
			.frameOptions().sameOrigin(); 
		http.exceptionHandling()
			.authenticationEntryPoint(jwtAuthenticationEntryPoint)
			.accessDeniedHandler(jwtAccessDeniedHandler)
			.and()
			.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 토큰 기반 인증이므로 세션 사용 x
		http.httpBasic().disable()
			.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
		http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
		
		return http.build();
	}

	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}
}

눈여겨 볼 부분은 두 곳 이다.

  • antMatcher 에서 requestMatchers 로 변경
  • http.addFilterBefore 방법 수정

antMatcher 를 사용하게 되면 JWT 에 대한 필터링은 통과하더라도, Securirty 의 Authentication 객체를 생성 할 수 없어 내부적으로 인증객체에 대해 anonymousUser 가 세팅되기 때문에 requestMatchers 로 변경했고,
addFilterBefore 를 사용 할 때 @Component 로 선언 된 필터를 추가하고 있었지만, Bean 으로 등록하는것을 피하기 위해 DI 를 사용하지 않고, 객체를 생성해서 추가하도록 변경했다.

수정 된 코드는 아래와 같다.

수정 된 Security

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Configuration
@Order(1)
@PropertySource(value = "classpath:application.yml")
@RequiredArgsConstructor
public class AdminSecurityConfig {
	
	private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
	private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

	private final JwtService jwtService;
	private final ObjectMapper objectMapper;

	@Bean
	protected SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {

		http
			.requestMatchers().antMatchers("/voc/answer").and()
			.requestMatchers().antMatchers("/actuator/**").and()
			.authorizeRequests()
			.antMatchers(HttpMethod.POST, "/voc/answer").hasAnyRole("ADMIN", "ADMIN2")
			.antMatchers("/actuator/**").hasAnyRole("ADMIN", "SYSTEM")
			.and().cors();
		
		http.csrf().disable(); 
		http.headers()
			.frameOptions().sameOrigin(); 
		http.exceptionHandling()
			.authenticationEntryPoint(jwtAuthenticationEntryPoint)
			.accessDeniedHandler(jwtAccessDeniedHandler)
			.and()
			.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 토큰 기반 인증이므로 세션 사용 x
		http.httpBasic().disable()
			.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues());
		http.addFilterBefore(new JwtRequestFilter(jwtService, objectMapper), UsernamePasswordAuthenticationFilter.class);
		
		return http.build();
	}
    
    @Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
		return authenticationConfiguration.getAuthenticationManager();
	}
	

}

위와 같이 수정하여 나의 의도대로 동작하게 됐지만, Filter 에서 @Component 를 지웠기 때문에 DI 를 사용하는 부분이 있는 경우에는 생성자에 매개변수를 함께 넘겨줘야 한다.

전체 코드는 여기 에서 확인 가능합니다.

Tags:

Updated:

Leave a comment