[Spring] 스프링 시큐리티 설정이 @Bean 기반 구성으로 바뀐 이유

2024. 8. 23. 13:56·Spring/Spring Security
반응형
 
 
 

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
'Spring/Spring Security' 카테고리의 다른 글
  • 스프링 시큐리티에서 SecurityContext를 비동기 스레드에 전파하는 방법
  • Spring Security6 - Authentication(인증)
  • Spring Security와 CORS: 크로스 도메인 요청 처리 기법 알아보기
  • Spring Boot 3.1 & Spring Security 6: Security Config 최적화 리팩토링 (12편)
Stark97
Stark97
문의사항 또는 커피챗 요청은 링크드인 메신저를 보내주세요! : https://www.linkedin.com/in/writedev/
  • Stark97
    오늘도 개발중입니다
    Stark97
  • 전체
    오늘
    어제
    • 분류 전체보기 (240) N
      • 개발지식 (20)
        • 스레드(Thread) (8)
        • WEB, DB, GIT (3)
        • 디자인패턴 (8)
      • JAVA (21)
      • Spring (88)
        • Spring 기초 지식 (35)
        • Spring 설정 (6)
        • JPA (7)
        • Spring Security (17)
        • Spring에서 Java 활용하기 (8)
        • 테스트 코드 (15)
      • 아키텍처 (5)
      • MSA (14)
      • DDD (7) N
      • gRPC (9)
      • Apache Kafka (18)
      • DevOps (23)
        • nGrinder (4)
        • Docker (1)
        • k8s (1)
        • 테라폼(Terraform) (12)
      • AWS (32)
        • ECS, ECR (14)
        • EC2 (2)
        • CodePipeline, CICD (8)
        • SNS, SQS (5)
        • RDS (2)
      • notion&obsidian (3)
  • 링크

    • notion기록
    • 깃허브
    • 링크드인
  • hELLO· Designed By정상우.v4.10.0
Stark97
[Spring] 스프링 시큐리티 설정이 @Bean 기반 구성으로 바뀐 이유
상단으로

티스토리툴바