Spring/Spring Security

[Spring Security] [번역]Spring Security without the WebSecurityConfigurerAdapter / Lambda DSL

공대키메라 2022. 8. 8. 22:16

필자는 최근에 Spring Security를 많이 보는데 Spring Security 5.7.0 이상 버전에서는 WebSecurityConfigurerAdapter 가 Deprecated되었기에 이에 대한 정보를 찾고 있었다.

 

그러던 중 우연히 Spring Blog 에서 이에 데해 설명하는 글을 읽었고, 이에 대한 내용을 소개하고자 한다.

 

이에 더해, 기존의 Spring Security 의 configuration중 lambda DSL이라는 기능을 추가했다고 하니 이것도 알아볼 것이다. 

 

참고한 내용들의 주소는 다음과 같다. 

 

출처:

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

https://spring.io/blog/2019/11/21/spring-security-lambda-dsl

https://www.codejava.net/frameworks/spring-boot/fix-websecurityconfigureradapter-deprecated

 

1. Spring Security - Lambda DSL

ENGINEERING | ELEFTHERIA STEIN-KOUSATHANA | NOVEMBER 21, 2019
 
spring security 5.2 release는 DSL 로 성능 개선을 포함하는데, 이것은 HTTP security를 lambda를 이용해서 설정할 수 있도록 돕는다.
 
더 중요한 설정 스타일이 여전히 확실하고 지원되는 것을 아는 것이 중요하다. (lambda 이전의 설정 방식도 그대로 있다!)
lambda의 추가는 더많은 유연성을 제공하도록 의도되었지만, 그들의 사용은 선택적이다.
 
Spring Security 문서나 샘플에서 이러한 스타일의 설정을 아마 본 적 있을 수 있다.

 

1.1 Configuration using lambdas

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .antMatchers("/blog/**").permitAll()
                    .anyRequest().authenticated()
            )
            .formLogin(formLogin ->
                formLogin
                    .loginPage("/login")
                    .permitAll()
            )
            .rememberMe(withDefaults());
    }
}

 

1.2 Equivalent configuration without using lambdas

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/blog/**").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .rememberMe();
    }
}

 

이걸 본 나는 딱 든 생각이 글쎄... 이거 도입을 하나 안하나 별 다른게 없는데... 하는 생각이 들었다... 굳이...? 
잘 모르겠는데... 쓰흡...

 

1.3 Lambda DSL configuration tips

위의 두 샘플을 비교할 때, 중요한 차이를 알았을 것이다. 

 

  - Lambda DSL에서 and() 메소드를 이용해서 설정 옵션을 연결할 필요가 없다. 

    HttpSecurity instance는 자동적으로 lambda method의 호출 후에 더 많은 설정을 위해 자동적으로 반환된다.

 

  - withDefault()는 spring 특징들이 Spring Security에 의해 제공되는 defaults를 사용할 수 있도록 한다.

    이것은 lambda expression 인 it -> {}의 줄임이다.  

 

 

1.4 WebFlux Security

  - 또한 WebFlux security 를 비슷한 방식으로 lamda를 사용해서 설정할 수 있다.

 

@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange(exchanges ->
                exchanges
                    .pathMatchers("/blog/**").permitAll()
                    .anyExchange().authenticated()
            )
            .httpBasic(withDefaults())
            .formLogin(formLogin ->
                formLogin
                    .loginPage("/login")
            );
        return http.build();
    }
}

 

1.5 Goals of the Lambda DSL

 

Lambda DSL은 다음 목적을 성취하기 위해 만들어졌다.

 

   - 자동적인 들여쓰기가 설정을 더 잘 읽히게 한다.

 

   - and()를 사용한 옵션 설정이 필요 없다.

 

   - Spring Security DSL은 Spring Integration과 Spring Clous Gateway같은 다른 Spring DSL과 비슷한 설정 스타일을 가진다.

 

다 읽어보니 글쎄... 약간 갸우뚱 하다. (알쏭달쏭 긴가민가 갸우뚱)

 

2. Spring Security without the WebSecurityConfigurerAdapter

ENGINEERING | ELEFTHERIA STEIN-KOUSATHANA | 2022년 2월 21일
 
Spring Security 5.7.0 M2 에서 우리는 사용자들이 component-based security configuration사용을 장려하기 위해서 WebSecurityConfigurerAdapter를 deprecated 했다. (너무해... ㅠㅠ)
 

이 새로운 스타일의 설정을 돕기 위해, 우리는 흔한 use-case들의 리스트를 complie해왔고, 대안사항을 제안해왔다.

 

아래 예시들은 우리가 우리의 권한 규칙들을 정하기 위해 SPring Security의 lambda DSL 과 method HttpSecucrity#authorizaHttpRequests를 사용함으로써 최고의 연습을 제공한다.

 

만약 당신이 lambda DSL에 처음이면 이 post를 읽어봐라.(이 읽어보라는 post 내용이 위에서 설명한 lambda DSL의 번역 내용...)

 

만약 우리가 왜 HttpSecucrity#authorizaHttpRequests를 사용하려고 하는지 더 알고 싶다면, 여기를 확인해라.(사실 이게 다음에 공부할 내용임. 이 글의 포스트에서는 어떻게 사용했는지만 확인하자.)

 

2.1 Configuring HttpSecurity

Spring Security 5.4에서 우리는 SecurityFilterChain 빈을 생성해서 HttpSecurity를 설정하는 방법을 도입했다.

 

아래는 HTTP Basic을 가진 모든 endpoints를 보호하는 WebSecurityConfigureAdapter를 사용하는 설정 예시이다.

 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
    }

}

 

앞으로, 이것을 하기에 추천되는 방식은 SecurityFilterChain bean을 등록하는 것이다.

 

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authz) -> authz
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());
        return http.build();
    }

}

 

2.2 Configuring WebSecurity

Spring Security 5.4에서 우리는 또 WebSecurityCustomizer를 도입했다.

 

WebSecurityCustomizer는 WebSecurity를 cusmize하기 위해 사용되는 callback interface이다. 

 

아래는 /ignore1과 /ignore2를 매치하는 요청을 무시하는 WebSecurityConfigureAdapter를 사용하는 설정 예시다.

 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/ignore1", "/ignore2");
    }

}

 

앞으로, 이것을 하는 추천되는 방식은 WebSecurityCustomizer 빈을 등록하는 것이다. 

 

@Configuration
public class SecurityConfiguration {

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
    }

}

 

주의: 만약 요청들을 무시하는 WebSecurity설정을 하고 있다면, HttpSecurity#authorizeHttpRequest를 이용해서 permitAll을 사용하는것을 고려해라. 더 자세한 내용을 위해 Javadoc 을 참고하라.

 

2.3 LDAP Authentication

Spring Security 5.7에서 우리는 EmbeddedLdapServerContextFactoryBean, LdabBindAuthenticastionManagerFactory와  LdapPasswordComparisonAuthenticationManagerFactory를 도입했다. (이것은 내장된 LDAP server와 LDAP 인증을 수행하는 AuthenticationManager를 생성하는 데 사용한다.)

 

아래는 내장된 LDAP server와 LDAP 인증을 수행하는 AuthenticationManager를 생성하는WebSecurityConfigurerAdapter를 사용하는 설정 예시다.

 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .ldapAuthentication()
            .userDetailsContextMapper(new PersonContextMapper())
            .userDnPatterns("uid={0},ou=people")
            .contextSource()
            .port(0);
    }

}

 

앞으로, 이것을 사용하는 추천된 방식이 새로운 LDAP 클래스들을 사용하는것이다.

 

@Configuration
public class SecurityConfiguration {
    @Bean
    public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
        EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean =
            EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
        contextSourceFactoryBean.setPort(0);
        return contextSourceFactoryBean;
    }

    @Bean
    AuthenticationManager ldapAuthenticationManager(
            BaseLdapPathContextSource contextSource) {
        LdapBindAuthenticationManagerFactory factory = 
            new LdapBindAuthenticationManagerFactory(contextSource);
        factory.setUserDnPatterns("uid={0},ou=people");
        factory.setUserDetailsContextMapper(new PersonContextMapper());
        return factory.createAuthenticationManager();
    }
}

 

2.4 JDBC Authentication

아래는 default 스키마를 초기화되고 하나의 사용자를 가진 내장된 datasource를 가진 WebSecurityConfigurerAdapter를 사용하는 설정 예시다. 

 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        auth.jdbcAuthentication()
            .withDefaultSchema()
            .dataSource(dataSource())
            .withUser(user);
    }
}

 

이것을 하는 추천되는 방식은 JdbcUserDetailsManager  빈을 등록하는 것이다. 

 

@Configuration
public class SecurityConfiguration {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
            .build();
    }

    @Bean
    public UserDetailsManager users(DataSource dataSource) {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
        users.createUser(user);
        return users;
    }
}

 

이 예제에서, 우리는 method User.withDefaultPasswordEncoder()를 가독성을 위해 사용한다. 이것은 생산성을 위해 의도되지 않았고, 대신에 우리는 외부적으로 당신의 password를 hashing하는것을 추천한다.

 

이것을 위한 한 방식은 Spring Boot CLI를 사용하는 것이다. (여기)

 

2.5 In-Memory Authentication

아래는 하나의 사용자를 가진 in-memory 사용자를 설정하는 WebSecurityConfigurerAdapter를 사용하는 설정 예시다.

 

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        auth.inMemoryAuthentication()
            .withUser(user);
    }
}

 

이것을 하는 추천되는 방식은 InMemoryUserDetailsManager 빈을 등록하는 것이다. 

 

@Configuration
public class SecurityConfiguration {
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
        return new InMemoryUserDetailsManager(user);
    }
}

 

이 예제에서, 우리는 method User.withDefaultPasswordEncoder()를 가독성을 위해 사용한다. 이것은 생산성을 위해 의도되지 않았고, 대신에 우리는 외부적으로 당신의 password를 hashing하는것을 추천한다.

 

이것을 위한 한 방식은 Spring Boot CLI를 사용하는 것이다. (여기)

 

(위에 이미 나온 말이라서 잠시 당황함.ㅋㅋ...)

 

2.6 Global AuthenticationManager

전체 application에서 사용할 수 있는 AuthenticationManager를 생성하기 위해, 단순하기 AuthenticationManager를 @Bean으로 등록할 수 있다.

 

2.7 Local AUthenticationManager

Spring Security 5.6에서 HttpSecurity@authenticationManager 메소드를 도입했는데 이것은 구체적인 SecurityFilterChain을 위해 default AutheticationManager를 오버라이드한다.

 

2.7 Accessing the local AuthenticationManager

local AuthenticationManager는 custom DSL에서 접근될 수 있다.

 

이것은 실제로 HttpSecurity.authorizeRequests() 같은 메소드를 구현하는 방식이다.

 

public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        http.addFilter(new CustomFilter(authenticationManager));
    }

    public static MyCustomDsl customDsl() {
        return new MyCustomDsl();
    }
}

 

 

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // ...
    http.apply(customDsl());
    return http.build();
}

 


 

이번 시간에는 spring security에서 deprecated되어버린 것과 그 의도를 알았고, 기존에 사용하던 코드를 어떻게 변경하면 되는지를 읽어보았다.

 

spring 에서 아예 component 기반의 security 설정을 하도록 유도한다고 해도 설정하는 부분이 그렇게 뭐 크게 달라진것은 없다.