티스토리 뷰

과거 - WebSecurityConfigurerAdapter 

 

  과거 spring security에서는 인증/인가 관련 설정을 위해 WebSecurityConfigurerAdapter 클래스를 상속받아 configure 메서드를 구현해 설정하도록 만들어놨었다. 코드로는 대충 아래와 같이 표현할 수 있다. 

@Configuration
@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 를 정의하는 방식이다. 

@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());


}

 

이러한 설정 방식에 대해서 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개정도 있을 것 같다. 

  1.  기존 메서드 체이닝을 사용했을 때보다 설정이 보다 더 분리되어 보이기 때문에 가독성이 향상된다.
  2. and() 메서드와 같이 다른 설정으로 전환하기 위한 메서드가 불필요하다. 
  3. 보안 설정이 보다 더 유연해지고, 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

 

Deprecate WebSecurityConfigurerAdapter · Issue #10822 · spring-projects/spring-security

With the following issues closed we've added the ability to configure the security of an application without needing the WebSecurityConfigurerAdapter. Related issues: #8804 #8978 #10040 #10138 Back...

github.com

 

 

Spring Security - Lambda DSL

Overview of Lambda DSL The release of Spring Security 5.2 includes enhancements to the DSL, which allow HTTP security to be configured using lambdas. It is important to note that the prior configuration style is still valid and supported. The addition of l

spring.io

 

 

Spring Security without the WebSecurityConfigurerAdapter

In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration. To assist with the transition to this new style of configuration, we have compiled a list of common

spring.io

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함