[환경] - 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의 로직을 간단히 설명하자면,
- 기존에 부모 객체에서 받은 authentication에서 principal을 이용해 유저 정보를 확인한다.
- 해당 유저의 username이 'test'로 시작하는지 확인한다.
- test로 시작하면, Admin 권한과 User 권한을 모두 부여하고 리턴한다.
- test로 시작하지 않으면, 기존 authentication 객체를 리턴한다.
- 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 - 안성훈
'개발 > Java|Spring' 카테고리의 다른 글
JWT 토큰 라이브러리 java-jwt와 jjwt 간단 비교 (0) | 2023.02.09 |
---|---|
Spring Security JWT 토큰으로 인증하기 (0) | 2023.02.07 |
인텔리제이(IntelliJ) 코드 실시간 반영(서버 자동 재시작) (0) | 2023.01.19 |
스프링 의존성주입 생성자주입(@RequiredArgsconstructor 쓰는 이유) (0) | 2023.01.18 |
Spring Security Authentication(인증) (0) | 2023.01.18 |