본문 바로가기
Spring/Spring Security

Spring Security 5.7 이후 버전에서 WebSecurityConfigurerAdapter가 Deprecate됨으로 인한 대처방법

by 졸린개발자 2022. 8. 11.

안녕하세요. 졸린개발자입니다.

 

오늘은 Spring Security가 5.7로 버전업이 되면서 

WebSecurityConfigurerAdapter가 Deprecate됨으로 인해 많은 분들이 혼란스러우실 것으로 생각됩니다.

 

저도 프로젝트 진행중에 버전업을 수행하면서 문제를 파악하였고, 해결하였습니다.

 

그래서 해결했던 과정을 한번 공유하면 좋지 않을까 해서 포스트를 써봅니다.

 

 

정확히 언제부터 이런일이 발생한 것인가?

Spring Security의 버전이 5.7이후부터 발생합니다.

Spring Boot의 버전은 2.7.1일때 Spring Secuirty 5.7.2버전이 적용되고, 이러한 문제가 생깁니다.

 

 

어떤것이 문제인가?

이전까지는 Spring Security의 필터 설정등의 많은 설정을 WebSecurityConfigurerAdapter를 상속하여 구현하였습니다.

하지만, 이제 Deprecate되었으니, 다른 방법을 써야합니다.

 

 

스프링 공식에서는 Migration에 대한 가이드를 어떻게 제공할까?

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

<p>In Spring Security 5.7.0-M2 we <a href="https://github.com/spring-projects/spring-security/issues/10822">deprecated</a> the <code>WebSecurityConfigurerAdapter</code>, as we encourage users to move towards a component-based security configuration.</p> <p

spring.io

Spring 공식 블로그에서 해당 내용을 참조할 수 있는데요,

SecurityFilterChain을 만드는법, web security를 조작하는 법을 소개해주고 있습니다.

 

문제는, 해당 공식 블로그에서 커스텀한 필터에 대한 설정에 대해 가이드를 전혀 하지 않습니다.

저의 경우, 로그인을 커스텀한 필터를 만들어 사용하고 있지만, 어떻게 바꿔야 할지 감도 오지 않습니다.

 

따라서 저는 이번에 삽질 전의 코드부터 시작해서 어떻게 최종적인 코드가 되었는지 공유해보겠습니다.

 

 

원래 코드

@Configuration
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	private final JsonAuthenticationProvider jsonAuthenticationProvider;
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring()
		   .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
		   .antMatchers("/docs/**", "/error", "/robots.txt");
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(jsonAuthenticationProvider);
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
            .authorizeRequests()
			.anyRequest().authenticated();
		
		http
			.addFilterBefore(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
	}
	
	public JsonAuthenticationFilter jsonAuthenticationFilter() throws Exception {
		JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter();
		jsonAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
		jsonAuthenticationFilter.setAuthenticationSuccessHandler(new JsonAuthenticationSuccessHandler());
		jsonAuthenticationFilter.setAuthenticationFailureHandler(new JsonAuthenticationFailureHandler());
		return jsonAuthenticationFilter;
	}
}

 

위의 코드를 자세히 설명하지는 않겠습니다.

본 포스트의 타겟 독자는 Spring Security 5.7 이후 버전으로의 migration의 문제를 해결하고 싶은 분들이기 때문에,

이미 이러한 코드에 대해 이해를 하고 있다는 가정하에 진행하겠습니다.

 

이제, 위의코드를 바꿔봅시다.

 

 

 

Web Security설정

제가 위에 링크를 걸어둔 스프링 공식 블로그에서 매우매우 잘 나와있습니다. 

단순히 Bean을 만들면, Spring Security에서 알아서 주입받아 사용합니다.

바로 코드로 보여드리겠습니다.

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return web -> {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())
                .antMatchers("/docs/**", "/error", "robots.txt");
    };
}

 

 

권한설정

마찬가지로, 단순히 Bean을 만들면, Spring Security에서 알아서 주입받아 사용합니다.

공식 블로그를 참조하시면 아주 쉽게 하실 수 있습니다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .anyRequest().authenticated();

    return http.build();
}

 

 

 

Custom한 Authentication Filter 설정

문제는 이부분입니다.

 

일단, 공식 블로그에 예시가 나와있지 않습니다.

 

따라서 제가 여러번의 삽질을 거친 결과, 제 코드가 나왔기 때문에, 

몇단계의 과정을 거쳐 최종 코드를 보여드리겠습니다.

 

 

방법1.

우선, filter는 권한설정에서와 마찬가지로 SecurityFilterChain에서 설정하면 되지않을까요?

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .anyRequest().authenticated();
    http.addFilterBefore(jsonAuthenticationFilter(???????), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

public JsonAuthenticationFilter jsonAuthenticationFilter(AuthenticationManager am) throws Exception {
    JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter();
    jsonAuthenticationFilter.setAuthenticationManager(???????????);
    jsonAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);
    jsonAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
    return jsonAuthenticationFilter;
}

그런데, 필터를 설정해주려면, Authentication Manager가 필요합니다.

 

전에는 WebSecurityConfigurerAdapter의 getAuthenticationManager()를 통해 Authentication Manager를 쉽게 가져올 수 있었습니다.

 

하지만, 바뀐 부분에서는 authenticationManager를 쉽게 가져올 수 없습니다.

다른 방법이 필요하겟네요.

 

 

방법2.

공식문서를 참조해보니 다음과 같은 코드가 보이는군요.

@Override
public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
    http.addFilter(new CustomFilter(authenticationManager));
}

HttpSecurity를 통해 Authentication Manager를 가져올 수 있네요,

실제로 적용해 보고 어떤 객체가 오는지 디버거로 확인해봅시다.

 

 

세상은 역시 호락호락하지 않군요, null이 떴습니다.

 

https://stackoverflow.com/questions/51986766/spring-security-getauthenticationmanager-returns-null-within-custom-filter

 

Spring Security getAuthenticationManager() returns null within custom filter

I am trying to implement a very simple example of customized authentication process in Spring to get a better understanding of the concept. I thought i had everything in place now but sending a re...

stackoverflow.com

 

저와 마찬가지로 문제를 겪고 있으신 분이 있군요.

 

위의 페이지에서 제시하는 해결방법은 Authentication Manager를 직접 Bean으로 등록하여 사용하라고 하는군요.

하지만, 그렇게 되면, Spring Security에서 기본적으로 제공하는 Authentication Provider를 직접 넣어야 하고,

현재는 AnonymousProvider와 DaoAuthenticationProvider를 기본으로 제공합니다.

 

하지만 AnonymousProvider를 직접만들어 넣기조차 쉽지 않습니다. AnonymousAuthenticationFilter와 공유하는 key를 넣어줘야하는데, 이러한 key를 어디에서 관리해야할까요? 

 

더욱이, Spring Security의 기본적인 설정이 변경되면, 그에 맞춰서 대응해줘야 할 수도 있습니다.

귀찮음을 싫어하는 저로써는 썩 마음에 들지는 않네요.

 

그럼 당연히 다른 방법이 있겠죠?

 

 

 

방법3(최종).

결국은 소스코드를 보면서 어떻게 작동하는지 살펴봅시다.

 

우선 추론해봅시다.

방법2에서 공식 블로그에서 제공한 코드는 AuthenticationManager를 조회하는 방법으로 

HttpSecurity를 통해서 접근합니다.

 

물론 방법2에서 null이 떴긴했지만, 그럼에도 불구하고,

HttpSecurity에서 AuthenticationManager를 가지고 뭔가를 할 수 있다는 추론이 가능하겠네요.

 

HttpSecurity는 SecurityFilterChain을 만들기위한 Builder역할을 합니다.

따라서 HttpSecurity의 생성자 부분부터 따라가면서 어떻게 SecurityFilterChain을 만들어가는지 살펴봅시다.

 

HttpSecurity의 생성자입니다.

 

오, 인자를 보니 AuthenticationManagerBuilder라는게 있군요, 

AuthenticationManager를 만들어주는 건가 봅니다.

 

디버거를 통해 보니, 아직 생성자에서 아무것도 하지 않아서 그런지 현재의 builder에는 provider가 아무것도 없군요,

parentBuilder에는 예상대로 DaoAuthenticationProvider가 있군요.

 

일단 계속해서 따라가보겠습니다. 

 

오, 놀랍게도 생성자가 끝나니, httpSecurity를 bean으로 등록하는 곳으로 코드가 진행되네요.

사실, 프록시 때문에, 스택트레이스가 너무 지저분해 여기까지 오는걸 예상하지 못했습니다.

 

여기서 기본적인 HttpSecurity의 설정을 해주는것 같습니다.

또 다른 깨달음을 얻네요. HttpSecurity에서 이렇게 기본설정을 해주는군요.

 

하지만, 어디서도 AuthenticationManager에 대해 찾아볼 수가 없습니다.

 

 

그러면 또 다시 추론을 해보죠.

처음 초기화할때가 아니면, HttpSecurity를 Build할때 AuthenticationManager를 생성해줄것으로 예상가능합니다.

 

이제 securityFilterChain의 마지막 부분에 breakpoint를 걸어보죠.

 

자, 따라가봅시다.

 

doBuild라는게 있네요. 저기서 HttpSecurity를 바탕으로 filterChain을 Build하는것 같습니다

따라가봅시다

 

뭔가 여기서 정답을 찾을것만 같은 기분이 드네요. 쭉쭉 따라가 봅시다.

 

지금은 아무것도 안하는군요. 일단 넘어갑시다

 

이 부분은 사실 중요한데, 지금은 상관없으니, 일단 넘어가고

뒤에서 이 메소드에 대해 설명하겠습니다.

 

아, 정답을 찾았습니다.

setSharedObject보이시죠? 저부분이 되어야 AuthenticationManager가 getSharedObject로 조회할 수 있습니다.

else부분을 보시면, AuthenticationManager를 build를 이때 하는것을 볼 수가 있네요.

 

결국 바뀐 Spring Security의 SecurityFilterChain생성방법을 정확히 알아야, 

이 문제를 해결할 수 있습니다.

 

간단한 pseudo code로 설명해드리겠습니다.

 

 

 

Spring Security의 SecurityFilterChain생성방법

1. HttpSecurity를 초기화합니다. 이때 기본적인 필터에 대한 설정을 넣습니다.
2. 초기화된 HttpSecurity가 Bean으로 등록됩니다.

3. 초기화된 HttpSecurity를 자신이 만들 SecurityFilterChain Bean에 주입받습니다.
4. Bean에서 초기화된 HttpSecurity에 자신의 설정을 집어넣습니다.

5. HttpSecurity를 Build합니다. 
    Build과정은 (beforeInit(), init(), beforeConfiguration(), configuration(), performBuild())의 순으로 이루어집니다.
5-1. beforeInit() - 전체 build과정의 init입니다. 기본적으로 아무것도 안합니다. overriding해서 커스텀할수도 잇겠네요.
5-2. init() - 모든 SecurityConfigurer의 init method를 호출합니다.
5-3. beforeConfiguration() - Authentication Manager를 Build하고 setSharedObject(AuthenticationManager)를합니다.
5-4. configuration() - 모든 SecurityConfigurer의 configure method를 호출합니다
5-5. performBuild() - SecurityFilterChain을 생성합니다.

 

자 위의 5-2와 5-4를 주목해봅시다.

SecurityConfigurer를 통해 5-2에서 init, 

5-4에서 configure를 합니다.

그리고 그 사이의 5-3에서 AuthenticationManager를 build합니다

 

즉, SecurityConfigurer를 만들어, configure method에서 AuthenticationManager를 활용하는 코드를 집어넣어주면 되겠네요.

 

제가 SecurityConfigurer까지 설명드리고 싶지만, 이미 글이 너무 길어져 생략하고, 코드를 보여드리겠습니다.

 

@Component
@RequiredArgsConstructor
public class JsonHttpConfigurer extends AbstractHttpConfigurer<JsonHttpConfigurer, HttpSecurity> {

    private final JsonAuthenticationProvider jsonAuthenticationProvider;
    private final JsonAuthenticationSuccessHandler successHandler;
    private final JsonAuthenticationFailureHandler failureHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(jsonAuthenticationFilter(http), UsernamePasswordAuthenticationFilter.class);
    }

    public JsonAuthenticationFilter jsonAuthenticationFilter(HttpSecurity http) throws Exception {
        JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter();
        jsonAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        jsonAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);
        jsonAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        return jsonAuthenticationFilter;
    }
}

 

위의 코드처럼 SecurityConfigurer를 만들어

HttpSecurity를 통해 getSharedObject(AuthenticationManager.class)를 호출하면 null이 아닌 객체를 받을 수 있습니다.

 

디버거로 통해 확인해보죠

 

네, 매우 잘뜨네요.

 

마지막으로 이 configurer를 SecurityFilterChain에 연결하기만 하면 완성입니다.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .anyRequest().authenticated();

    http.apply(jsonConfigurer);

    return http.build();
}

최종적인 SecurityFilterChain 코드입니다.

 

 

Custom한 Authentication Provider 설정

이상하게도, Authentication Provider는 HttpSecurity가 Build하기 전까지만 주면 됩니다.

 

이상한 점은, 5-3에서 AuthenticationManager를 build하면서 build하기전에 AuthenticationProvider를 넘겨줘야,

그걸 가지고 build를 하는데, build를 한 이후에도 Provider를 넘겨줄 수 있습니다.

 

httpSecurity.authenticationProvider(jsonAuthenticationProvider)에서도 코드를 까보면,

builder에게 정보를 넘겨주지, 그걸 직접 AuthenticationManager가 받는 구조는 아닙니다.

 

뭔가 더 있을 거 같은데, 이건 나중에 더 찾아봐야 겠네요.

 

어쨌든 결론은, SecurityFilterChain을 만들때 같이 인자로 줘도 되고, 아니면 Configurer에서도 줘도 됩니다.

 

@Component
@RequiredArgsConstructor
public class JsonHttpConfigurer extends AbstractHttpConfigurer<JsonHttpConfigurer, HttpSecurity> {

    private final JsonAuthenticationProvider jsonAuthenticationProvider;
    private final JsonAuthenticationSuccessHandler successHandler;
    private final JsonAuthenticationFailureHandler failureHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
    	http.authenticationProvider(jsonAuthenticationProvider);
        http.addFilterBefore(jsonAuthenticationFilter(http), UsernamePasswordAuthenticationFilter.class);
    }

    public JsonAuthenticationFilter jsonAuthenticationFilter(HttpSecurity http) throws Exception {
        JsonAuthenticationFilter jsonAuthenticationFilter = new JsonAuthenticationFilter();
        jsonAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        jsonAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);
        jsonAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
        return jsonAuthenticationFilter;
    }
}

 

위의 AuthenticationFilter를 설정한 코드에서, 바뀐부분은 한줄입니다.

http.authenticationProvider(jsonAuthenticationProvider);

 

이 한줄을 추가하시면 됩니다.

 

 

SecurityConfigurer를 설정하였을 때 장점

5.7이전에는 한곳의 Configuration설정에 모든 Security 설정을 넣었어야 했습니다.

 

하지만 이렇게 SecurityConfigurer를 통해, 여러개의 설정파일로 쪼개어,

여러개의 커스텀한 필터를 적용하였을때, 한곳에 복잡한 설정을 보는것보다

훨신 편하게 유지보수가 가능합니다.

 

굳이 말한다면, 한 SecurityConfigurer에서 하나의 filter만을 담당하니, SRP를 지킨다고 볼 수 있겠네요.

 

 

 

결론

오늘도 한건 해결했네요.

 

근데 커스텀한 필터를 제공하는 방법을 왜 spring 공식 블로그에서 알려주지 않았을까요?

 

음. 추측해 보자면 너무 어려워서 넣지 않았거나,

SecurityConfigurer를 설정해줘야 해서 그런것일지도 모르겠군요.

아니면 어쩌면 커스텀한 필터를 넣는것 자체를 그렇게 추천하지 않을지도 모르겠네요.

 

오늘도 긴글 읽어주셔서 감사합니다.