-
[Security6] Factory method 'securityFilterChain' threw exception with message: This object has already been built개발/Spring Security 2024. 2. 26. 01:52
시큐리티에서는 생전 처음 보는 버그가 종종 나와 미소 짓게 한다.
5버전과 달리 6버전에서는 설정 클래스가 상속하는 클래스가 없고 메소드 빈 방식으로 설정 작업이 이뤄지기 때문에Authentication Manager(이하 매니저)를 얻는 방식이 기존과 다르다. 여기서 소개할 방법은 사용자가 정의한 Authentication Provider(이하 프로바이더)를 추가하기 용이한 방법이다.
이 설정에서는 필터, 프로바이더를 새로 정의해서 등록한다.
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable); http.addFilterBefore(customAuthenticationFilter(http), UsernamePasswordAuthenticationFilter.class); http.csrf(AbstractHttpConfigurer::disable); return http.build(); } @Bean public CustomAuthenticationFilter customAuthenticationFilter(HttpSecurity http) throws Exception { return new AjaxAuthenticationFilter(authenticationManager(http)); }
설명에서 필요하지 않은 코드는 제외했다. addFilterBefore로 커스텀 필터를 넣기 때문에 @Bean을 달아줄 필요 없지만 반드시 달아야 하는 상황을 후술하겠다. 커스텀 필터에는 커스텀 프로바이더가 사용되어야 하는데, 과거 방식에서는 아래 함수를 이용해서 추가해 테스트했다.
http.authenticationProvider()
deprecated api가 아니기 때문에 사용해봤만 요청을 받았을 때 등록한 프로바이더가 프로바이더 목록에 노출되지 않아 인증 처리가 되지 않았다. 참고로 저 함수는 내부적으로 add 함수를 호출해 추가한다. 웬만하면 내가 겪은 이유들은 하나하나 디버깅해보면서 찾아왔지만, 이번 이슈는 이유를 알기 어려웠다. 어찌 됐든 되지 않으니 다른 방법을 찾아본 결과, 매니저를 직접 얻어서 직접 추가하는 방법을 알아냈다.
public AuthenticationProvider customAuthenticationProvider() { return new CustomAuthenticationProvider(passwordEncoder, userDetailsService); } public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { AuthenticationManagerBuilder managerBuilder = http.getSharedObject( AuthenticationManagerBuilder.class); managerBuilder.authenticationProvider(customAuthenticationProvider()); return managerBuilder.build(); }
완벽하다고 생각했지만 이상한 오류 메시지가 발견됐다. This object has already been built.
결론부터 말하면 같은 HttpSecurity 인스턴스에서 http.build, managerBuilder.build 이렇게 build 함수가 두 번 호출되는 것이 문제다. ManagerBuild 객체는 HttpSecurity 객체에서 얻기 때문이다.
public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> { private AtomicBoolean building = new AtomicBoolean(); private O object; @Override public final O build() throws Exception { if (this.building.compareAndSet(false, true)) { this.object = doBuild(); return this.object; } throw new AlreadyBuiltException("This object has already been built"); }
build 함수가 한 번 호출되면 building 값이 원자적으로(동시성 보장) 변경된다. building의 초기값은 false. 최초 호출 때 compareAndSet을 통해 true로 변경된다. compareAndSet(false, true)의 의미는 building이 false(expectedValue)라면, true(newValue)로 변경하라는 것이다. 이 처리에서는 true를 반환한다. false 반환은 buiding이 expectedValue와 다르다는 걸 뜻한다.
즉 securityFilterChain에서 주입받은 HttpSecurity 인스턴스로 build()가 두 번 호출됐기 때문에 AlreadyBuiltException이 발생했다. 이를 해결하기 위해 커스텀 필터 빈을 생성하는 메소드에 @Bean을 붙여 새로운 HttpSecurity 인스턴스를 주입받도록 한다.
customAuthenticationFilter를 @Bean으로 생성할 때의 HttpSecurity 인스턴스의 주소 해시값
securityFilterChain의 HttpSecurity 인스턴스의 주소 해시값