필자는 최근에 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
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
이 새로운 스타일의 설정을 돕기 위해, 우리는 흔한 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 설정을 하도록 유도한다고 해도 설정하는 부분이 그렇게 뭐 크게 달라진것은 없다.