Spring Security에서 WebSecurityConfigurerAdapter를 @Bean 기반 구성으로 변경한 이유
📌 서론
Spring Security는 스프링 서버를 구성하면서 보안을 적용하는 데 많이 사용된다.
특히, 요즘처럼 클라이언트 측 렌더링(Client-Side Rendering, CSR)을 많이 사용하는 환경에서 JWT(JSON Web Token)를 사용하여 인증 및 인가를 구현하는 애플리케이션에서 Spring Security는 매우 중요한 역할을 한다.
Spring Security 5.7부터는 시큐리티의 클래스 구성 방식이 기존과는 상당히 달라졌다. 이 글에서는 그 변화를 간단히 설명하고, 새로운 보안 구성 클래스의 작성 방법을 간단히 알아보도록 하자.
1. WebSecurityConfigurerAdapter의 등장과 역할
WebSecurityConfigurerAdapter
- 기존 Spring Security에서는 보안 구성을 위해 WebSecurityConfigurerAdapter 클래스를 상속하는 방식이 주로 사용되었다. 이 클래스는 보안 설정을 위한 여러 메서드를 제공하며, 개발자는 이 메서드를 오버라이드하여 보안 정책을 정의했다.
- 예를 들어, configure(HttpSecurity http) 메서드를 오버라이드하여 HTTP 요청에 대한 보안 규칙을 설정할 수 있었다.
- 이렇게 코드를 작성하는 방식은 간편하면서도 강력했지만, WebSecurityConfigurerAdapter를 상속하는 것이 필수적이었기 때문에 클래스 설계가 경직될 수 있다는 단점이 있다.
예시코드
- 하단의 예시코드는 실제로 동작하도록 하려면 조금 더 다듬고 수정 작업을 해야 한다. "이렇게 코드를 작성한다."라는 것을 표현한 것이기 때문에 그대로 가져다 사용하기에는 무리가 있다. 이해를 위해서만 확인하도록 하자.
- @Configuration 어노테이션을 사용하여 @Bean을 등록하는 클래스는 스프링이 자동으로 관리하는 빈 컨테이너의 일부가 된다. 이 클래스 내에서 정의된 메서드들은 자동으로 스프링 빈으로 등록되고, 등록하는 과정에서 빈에서 필요로 하는 의존성(빈)들은 스프링이 자동으로 주입한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomUserDetailsService customUserDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/member/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
return User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword())
.roles(userEntity.getRole()) // role 필드는 "ROLE_USER"와 같은 형태로 저장되어야 함
.build();
}
}
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT 토큰은 "Bearer [token]" 형태로 제공됨
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.warn("JWT Token has expired");
} catch (JwtException e) {
logger.error("JWT Token is invalid");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// 토큰을 받았고 사용자가 인증되지 않았으면, 인증을 진행
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 토큰이 유효한지 검사
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 컨텍스트에 인증 객체 설정
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
2. WebSecurityConfigurerAdapter의 폐기와 @Bean 기반 설정의 도입
@Bean 직접 등록 방식의 도입
- Spring Security 5.7 이후, WebSecurityConfigurerAdapter가 더 이상 권장되지 않으며, 공식적으로는 사용하지 않도록 권고된다. 대신, @Bean 어노테이션을 사용하여 보안 설정을 구성하는 방식이 도입되었다.
@Bean 등록의 장점
- 유연성 증가: 설정을 더 작은 단위로 나누어 관리할 수 있으며, 이를 조합하거나 조건에 따라 설정을 변경하기가 더 쉬워졌다.
- Spring의 다른 기능과의 통합 용이성: @Bean으로 정의된 보안 구성 요소들은 Spring의 IoC(제어의 역전) 컨테이너에서 관리되며, 다른 빈과 자연스럽게 결합할 수 있다.
- 클래스 설계의 단순화: 특정 클래스를 상속하지 않아도 되므로, 보안 설정을 정의하는 클래스가 더욱 간결해졌다.
예시코드
- @EnableWebSecurity 어노테이션은 Spring Security 5.7 이후로는 더 이상 필수적이지 않다.
- 이제는 상속받는 구조가 아니기 때문에 재정의(Override)할 필요가 없다. 필요한 설정들을 내가 찾아서 코드로 작성한 다음 @Bean으로 등록시켜 주면 된다.
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/member/**").permitAll()
.anyRequest().authenticated())
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
3. 왜 이러한 변화가 필요했을까?
구성의 단순화와 유연성 증대
- 이전 방식: WebSecurityConfigurerAdapter를 상속하여 보안 설정을 구성하는 방식은 개발자가 설정을 확장하거나 조정하는 데 유연성이 제한되었다. 스프링 문서에서는 이를 "상속을 통한 강제적인 코드 구조"로 인해 발생하는 제약사항으로 설명한다.
- 현재 방식: @Bean을 통한 구성은 보안 설정을 더 작은 단위로 나누어 관리할 수 있으며, 필요에 따라 쉽게 조합하거나 대체할 수 있다. 이는 Spring Security의 최신 구성 방법론에서 "컴포넌트 기반 설정"을 촉진하기 위해 도입되었다.
Spring의 일관성 유지
- Spring Framework는 지속적으로 XML 기반 설정에서 자바 기반 설정으로 발전해 왔으며, 이 과정에서 "컴포넌트 기반 설정"이 주요 원칙으로 자리 잡았다. 이는 스프링의 다른 모듈에서도 동일하게 적용되는 철학으로, 일관된 구성 패턴을 유지하기 위한 전략이다.
- 보안 설정을 빈으로 관리함으로써 다른 스프링 빈과의 상호작용이 자연스럽게 이루어지며, 애플리케이션 전체의 일관성과 효율성이 강화된다.
아래 글을 통해 어떻게 컴포넌트 기반 설정이 주요 원칙이 되었는지 진화 과정을 간단히 알아보자!
[Spring] 스프링 빈 설정의 진화: XML에서 자바, 그리고 컴포넌트 기반으로
스프링에서는 빈 등록 과정이 어떻게 발전되어 왔을까?📌 서론Spring Framework에서의 빈(bean) 설정 방식은 시간이 지나면서 점차 발전해 왔다.초기에는 XML 기반 설정이 주로 사용되었으나, 자바 기
curiousjinan.tistory.com
테스트와 유지보수의 용이성
- @Bean 방식은 각 구성 요소를 독립적으로 테스트할 수 있게 하며, 이는 코드의 유지보수성과 가독성을 향상시킨다.
- 공식 문서에서는 WebSecurityConfigurerAdapter 방식이 상속 구조로 인해 테스트와 유지보수가 복잡해질 수 있다고 명시되어 있으며, 이를 해결하기 위해 모듈화 된 @Bean 기반 설정이 도입되었다고 설명한다.
참고자료 (공식 가이드 문서)
WebSecurityConfigurerAdapter (spring-security-docs 5.7.0-M2 API)
Provides a convenient base class for creating a WebSecurityConfigurer instance. The implementation allows customization by overriding methods. Will automatically apply the result of looking up AbstractHttpConfigurer from SpringFactoriesLoader to allow deve
docs.spring.io
Java Configuration :: Spring Security
Spring Security’s Java Configuration does not expose every property of every object that it configures. This simplifies the configuration for a majority of users. Afterall, if every property was exposed, users could use standard bean configuration. While
docs.spring.io
'Spring > Spring Security' 카테고리의 다른 글
스프링 시큐리티에서 SecurityContext를 비동기 스레드에 전파하는 방법 (0) | 2025.01.12 |
---|---|
Spring Security6 - Authentication(인증) (1) | 2023.10.21 |
Spring Security와 CORS: 크로스 도메인 요청 처리 기법 알아보기 (0) | 2023.10.21 |
Spring Boot 3.1 & Spring Security 6: Security Config 최적화 리팩토링 (12편) (0) | 2023.09.04 |
Spring Boot 3.1 & Spring Security 6: JWT 검증 리팩토링 (11편) (2) | 2023.09.04 |