SpringSecurity – 실패한 DSL(javaconfig)

원래 스프링시큐리티는 구조가 단순하다.

Filter가 하나 등록된다

public class FilterChainProxy extends GenericFilterBean

public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator

public interface SecurityFilterChain

ChainOfResponsibility패턴으로 구현된 FilterChain….

그리고 이 Chain에서 순차적으로 Filter를 실행시킨다.으로 인증필터 몇개 넣어준 다음
Request들어올 때 마다 반복문 돌려서 주소패턴 match되면 처리해주는거

  1. WebAsyncManagerIntegrationFilter
  2. SecurityContextPersistenceFilter
  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter
  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. BasicAuthenticationFilter
  10. RequestCacheAwareFilter
  11. SecurityContextHolderAwareRequestFilter
  12. AnonymousAuthenticationFilter
  13. SessionManagementFilter
  14. ExceptionTranslationFilter
  15. FilterSecurityInterceptor
https://www.javadevjournal.com/spring-security/spring-security-filters/
https://docs.spring.io/spring-security/reference/6.0.0-M3/servlet/architecture.html#servlet-filterchainproxy

대강 앞에서 막아줄거 막고 -> Authentication -> Principal threadlocal에 쑤셔박기 -> 꺼내서 Authorization하는 구조

좀 복잡한 필터가 몇개 있는데 사실 간단하게 구현해도 되는 부분이 많다.
UsernamePasswordAuthenticationFilter 등등 몇가지에서 너무 많은걸 자동으로 해주려다 보니 과도하게 복잡해진게 있긴한데.. 그래도 쓸만했었다

DSL이 나오기 전까지는…

프레임워크 자체도 조금 개선이 필요하다고 생각하는데
(UserDetails에 기본User를 생성해놓는다던가…하는 구현체를 너무 많이 만들어놓는것)
그래도 꽤 좋았는데

Java Config DSL에다가 Springboot Autoconfiguration까지 들어가니까…

또 스프링에서 자동으로 해주겠다고 하다가 망한 케이스

bean을 하나하나 설정 할 때는 혼란이 없었다.

설정 해 놓은대로
순차적으로 설정된 필터가 몇개인지
어떤게 설정되어 있는지 보였으니까
오류가 발생하면 어디서 발생했는지 이름만 봐도 바로 알 수 있었다.

그런데 JavaConfig는?

이게 몇년전쯤 쓰던 SpringSecurity 자바설정파일인데 이걸 보고 뭐가먼저 동작할지 예상이나 할 수 있을까


@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
	http
			.csrf().disable()
//좆같은거 추가
		.anonymous().and().anonymous().and().csrf().and()
			.authorizeRequests()
			//resource
			.antMatchers("/favicon.ico", "/vendor/**", "/static/**").permitAll()
			.antMatchers("/", "/home", "/join").permitAll()
			.antMatchers("/console/*").permitAll()
			.antMatchers("/user/join").anonymous()
			.anyRequest().authenticated()
			.and()

			.headers()
			.addHeaderWriter(new StaticHeadersWriter("X-Content-Security-Policy", "script-src 'self'"))
			.frameOptions().disable()
			.and()

			.formLogin()
			.loginPage("/login")
			.permitAll()

			.and()

			.logout()
			.permitAll();
}

	@Bean
	public DbUserDetailsService userDetailsService() {
		return new DbUserDetailsService();
	}

	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	public DaoAuthenticationProvider authenticationProvider() {
		DaoAuthenticationProvider bean = new DaoAuthenticationProvider();
		bean.setUserDetailsService(userDetailsService());
		bean.setPasswordEncoder(passwordEncoder());
		return bean;
	}

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth
				.authenticationProvider(authenticationProvider());
//				메모리 로그인으로 적용시 사용.
//				.inMemoryAuthentication()
//				.withUser("user").password("password").roles("USER");
	}
}

내 코드에서 이정도 설정을 했지만
WebSecurityConfigurerAdapter에서 또 뭔가 존나게 해놨다

이 설정을 보면 /login이 로그인 페이지로 간다는건 대강 눈치챌 수 있지만 몇번째 필터에서 돌아가는지 알 수 있을까? 문제가 발생하면 디버깅이나 로그를 존나 훑어봐야알 수 있게 된다.
스프링 시큐리티에서는 이런식으로 문제가 발생한다.

XML 도 마찬가지

갑자기 생각 해 보니 java config가 개판이 된게 XML설정을 JavaConfig화 해서 그랬던 것 같다. 진짜 문제는 javaconfig가 아닌 설정방법 그 자체였던듯

이런식으로 설정하는건데…
그냥 튜토리얼 쓸때는 괜찮은데 커스텀 하기 시작하면 bean을 하나하나 하는게 낫다.

<?xml version="1.0" encoding="UTF-8"?> 
<beans:beans xmlns="http://www.springframework.org/schema/security" 
xmlns:beans="http://www.springframework.org/schema/beans" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xsi:schemaLocation="http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans.xsd 
http://www.springframework.org/schema/security 
http://www.springframework.org/schema/security/spring-security.xsd"> 
<http auto-config="true"> 
<intercept-url pattern="/admin"
access="hasRole('ROLE_ADMIN')" /> </http> 
<authentication-manager> 
<authentication-provider>
~~~

결론?

스프링시큐리티는 수동으로 하나하나 설정해서 쓰자
어차피 한번 만들어놓으면 계속 쓰니까
이미 JavaConfig(DSL)로 선언 해 놓은것들도 다 하나하나 생성하는 방식으로 만들어야된다고 생각한다.

(간단하게 쓰려면 그냥 DSL을 써도 괜찮음)

https://docs.spring.io/spring-security/reference/6.0.0-M3/servlet/architecture.html#servlet-filterchainproxy

Kotlin 때문에 드러난 SpringFramework DTO Validation의 문제점

발견

  • Kotlin: Validation-Mapping
  • Java: Mapping-Validation

기존에 Java만 사용 할 때는 아무 문제가 없었다
자바 dto는 null이건 뭐건 상관이 없었으니까
일단 값을 처넣은(Mapping) 다음에 Validation을 한다

@Data
class PersonRequestDto {
    @NotEmpty
    @Size(min=1, max=30)
    private String name;
    @NotNull
    @Min(15)
    private Int age;
    @NotNull
    private String phone;
}
data class PersonRequestDto(
    @NotEmpty
    @Size(min=1, max=30)
    val name: String,
    @NotNull
    @Min(15)
    val age: Int,
    @NotNull
    val phone: String?,
)

여기서 코틀린을 자바처럼 하면 어떨게 될까?

오류가 난다
NotNull이 다 무의미해진다
Lombok의 @NonNull을 쓰는것처럼 변수 대입자체가 안되니까

그럼 어차피 안 들어가니까 상관없는거 아닌가?? 라고생각할 수 있지만
validation result 자체를 활용하려고 하거나 전체값의 데이터를 다 확인하고 싶은경우에 그게 불가능하다

해결방안

일단 현재로써는 없다

스프링에서 Validation을mapping 전에 해야하는데
이걸 바꿀 수 있을까….
힘들어보이는데

다른 프레임워크의 경우에는 request body에서validation을 처리하면 되지 않으려나

DTO에 설정되어 있는 field, annotation 스캔해서
json을 validation

코틀린에서 조금 부족하지만 쓸만한 벨리데이션툴이 있다

  • https://github.com/rcapraro/kalidation
  • https://github.com/konform-kt/konform