Spring Security 기본 설정, 커스텀

mainimg.jpg

기본 필터체인

SpringBootWebSecurityConfiguration.class을 확인하면 다음과 같이 기본 필터체인이 등록되었음을 확인할 수 있다.

@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
    SecurityFilterChainConfiguration() {
    }

    @Bean
    @Order(2147483642)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> {
            ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl)requests.anyRequest()).authenticated();
        });
        http.formLogin(Customizer.withDefaults());
        http.httpBasic(Customizer.withDefaults());
        return (SecurityFilterChain)http.build();
    }
}
  • http.authorizeHttpRequests : 이 구문은 해당 필터체인의 인가 규칙을 정의한다.

    • ((TypeCasting)requests.anyRequest()).authenticated() : 모든 요청에 대해 인증이 필요하다고 설정한다.
    • ⚠️ 최신 스프링 시큐리티에서는 TypeCasting 없이 사용할 수 있도록 개선됐다. -> requests -> {requests.anyRequest().authenticated()}
  • http.formLogin : 폼 기반 로그인을 설정한다. 프론트엔드와 백엔드가 나뉘어 http로 유저네임과 비밀번호를 전송하는 경우에는 사용하지 않는다.

  • http.httpBasic : http 기본 인증을 설정한다. 이 설정은 http 헤더를 통해 인증을 수행할 수 있도록 한다. -> 이는 JWT의 그것과 다르다. 자세한 내용은 아래 참조.

    HTTP Basic?

    HTTP Basic 인증 과정

    1. 클라이언트 요청:
      • 클라이언트는 보호된 리소스에 접근하기 위해 서버에 HTTP 요청을 보냅니다. 이때 클라이언트는 아직 인증되지 않은 상태입니다.
    2. 서버의 인증 요구:
      • 서버는 클라이언트가 인증되지 않았음을 인지하고, 401 Unauthorized 상태 코드와 함께 WWW-Authenticate: Basic 헤더를 포함한 응답을 반환합니다. 이 응답은 클라이언트에게 사용자명과 비밀번호를 제공하라고 요청합니다.
    3. 클라이언트의 인증 정보 제공:
      • 클라이언트는 사용자명과 비밀번호를 username:password 형식의 문자열로 결합한 후, Base64로 인코딩합니다.
      • 클라이언트는 인코딩된 자격 증명을 Authorization: Basic {encoded_credentials} 헤더에 포함하여 서버에 다시 요청을 보냅니다. (이 부분은 Bearer를 사용하는 JWT와 대조적)
    4. 서버의 인증 정보 검증:
      • 서버는 요청 헤더에서 인코딩된 자격 증명을 추출하고, 이를 디코딩하여 사용자명과 비밀번호를 확인합니다.
      • 서버는 이 정보를 바탕으로 사용자가 유효한지 검증합니다.
    5. 응답:
      • 자격 증명이 유효한 경우, 서버는 요청된 리소스에 접근을 허용하고, 정상적인 응답을 반환합니다.
      • 자격 증명이 유효하지 않은 경우, 서버는 다시 401 Unauthorized 응답을 반환합니다.

    HTTP Basic 인증의 장점과 단점

    장점:

    • 구현이 매우 간단합니다.
    • 별도의 세션 관리가 필요 없습니다.
    • HTTP 프로토콜 표준의 일부로, 대부분의 HTTP 클라이언트와 서버가 지원합니다.

    단점:

    • 자격 증명이 Base64로 인코딩되어 전송되지만, 이는 단순한 인코딩일 뿐 암호화가 아닙니다. 따라서 네트워크 상에서 쉽게 탈취될 수 있습니다.
    • HTTPS를 사용하지 않으면 자격 증명이 평문으로 전송되는 것과 같습니다.
    • 매 요청마다 자격 증명을 포함하여 전송해야 하므로, 비효율적입니다.
  • http.build : HttpSecurity 객체 http를 빌드하여 SecurityFilterChain으로 반환한다. 최신 버전에서는 앞의 타입 캐스팅을 생략할 수 있다.

기본 필터체인 (최신)

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {

    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.formLogin(withDefaults());
        http.httpBasic(withDefaults());
        return http.build();
    }

}

커스텀 필터체인 등록하기

@Configuration
public class ProjectSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.formLogin(withDefaults());
        http.httpBasic(withDefaults());
        return http.build();
    }
}

ProjectSecurityConfig 클래스를 지정하고 @Configuration 어노테이션을 달아주었다. 그 안에 빈으로 커스텀 필처 체인을 등록했는데, 이는 아까 본 기본 필터체인을 그대로 복붙한 뒤 @Order어노테이션만 제거해준 것이다. 이제부터 시리즈의 포스팅에서는 이 커스텀 필터체인을 가지고 놀아볼 것이다. 이렇게 커스텀 필터체인을 등록해주면, 기존에 동작하던 기본 필터체인은 더 이상 동작하지 않게 된다.

허용 URL과 차단 URL 설정하기

@Configuration
public class ProjectSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requests ->
                requests.requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
                        .requestMatchers("/notices", "/contact").permitAll()
                        .anyRequest().authenticated()
        )
                .formLogin(withDefaults())
                .httpBasic(withDefaults());
        return http.build();
    }
}
  • Spring Security 6.1 버전 이후로 람다 DSL 스타일이 권장되어 위와 같이 설정했다.
  • "/myAccount", "/myBalance", "/myLoans", "/myCards" 경로에 대해서는 인증을 요구한다.
  • "/notices", "/contact" 경로에 대해서는 기본적으로 허용한다.
  • anyRequest() 구문을 통해 명시되지 않은 다른 모든 경로는 차단한다.

인메모리 유저 추가하기

@Bean
public InMemoryUserDetailsManager userDetailsService() {
    UserDetails admin = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("12345")
            .authorities("admin")
            .build();
    UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("12345")
            .authorities("read")
            .build();
    return new InMemoryUserDetailsManager(admin, user);
}
  • 권한에 따라 여러 유저가 필요할 수도 있다. 그럴 때는 Security Config 클래스 내부에 위와 같이 유저 정보를 추가해주면 된다.
  • 위 내용이 빈으로 등록되어 adminuser 두 개의 계정이 생성된다.
  • 이 방법은 PasswordEncoder로 DefaultPasswordEncoder를 사용하여, 운영 환경에는 적합하지 않다.

다른 방법

@Bean
public InMemoryUserDetailsManager userDetailsService() {
    UserDetails admin = User
            .withUsername("admin")
            .password("12345")
            .authorities("admin")
            .build();
    UserDetails user = User
            .withUsername("user")
            .password("12345")
            .authorities("read")
            .build();
    return new InMemoryUserDetailsManager(admin, user);
}

@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
  • 계정을 만들 때 따로 패스워드 인코더를 지정하는 대신 NoOpPasswordEncoder의 싱글톤 인스턴스를 빈으로 등록하는 방법