티스토리 뷰
과거 - WebSecurityConfigurerAdapter
과거 spring security에서는 인증/인가 관련 설정을 위해 WebSecurityConfigurerAdapter 클래스를 상속받아 configure 메서드를 구현해 설정하도록 만들어놨었다. 코드로는 대충 아래와 같이 표현할 수 있다.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
레거시 코드는 아래와 같은 방식으로 작성했다.
(설명을 위해 구현한 예제용 샘플입니다. 실제 구현과 다릅니다.)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
@Qualifier("CustomAuthenticationProvider")
private AuthenticationProvider authenticationProvider;
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Collections.singletonList(authenticationProvider));
}
@Order(1)
@Configuration
public static class ApiConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.cors().disable().csrf().disable();
http.antMatcher("/api/**")
.authorizeRequests()
.antMatchers("/api/user/**").hasAnyAuthority("API_ALL", "USER_ALL")
.antMatchers("/api/**").hasAnyAuthority("API_ALL")
.and()
.exceptionHandling().accessDeniedHandler(new ApiAccessDeniedHandler())
.authenticationEntryPoint(new ApiAuthenticationEntryPoint());
}
}
@Configuration
@Order(2)
public static class PageConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login", "/access-denied").permitAll()
.antMatchers("/user/**", "/user").hasAnyAuthority("USER_READ")
.antMatchers("/**").hasAuthority("ADMIN_ALL")
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login_process")
.permitAll()
.failureHandler(new LoginFailureHandler())
.successHandler((request, response, authentication) -> {
response.sendRedirect("/dashboard");
})
.and()
.exceptionHandling()
.authenticationEntryPoint(new PageAuthenticationEntryPoint())
.accessDeniedHandler(new PageAccessDeniedHandler());
}
}
}
이러한 구현은 두가지 특징이 있다.
첫째로, 이 구현은 template method 패턴을 사용한 것인데, 설정할 때 특정 동작을 강요하기 위해 위와 같은 패턴을 사용했을 것으로 보인다. 다만, 일반적인 스프링 설정 방식과는 거리가 좀 있다. 스프링에서 설정을 추가할 때는 Bean 메서드를 구현해 의존성을 주입해주는게 일반적이다.( 예시는 링크를 참고한다. )
두번째 특징은 메서드 체이닝을 사용했다는 것이다.
파라미터로 HttpSecurity를 받는데, 여기에 일종의 DSL을 만들어 메서드 체이닝 방식으로 설정을 엮어서 설정하도록 구현이 되어있었다. 아래 코드를 보면, 해당 http 설정은 /** 에 대해 동작하며, authorizeRequests 메서드를 호출한 후, 인가를 할 URL 과 권한을 묶어준다. (설정이 중복되면 앞 설정이 먼저 적용되고, 뒤에 것은 적용되지 않는다) 그리고 and()를 통해 설정을 한번 분리시키고, 다른 설정을 추가한다. formLogin 메서드 뒤에는 username, password, 등을 통해 어떤 동작을 할지 정의하고, 로그인 성공시 동작은 successHandler, 실패시 동작은 failureHandler에 정의한다.
and()를 통해 다른 설정으로 넘어가고, exceptionHandling()을 호출한 다음에 인증이 실패할 경우 처리할 authenticationEntryPoint 설정과 인가가 실패했을 때 처리할 accessDeniedHandler 를 정의하는 방식이다.
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/**")
.authorizeRequests()
.antMatchers("/login", "/access-denied").permitAll()
.antMatchers("/user/**", "/user").hasAnyAuthority("USER_READ")
.antMatchers("/**").hasAuthority("ADMIN_ALL")
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login_process")
.permitAll()
.failureHandler(new LoginFailureHandler())
.successHandler((request, response, authentication) -> {
response.sendRedirect("/dashboard");
})
.and()
.exceptionHandling()
.authenticationEntryPoint(new PageAuthenticationEntryPoint())
.accessDeniedHandler(new PageAccessDeniedHandler());
}
이러한 설정 방식에 대해서 spring security 쪽에서도 문제의식이 있었던 것으로 보인다.
이에 따라 먼저 메서드 체이닝 방식의 경우 spring security 5.2에서 람다DSL을 도입하고, spring security 5.4부터는 새로운 설정방식인 SecurityFilterChain을 도입했다.
현재 - SecurityFilterChain 과 람다 DSL
SecurityFilterChain 에서는 더이상 상속구조를 이용하지 않는다. SecurityFilterChain Bean을 생성해 바로 주입한다.
또한, 설정도 람다DSL을 활용하는 방식으로 바뀌었다. 아래 예제는 Security 설정을 2개 설정하는 예시이다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
@Qualifier("CustomAuthenticationProvider")
private AuthenticationProvider authenticationProvider;
@Bean
@Order(1)
public SecurityFilterChain securityFilterChainForApi(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf(AbstractHttpConfigurer::disable).securityMatcher("/api/**")
.authorizeHttpRequests(k -> {
k.requestMatchers(HttpMethod.GET, "/api/user/**").hasAnyAuthority("USER_READ" , "API_ALL")
.requestMatchers("/api/user/**", "/api/user").hasAnyAuthority("USER_ALL", "API_ALL")
.requestMatchers("/api/**").hasAnyAuthority("ADMIN_ALL", "API_ALL");
})
.authenticationProvider(authenticationProvider).exceptionHandling((s) -> {
s.authenticationEntryPoint(new ApiAuthenticationEntryPoint());
s.accessDeniedHandler(new ApiAccessDeniedHandler());
}).build();
}
@Bean
@Order(2)
public SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
return security.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
authorizationManagerRequestMatcherRegistry.requestMatchers("/login", "/create", "/logout").permitAll()
.requestMatchers("/user", "/user/**").hasAnyAuthority("USER_ALL")
.anyRequest().authenticated();
})
.formLogin((s) -> s.loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new LoginFailureHandler()).successHandler((request, response, authentication) -> {
response.sendRedirect("/dashboard");
}).loginProcessingUrl("/login_process"))
.logout((s) -> s.logoutUrl("/logout").logoutSuccessUrl("/login").permitAll())
.authenticationProvider(authenticationProvider).exceptionHandling((s) -> {
s.authenticationEntryPoint(new PageAuthenticationEntryPoint());
s.accessDeniedHandler(new PageAccessDeniedHandler());
}).build();
}
}
@Component(value = "CustomAuthenticationProvider")
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final AdminLoginService userLoginService;
public CustomAuthenticationProvider(AdminLoginService userLoginService) {
this.userLoginService = userLoginService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String id = (String) authentication.getPrincipal();
String pw = (String) authentication.getCredentials();
Set<SimpleGrantedAuthority> authorities = userLoginService.login(id, pw);
return new UsernamePasswordAuthenticationToken(id, pw, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
위 코드에서 Order는 SecurityFilterChain을 적용할 순서를 의미한다. securityFilterChainForApi 를 먼저 검사하고, securityFilterChain 를 검사한다. 이 때, securityFilterChainForApi Bean에서 검사가 된 것들은 securityFilterChain 에서 검사하지 않는다는 점을 주의해야 한다.
검사 대상은 HttpSecurity#securityMatcher 메서드에서 지정한다. 편의상 "/api/**" 로 지정했지만, 사실 내부에 람다 표현식으로 RequestMatcher를 구현해 복잡한 설정을 추가할 수 있다. 기본적으로 파라미터는 securityMatcher 예를 들어 Accept 헤더가 application/json으로 들어오는 경우에 대한 필터를 만들고 싶으면 아래와 같이 만들수 있다.
@Bean
@Order(1)
public SecurityFilterChain securityFilterChainForApi(HttpSecurity httpSecurity) throws Exception {
return httpSecurity.csrf(AbstractHttpConfigurer::disable).securityMatcher(s-> Optional.ofNullable(s.getHeader("Accept")).orElse("").equals("application/json"))
.authorizeHttpRequests(k -> {
k.requestMatchers(HttpMethod.GET, "/api/user/**").hasAnyAuthority("USER_READ" , "API_ALL")
.requestMatchers("/api/user/**", "/api/user").hasAnyAuthority("USER_ALL", "API_ALL")
.requestMatchers("/api/**").hasAnyAuthority("ADMIN_ALL", "API_ALL");
})
.authenticationProvider(authenticationProvider).exceptionHandling((s) -> {
s.authenticationEntryPoint(new ApiAuthenticationEntryPoint());
s.accessDeniedHandler(new ApiAccessDeniedHandler());
}).build();
}
authorizeHttpRequests 는 인가에 대한 처리이다. url, method 등을 지정해 권한을 처리해 줄 수 있다. 이것도 과거에는 뒤에 메서드를 추가해서 구현했지만, 이제 메서드의 파라미터 안에 람다표현식으로 들어왔다. 더이상 antMatchers를 사용하지 않고 RequestMatcher 를 사용한다. 각 requestMatcher 뒤에 권한을 지정해준다. 이 때, 앞 설정이 뒷 설정에 우선하므로, 앞에서 체크한 URL은 뒤에서 체크하지 않는다는 점에 유의한다.
뒤에는 인증인가에 실패했을 때의 동작을 정의하는 exceptionHandling 이 있다. 여기도 이제 람다표현식으로 메서드 체이닝을 대체했다.
/api/** 가 아닌 URL은 securityFilterChain Bean에서 처리한다. 다른건 아래 부분이다. /api/** 가 아닌 URL의 경우엔 로그인 절차가 추가되었고, 인증/인가 실패시 동작이 별도의 구현체를 사용한다.
.formLogin((s) -> s.loginPage("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new LoginFailureHandler()).successHandler((request, response, authentication) -> {
response.sendRedirect("/dashboard");
}).loginProcessingUrl("/login_process"))
.logout((s) -> s.logoutUrl("/logout").logoutSuccessUrl("/login").permitAll())
.authenticationProvider(authenticationProvider).exceptionHandling((s) -> {
s.authenticationEntryPoint(new PageAuthenticationEntryPoint());
s.accessDeniedHandler(new PageAccessDeniedHandler());
})
WebSecurityConfigurerAdapter 와 기존 DSL을 사용한 구현과 비교했을 때, 새로운 구현 방식의 장점은 3개정도 있을 것 같다.
- 기존 메서드 체이닝을 사용했을 때보다 설정이 보다 더 분리되어 보이기 때문에 가독성이 향상된다.
- and() 메서드와 같이 다른 설정으로 전환하기 위한 메서드가 불필요하다.
- 보안 설정이 보다 더 유연해지고, Bean 주입을 통해 이루어지기 때문에 다른 구현과 통일성이 생긴다.
레퍼런스
https://github.com/spring-projects/spring-security/issues/10822
https://spring.io/blog/2019/11/21/spring-security-lambda-dsl
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
'개발 > 스프링' 카테고리의 다른 글
spring boot 3.2.2 전환기(5) - pom.xml 추가수정 (0) | 2024.04.13 |
---|---|
spring boot 3.2.2 전환기(4) - 타임리프 변경사항 (0) | 2024.03.25 |
spring boot 3.2.2 전환기(2) - Spring Web MVC 변경 (0) | 2024.03.23 |
Spring boot 3.2.2 전환기(1) - 버전 변경 (1) | 2024.03.23 |
코드 내에서 타임리프 파싱하기 (0) | 2022.06.12 |
- Total
- Today
- Yesterday
- JPA
- 티스토리챌린지
- Azure
- 탈세
- 부가가치세
- Request
- 안전신문고
- n+1
- 광군제
- 이륜차
- 전세사기
- 한국교통안전공단
- Thymeleaf
- springboot
- tomcat
- ouath2
- 포상금
- 오블완
- 알리익스프레스
- ORM
- 홈택스
- Spring
- 알리
- springboot3
- 현금영수증
- k베뉴
- Java17
- 토스페이
- java
- 공익제보단
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |