관리 메뉴

제뉴어리의 모든것

Filter 등록시 순서 유의 사항 본문

Spring Boot/Spring Security

Filter 등록시 순서 유의 사항

제뉴어리맨 2022. 9. 27. 23:52

문제 에러

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'filterChain' defined in class path resource [com/codestates/new_component/config/SecurityConfigurationV5.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'filterChain' threw exception; nested exception is java.lang.IllegalArgumentException: The Filter class com.codestates.new_component.auth.filter.JwtVerificationFilter does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.

 

문제의 코드

import com.codestates.new_component.auth.filter.JwtVerificationFilter;
import com.codestates.new_component.handler.MemberAuthenticationFailureHandler;
import com.codestates.new_component.handler.MemberAuthenticationSuccessHandler;
import com.codestates.new_component.auth.filter.JwtAuthenticationFilter;
import com.codestates.new_component.token.JwtTokenizer;
import com.codestates.new_component.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

import static org.springframework.security.config.Customizer.withDefaults;


@Configuration
public class SecurityConfigurationV5 {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    public SecurityConfigurationV5(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//        http.headers().frameOptions().sameOrigin() //H2를 위해 같은 도메인에서는 frame 같은 태그에서 uri로 접속 요청 들어 오는것 허용
//                .and()
//                .csrf().disable()
//                .cors(withDefaults()) // CORS 관련 필터를 등록하기 위핸, 기본적으로 corsConfigurationSource 이라는 빈을 등록
//                .formLogin().disable() //폼 로그인 형식 사용 안함
//                .httpBasic().disable() //rest api 이므로 httpBasic() 비활성. disable 안하면 비인증시 로그인폼 화면으로 리다이렉트 된다.
//                .apply(new CustomFilterConfigurer()) //Spring Security의 Configuration을 사용자정의 버전으로 등록
//                .and()
//                .authorizeHttpRequests(authorize -> authorize
//                        .anyRequest().permitAll());
//
//        return http.build();
        http
                .headers().frameOptions().sameOrigin()
                .and()
                .csrf().disable()
                .cors(withDefaults())
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //세션을 생성하지 않도록 정책 지정
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .apply(new CustomFilterConfigurer())  // 추가
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers(HttpMethod.POST,"/*/members").permitAll()
                        .antMatchers(HttpMethod.PATCH, "/*/members/**").hasRole("USER")
                        .antMatchers(HttpMethod.DELETE, "/*/members/**").hasRole("USER")
                        .antMatchers(HttpMethod.GET,"/*/members").hasRole("ADMIN")
                        .antMatchers(HttpMethod.GET,"/*/members/**").hasAnyRole("ADMIN","USER")
                        .anyRequest().permitAll()
                );
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    //본 사이트의 Cors 정책에 대한 설정정보를 빈으로 등록
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST","PATCH","DELETE"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;

    }

    //Spring Security 의 Configuration을 커스터마이징 한것. 기존에는 설정자까지는 커스터마이징을 하지 않았는데, 여기선 설정자 자체를 커스터마이징
    //우리가 구현한 JwtAuthenticationFilter를 등록하는 역할, 말그대로 필터를 등록하고 여러가지 설정을 구성하는 설정자.
    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {

        //실질적으로 커스터마이킹 하는 메소드
        @Override
        public void configure(HttpSecurity builder) throws Exception {

            //AuthenticationManager 객체 얻기, AuthenticationManager : 인증에 대한 처리를 총괄하는 클래스
            //getSharedObject() 를 통해서 Spring Security의 설정을 구성하는 SecurityConfigurer 간에 공유되는 객체를 얻을 수 있음
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            //Spring Security Filter에 추가될 객체, 현재 Custom으로 만든 Filter인 JwtAuthenticationFilter에게 DI 해줌
            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);

            //디폴트 request URL인 “/login”을 “/v11/auth/login”으로 변경
            jwtAuthenticationFilter.setFilterProcessesUrl("/v11/auth/login");
            //인증 성공시 작동되는 핸들러 메소드 위해 핸들러 클래스 등록
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());

            //인증 실패시 작동되는 핸들러 메소드 위해 핸들러 클래스 등록
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);

            //필터 등록 순서 중요
            //Custom으로 만든 Filter를 실질적으로 Spring Security Filter Chain에 추가
            builder.addFilter(jwtAuthenticationFilter)
                    //검증 request시 넘어 온 jwt의 검증 필터 추가, addFilterAfter : 2번째의 인자인 필터의다음으로 필터를추가함
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);




        }
    }

}

 

어디서부터 어디까지 코드를 넣어야 마음속으로 와닿을지 몰라 다 넣었다.

 

문제의 코드 그리고 현 포스트에서 말하려는 부분은 위에 코드 중 아래부분이다.

@Override
public void configure(HttpSecurity builder) throws Exception {

    //AuthenticationManager 객체 얻기, AuthenticationManager : 인증에 대한 처리를 총괄하는 클래스
    //getSharedObject() 를 통해서 Spring Security의 설정을 구성하는 SecurityConfigurer 간에 공유되는 객체를 얻을 수 있음
    AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

    //Spring Security Filter에 추가될 객체, 현재 Custom으로 만든 Filter인 JwtAuthenticationFilter에게 DI 해줌
    JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);

    //디폴트 request URL인 “/login”을 “/v11/auth/login”으로 변경
    jwtAuthenticationFilter.setFilterProcessesUrl("/v11/auth/login");
    //인증 성공시 작동되는 핸들러 메소드 위해 핸들러 클래스 등록
    jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());

    //인증 실패시 작동되는 핸들러 메소드 위해 핸들러 클래스 등록
    jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

    JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);

    //필터 등록 순서 중요
    //Custom으로 만든 Filter를 실질적으로 Spring Security Filter Chain에 추가
    builder.addFilter(jwtAuthenticationFilter)
            //검증 request시 넘어 온 jwt의 검증 필터 추가, addFilterAfter : 2번째의 인자인 필터의다음으로 필터를추가함
            .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}

또 위에 코드 중에서도 아래 부분이다.

builder.addFilter(jwtAuthenticationFilter).addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);

 

위 코드를 설명하자면

HttpSecurity의(builder 부분) addFilter()와 addFilterAfter()를 이용하여 

필터를 등록하는 부분이다.

그리고 등록하는 필터는

JwtVerificationFilter 필터와 JwtAuthenticationFilter 필터 (두개의 클래스 모두 Filter를 상속하여 내가 재정의한 사용자 정의 필터) 이다.

그런데, 중요한점은

addFilter() 의 인자로 JwtAuthenticationFilter 필터는 들어갈 수 있지만,

JwtVerificationFilter 필터는 들어갈 수 없다.

같은 필터인데 말이다.

 

이유는 

JwtAuthenticationFilter : UsernamePasswordAuthenticationFilter 상속

JwtVerificationFilter :  OncePerRequestFilter 상속

인 상태이고

 

UsernamePasswordAuthenticationFilter 와 OncePerRequestFilter 모두 조상을 타고타고 올라가보면 Filter 기능을 할 수 있는 GenericFilterBean 을 상속하고 있긴 하다.

그렇지만, 아래의 사진에서 볼 수 있듯이

addFilter()의 인자로 들어갈 수 있는 필터 타입은 공식문서에서 아래와 같이 명시하였기 때문이다.

그러므로, 두 클래스의 순서는 바뀔수가 없다.

당연히 addFilter에는 JwtAuthenticationFilter 밖에 오지 못한다.

그리고 필터를 여러개 등록하는 상황이면

addFilter() 이후 addFilterAfter() 순으로 등록할 수 있다.

 

 

 

 

결론 : Spring Security필터를 여러개 등록할때

addFilter() 와 addFilterAfter() 메소드를 이용하여 등록하는데,

이때 addFilter()에 올 수 있는 사용자 정의 필터는 무엇을 상속 받아 구현하였느냐에 따라, 인자값이 되는 필터가 될수도 안될수도 있다.

 

 

 

 

참조 : https://jinseongsoft.tistory.com/373