Spring Boot를 사용하면 별다른 설정 없이도 기본적인 로그인 페이지와 보안 기능이 자동으로 활성화됩니다. 정말 편리하지만, 실제 서비스를 개발하려면 "관리자만 이 페이지에 접근해야 해", "API 요청은 반드시 인증을 거쳐야 해" 와 같은 우리만의 규칙이 필요합니다.
오늘은 자동 설정의 편리함에서 벗어나, Spring Security를 입맛에 맞게 제어하는 첫걸음을 떼보려 합니다.
나만의 보안 설정을 시작하기 위한 단 하나의 규칙이 있다면, 바로 SecurityFilterChain 타입의 빈을 최소 하나 이상 직접 등록하는 것입니다.
우리가 SecurityFilterChain 빈을 직접 등록하는 순간, Spring Boot가 제공하던 편리한 자동 보안 설정은 비활성화됩니다. 만약 아무런 SecurityFilterChain 빈을 등록하지 않는다면, 바로 이 자동 설정이 동작하여 기본 보안 기능이 적용됩니다.
과정은 매우 간단합니다.
1단계: 설정 클래스 생성 (@EnableWebSecurity)
먼저 @Configuration 어노테이션을 붙인 설정 클래스를 만들고, 클래스 위에 @EnableWebSecurity 어노테이션을 붙여줍니다. 이 어노테이션이 바로 "이제부터 Spring Security의 웹 보안 기능을 내가 직접 설정하겠다"라고 선언하는 스위치입니다.
2단계: SecurityFilterChain 빈과 HttpSecurity 주입
클래스 내부에 SecurityFilterChain을 반환하는 메서드를 만들고 @Bean으로 등록합니다. 이때 파라미터로 HttpSecurity 객체를 의존성 주입받는 것이 핵심입니다.
HttpSecurity는 보안 설정을 위한 일종의 만능 도구(DSL)입니다. Spring Security가 기본적으로 11개 이상의 필터를 적용해주지만, HttpSecurity를 사용하면 이 기본 동작에 더해 우리만의 규칙을 자유롭게 추가하고 변경할 수 있습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 3단계: HttpSecurity로 상세 규칙 정의하기
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated() // 모든 요청은 인증을 받아야 함
);
// 4단계: 설정 기반으로 SecurityFilterChain 빌드 및 반환
return http.build();
}
}
3단계: http.build()
모든 규칙 설정이 끝났다면, 마지막에 http.build()를 호출하여 지금까지 정의한 내용을 바탕으로 SecurityFilterChain 객체를 생성하고 반환하면 모든 과정이 끝납니다.
Customizer란?위 코드에서 .authorizeHttpRequests(auth -> ...) 와 같은 람다식을 보셨을 겁니다. 여기서 사용되는 것이 바로 Customizer 인터페이스입니다. Customizer는 특정 설정(여기서는 authorizeHttpRequests)에 대한 세부 구성을 더 쉽게 할 수 있도록 Spring Security가 제공하는 함수형 인터페이스입니다. 덕분에 우리는 더 깔끔하고 직관적인 코드로 복잡한 설정을 할 수 있습니다.
나만의 보안 규칙을 만들었으니, 이제 로그인을 테스트할 사용자가 필요합니다. Spring Security는 두 가지 간단한 방법으로 메모리상에 사용자를 추가하는 기능을 제공합니다.
방법 1: application.yml 파일에 정의
가장 간단한 방법입니다. application.yml (또는 .properties) 파일에 사용자 이름, 비밀번호, 역할을 직접 명시할 수 있습니다. 간단한 테스트에 매우 유용합니다.
spring:
security:
user:
name: user
password: 1111
roles: USER
방법 2: Java 설정 클래스에 직접 정의
조금 더 유연한 방법은 설정 클래스에 UserDetailsService 타입의 빈을 직접 등록하는 것입니다.
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}1111") // {noop}은 암호화 없이 텍스트 그대로 사용하겠다는 의미
.roles("USER")
.build();
// 사용자를 메모리에 저장하여 관리
return new InMemoryUserDetailsManager(user);
}
InMemoryUserDetailsManager는 사용자를 메모리에 저장하는 가장 단순한 구현체입니다. 실제 애플리케이션에서는 DB에서 사용자 정보를 가져오는 구현체로 교체하게 될 것입니다.