개발/Java|Spring

Spring Security Custom Fiilter 적용(UsernamePasswordAuthenticationFilter를 활용해 모든 권한을 가진 tester 계정 만들기)

달리초이 2023. 2. 7. 11:51

 

 

[환경] - Spring Security 5.7.6 기준, Java 8 이상

 

'test'라는 계정을 이용해 모든 권한 테스트를 편하게 하고 싶을 때, UsernamePasswordAuthenticationFilter를 활용하면 간단하게 할 수 있다. 그럼 해당 필터를 커스텀하여 테스트를 좀 더 손쉽게 만들어 보자.

 

UsernamePasswordAuthenticationFilter

Spring Security가 제공하는 formLogin을 이용하면 UsernamePasswordAuthenticationToken을 내려주는데 바로 이 토큰을 제공하는데 필터다. username과 password로 로그인을 하려고 하는지 체크하고, 만약 로그인이면 여기서 토큰을 처리하고 가야 할 페이지로 보내준다. 

 

이제 이 필터를 test라는 이름을 가진 계정이 로그인되면 모든 권한을 부여하도록 살짝 커스텀 해보자.

UsernamePasswordAuthenticationFilter를 상속받아 attemptAuthentication을 오버라이드 해 주면 손쉽게 해결된다.

 

UsernamePasswordAuthenticationFilter Custom

/**
 * 테스트 유저인 경우에는 어드민과 유저 권한 모두를 줍니다.
 */
public class TesterAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	public TesterAuthenticationFilter(AuthenticationManager authenticationManager) {
		super(authenticationManager);
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws
			AuthenticationException {
		Authentication authentication = super.attemptAuthentication(request, response);
		User user = (User) authentication.getPrincipal();
		if (user.getUsername().startsWith("test")) {
			// 테스트 유저인 경우 어드민과 유저 권한 모두 부여
			return new UsernamePasswordAuthenticationToken(
					user,
					null,
					Stream.of("ROLE_ADMIN", "ROLE_USER")
							.map(authority -> (GrantedAuthority) () -> authority)
							.collect(Collectors.toList())
			);
		}
		return authentication;
	}
}

 

이 TesterAuthenticationFilter의 로직을 간단히 설명하자면,

 

  1. 기존에 부모 객체에서 받은 authentication에서 principal을 이용해 유저 정보를 확인한다.

  2. 해당 유저의 username이 'test'로 시작하는지 확인한다.
    1. test로 시작하면, Admin 권한과 User 권한을 모두 부여하고 리턴한다.
    2. test로 시작하지 않으면, 기존 authentication 객체를 리턴한다.
  3. 2번에서 생선된 UsernamePasswordAuthenticationToken을 담은 authentication 객체를 AbstractAuthenticationProcessingFilter 클래스의 doFilter 메소드로 다시 리턴한다.

 

커스텀 필터를 구현했으니, 이제 적용을 하자. 커스텀 필터를 addFilterBefore() 메소드를 이용하여 UsernamePasswordAuthenticationFilter 클래스 앞에 넣어주면 의도한대로 작동된다.

 

참고로 @EnableWebSecurity 어노테이션에 (debug = true) 설정을 추가하면, security 필터체인이 적용되는 순서를 아래처럼 확인할 수 있다.

이렇게 작동되면 됨

 

 

적용에 앞서 Spring Security 설정을 먼저 살펴보면, 기존에는 Adapter를 이용하여 아래 코드처럼 구현해왔다.

 

WebSecurityConfigurerAdapter를 상속받는 버전 - deprecate

@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // tester authentication filter
        http.addFilterBefore(
                new TesterAuthenticationFilter(this.authenticationManager()),
                UsernamePasswordAuthenticationFilter.class
        );
        http.httpBasic().disable();
        http.csrf();
        http.rememberMe();
        http.authorizeRequests()
                .antMatchers("/", "/home", "/signup").permitAll()
                .antMatchers("/note").hasRole("USER")
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
                .antMatchers(HttpMethod.DELETE, "/notice").hasRole("ADMIN")
                .anyRequest().authenticated();
        http.formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll();
        http.logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/");
    }

 

하지만,

 

WebSecurityConfigurerAdapter 설계에 결함이 있어서 제대로 수정할 수 없는 문제가 꽤 많이 발생했다. 결국 2022년에 deprecate 돼버렸는데, 그러면서 기존 Spring Security 코드를 5.7.6 버전으로 작성하면서 몇 가지 문제가 발생했다.

관련 깃헙 이슈 : Deprecate WebSecurityConfigurerAdapter #10822

 

그래서 필터체인 방식으로 수정하면, 아래와 같다.

 

SecurityFilterChain @Bean 등록하는 버전

@EnableWebSecurity(debug = true)
@RequiredArgsConstructor
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            // tester authentication filter
            http.addFilterBefore(
                new TesterAuthenticationFilter(authenticationManager를 삽입하는 위치), // 여기서 문제 발생
                UsernamePasswordAuthenticationFilter.class
            );
            http.httpBasic().disable().csrf();
            http.rememberMe();
            http
                .authorizeHttpRequests(auth -> auth
                .antMatchers("/", "/home", "/signup").permitAll()
                .antMatchers("/note").hasRole("USER")
                .antMatchers("/admin").hasRole("ADMIN")
                .antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
                .antMatchers(HttpMethod.DELETE, "/notice").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .permitAll()
            )
            .logout(logout -> logout
                // .logoutUrl("/logout") // post 방식으로만 동작
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // get 방식으로도 동작
                .logoutSuccessUrl("/")
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
            );

    		return http.build();
	}

	@Bean
	public WebSecurityCustomizer webSecurityCustomizer() {
		// 정적 리소스 spring security 대상에서 제외
		return (web) -> web.ignoring()
				.antMatchers("/h2-console/**")
				.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
				;
	}

 

변경이 어렵진 않은데, 한가지 문제가 있다.

// Adapter를 이용한 이전 버전
http.addFilterBefore(
    new TesterAuthenticationFilter(this.authenticationManager()),
    UsernamePasswordAuthenticationFilter.class
);

// FilterChain에 빈 등록하는 버전
http.addFilterBefore(
    new TesterAuthenticationFilter(authenticationManager를 삽입하는 위치), // 여기서 문제 발생
    UsernamePasswordAuthenticationFilter.class
);

 

기존에는 커스텀 필터에 필요한 AuthenticationManager 객체를 WebSecurityConfigurerAdapter에서 가져와서 주입했는데, 지금은 해당 방법을 이용할 수 없다. 그렇다면 AuthenticationManager 객체를 만들어서 넣어주자.

 

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


@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // tester authentication filter
    http.addFilterBefore(
        new TesterAuthenticationFilter(
            authenticationManager(
                http.getSharedObject(AuthenticationConfiguration.class)
            )
        ),
        UsernamePasswordAuthenticationFilter.class
    );
    http.~~~생략~~~
}

 

이렇게 설정을 해주면 해당 커스텀 필터에 AuthenticationManeger가 주입될 때 null 을 피할 수 있다.

 

 

 

 

 

[참고]

Deprecate WebSecurityConfigurerAdapter #10822

패스트캠퍼스 Spring Security - 안성훈

728x90
반응형