Spring Boot 3.x + Security 6.x 기본 설정 및 변화

Yoonhwan Kim·2023년 8월 15일
9

security

목록 보기
1/5
post-thumbnail
post-custom-banner

들어가기

Spring Boot3.x Version 으로 들어서면서 Minor Version 이 바뀌었다보니
확실히 생각보다 변화된 부분이 많습니다.

그 중에서도 제가 자주 설정하는 부분 중 하나인 Spring Security 부분에 대해 한번 알아보고자 작성하게 되었네요, 특히 관심을 가지는 프레임워크 이기도 하고, 최근에 했던 프로젝트에서 중추적으로 맡았던 부분이기 때문에 오히려 다른 기술에 비해서 더 알아보게 되었네요,

사실 업무적인 부분을 기술적 고민과 함께 풀어낸 글 외에는 작성하지 않기로 했는데,
Security 는 한번 작성을 해보고 싶었습니다,

Form 방식이 아닌 Rest API 방식의 처리에 대한 포스팅은 극소수니까요,
그래서 저는 이번에 변화된 Spring Security 3.x.x 버전에서의 Rest API 방식의 Security 를 조만간,, 작성해보려고 합니다. ( 안할수도,,, )

프로젝트 환경

  • Spring Boot 3.1.2
  • Spring Security 6.1.2
  • H2
  • Java 17

Github Repository


build.gradle

간단하게 의존성의 경우 H2, Security, Spring Web 을 추가하시면 됩니다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.2'
    id 'io.spring.dependency-management' version '1.1.2'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation 'org.springframework.security:spring-security-test'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

application.yml

spring:
  datasource:
    hikari:
      jdbc-url: jdbc:h2:mem://localhost/~/testdb;
    driver-class-name: org.h2.Driver
    username: sa
    password:

  h2:
    console:
      enabled: true
      path: /h2-console

본격적인 설정

Spring Boot 3.x.x 이전 설정 코드

기존 코드는 아래와 같이 Builder 형식으로 추가적인 설정을 해줬어야 했습니다.
.disable() 이 대표적인 예시라고 보면 됩니다.

Security 에서는 5.2.x 버전부터 Lambda DSL 방식으로 설정할 수 있도록 변경이 이루어진 적이 있습니다. 따라서, 예로 authorizeRequests() 에서 람다를 사용해서 표현할 수 있도록 되었습니다.

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;
    private final DataSource dataSource;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .csrf().disable()
                .formLogin().disable()
                .and().authorizeRequests()
                .antMatchers("/auth/**").authenticated()
                .and()

                .headers()
                .frameOptions()
                .sameOrigin()

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 정적 리소스 spring security 대상에서 제외
        return (web) -> {
            web
              .ignoring()
              .requestMatchers(
                  PathRequest.toStaticResources().atCommonLocations()
              );
        };
    }
}

Spring boot 3.x 이후 설정 코드

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            .csrf(AbstractHttpConfigurer::disable)
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(authorizeRequest ->
                    authorizeRequest
                            .requestMatchers(
                                    AntPathRequestMatcher.antMatcher("/auth/**")
                            ).authenticated()
                            .requestMatchers(
                                    AntPathRequestMatcher.antMatcher("/h2-console/**")
                            ).permitAll()
            )
            .headers(
                    headersConfigurer ->
                            headersConfigurer
                                    .frameOptions(
                                            HeadersConfigurer.FrameOptionsConfig::sameOrigin
                                    )
            );

        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 정적 리소스 spring security 대상에서 제외
        return (web) ->
                web
                    .ignoring()
                    .requestMatchers(
                            PathRequest.toStaticResources().atCommonLocations()
                    );
    }
}

Spring Boot 3.x.x 버전 즉, Security 6.1.x 버전 부터는 기존에 썼던 csrf() , formLogin() , headers() 와 같이 인수를 갖지 않는 메서드들은 전부 Deprecated 되었습니다.

Security 개발진 측에서 해당 방식의 메서드들이 가독성을 떨어트리는 부분이고, 설정에 대한 해석이 한눈에 들어오지 않는다는 판단을 통해 결정난 것입니다.

다음 코드의 내부를 보시면 알 수 있습니다.

  • 6.1.x 버전 이후로 Deprecated 했다.
  • 7.0 버전에서 삭제된다.
  • header(Customizer) 또는 Customizer.withDefaults() 를 사용해라.

즉, Lambda DSL 방식을 통한 설정으로 전체적인 코드를 바꿨다는 거고,
기본 옵션으로 사용하려면 매개변수에 Customizer.withDefaults() 를 쓰면 된다는 것이죠,


추가 설정 & 바뀐 설정 설명

h2 허용설정

설정하는 것에 집중하셨다면, 모르시겠지만 기존에 H2 를 사용해서 Security 를 연습하시던 분이라면 아실수도 있는데요. H2 를 쓰기 위해서는 아래의 코드가 필요합니다.

http
     .csrf().disable()
     .formLogin().disable() // 제외
     .and()

     .headers()
     .frameOptions()
     .sameOrigin()

CSRF 옵션과 SameOrigin 정책을 허용시켜서 iframe에 대한 접근이 허용되도록 설정이 있어야 Forbbiden(403) 이 나오지 않고 정상적으로 접근 및 테스트가 가능합니다.

만약 변화된 코드로 설정한다면 아래와 같이 할 수 있죠

 http
     .csrf(AbstractHttpConfigurer::disable)
     .headers(
           headersConfigurer ->
                    headersConfigurer
                           .frameOptions(
                                    HeadersConfigurer.FrameOptionsConfig::sameOrigin
                            )
                    )
			);

위처럼 설정을 해주시면 우선은 문제없이 h2 를 접근할 수 있지만,
특정 리소스에 대한 접근권한 설정이 이루어지면 추가 설정이 필요해집니다.
그 부분에 대한 설정은 아래에 계속..


권한 설정 코드

Boot 3.x 이전에는 antMatchers(...), mvcMatchers(...) 만 선언해서 특정 리소스에 대한 접근권한 설정을 줄 수 있었습니다.

http
    .csrf().disable()
    .formLogin().disable()
    .and()
    .authorizeRequests()
    .antMatchers("/auth/**").authenticated()

만약 위 구조를 Lambda DSL 로 변경하면 다음처럼 바뀌죠

.authorizeHttpRequests(authorizeRequest ->
                    authorizeRequest
                            .antMatcher("/auth/**").authenticated()
                     )

Boot 3.x 이후 부터는 antMatchers(), mvcMatchers() 가 없어지고 requestMatchers() 를 사용하도록 바뀌었습니다.

.authorizeHttpRequests(authorizeRequest ->
                    authorizeRequest
                            .requestMatchers("/auth/**").authenticated()
                     )

문제는 위와 같이 설정을 할 경우 예외가 발생합니다

Factory method 'filterChain' threw exception with message: This method cannot decide whether these patterns are Spring MVC patterns or not. If this endpoint is a Spring MVC endpoint, please use requestMatchers(MvcRequestMatcher); otherwise, please use requestMatchers(AntPathRequestMatcher).

해당 문제를 해결하기 위해서 AntPathRequestMatcher 클래스의 속성을 집어넣었습니다.

.authorizeHttpRequests(authorizeRequest ->
                    authorizeRequest
                            .requestMatchers(
                                  AntPathRequestMatcher.antMatcher("/auth/**")
                            ).authenticated()
                     )

권한 설정 + H2 문제

여태까지 작성된 코드들을 합쳐보겠습니다.


 http
     .csrf(AbstractHttpConfigurer::disable)
     .formLogin(AbstractHttpConfigurer::disable)
     .authorizeHttpRequests(authorizeRequest ->
                    authorizeRequest
                            .requestMatchers(
                                  AntPathRequestMatcher.antMatcher("/auth/**")
                            ).authenticated()
                     )
     .headers(
           headersConfigurer ->
                    headersConfigurer
                           .frameOptions(
                                    HeadersConfigurer.FrameOptionsConfig::sameOrigin
                            )
                    )
			);

현재의 설정은 이렇습니다.

  • CSRF 비활성화
  • FormLogin 비활성화
  • /auth/** 요청 권한필요
  • h2 접근을 위한 SameOrigin (프레임 허용) & 보안 강화 설정

결과는..?

이렇게 서버를 띄우고 h2 를 접근하면 Forbbiden이 발생한다.

우리가 프레임 허용을 위해서 SameOrigin 설정까지 완료했지만, 추가적인 권한 설정을 추가함으로써 갑작스럽게 403 이 발생했는데요, 왜일까요?

Security 에서 authrozieHttpRequest 를 (이전버전은 authorizeRequest) 설정하지 않으면 특정 페이지에 대한 접근문제를 발생하지 않지만, 설정을 하게 되면 설정한 요청 외의 다른 요청들은 모두 특별한 권한 설정이 없기 때문에 접근을 차단하게 됩니다.

결국 h2 접근을 위해서 sameOrigin() 옵션을 줬지만, 추가적인 권한 설정을 하게 된다면 /h2-console/** 경로의 권한설정을 추가적으로 해줘야 합니다.

.requestMatchers(         
		AntPathRequestMatcher.antMatcher("/h2-console/**").authenticated()
)

궁금한 점

  1. Spring boot 3.x 버전 이전에는 h2-console 의 경로에 대한 권한설정이 필요 없어도 접근이 가능한데 그 이유는 무엇일까?

    찾아봤으나, 3.x 버전 이후에는 자동설정 및 보안규칙에 대한 부분이 강화되어서 추가적인 보안 규칙을 정해줘야 접근이 되는 것 같다고 판단이 되고 있습니다.

  2. 우리는 로그인을 하지 않았습니다. 근데 왜 권한이 필요한 요청에서 UnAuthorized(401) 이 아니라 Forbbiden(403) 이 나올까?

    사실 여태 쓰면서 이 부분에 대한 생각을 왜 안해봤는지는 모르겠지만,
    기본적으로 Security 동작에서 로그인을 하지 않으면 인증 처리필터가 익명 사용자(anonymous) 권한을 적용합니다. 그렇기 때문에 권한에 대한 부분에서 예외가 발생하는 것입니다.

이후에는 Rest API 를 구현하는 정답 없는 과정을 보여드리려고 합니다.

post-custom-banner

4개의 댓글

comment-user-thumbnail
2023년 8월 15일

정리가 잘 된 글이네요. 도움이 됐습니다.

1개의 답글
comment-user-thumbnail
2024년 3월 21일

6.x 로 바뀌고 엄청 헤맸었는데, 쉬운 설명 감사합니다!!!

1개의 답글