Spring Boot Security 자동 설정 끄기와 SecurityFilterChain 설정 (Boot 3·4)
by Jeongjin Kim
Spring Boot에 Security Starter를 추가했더니 모든 요청이 로그인 화면으로 이동하거나 401 Unauthorized를 반환하는 경우가 있습니다. 이때 바로 SecurityAutoConfiguration을 제외하기보다는 기본 웹 보안을 대체하려는 것인지, 자동 생성 사용자를 없애려는 것인지 먼저 구분해야 합니다.
대부분의 애플리케이션에서는 자동 설정 전체를 끄지 않고 SecurityFilterChain 빈으로 접근 정책을 정의하는 것이 적절합니다. 이 글은 과거 Gist 예제를 본문 코드로 교체하고, Boot 4에서 바뀐 자동 설정 패키지와 책임까지 반영했습니다.
수정일: 2026-10-11. 공식 문서와 API를 대조한 범위는 Spring Boot 3.5 / Spring Security 6.5, Spring Boot 4.0 / Spring Security 7.0의 Servlet 기반 Spring MVC 애플리케이션입니다. 대표 실행 검증은 Java 21.0.2, Boot 3.5.16, Security 6.5.11에서 본문 Java 코드의 컴파일과 MockMvc 테스트 6건으로 한정했습니다. 이 버전을 최신 패치로 안내하는 것은 아닙니다. Boot 4 코드는 문서 대조만 했으며, 실제 서버에 대한 curl 호출, WebFlux, OAuth2 인증 서버, 별도 management 포트 구성은 실행 검증하지 않았습니다.
1. 기본 웹 보안과 기본 사용자는 별개다
Boot 3.5 보안 문서와 Boot 4.0 보안 문서는 다음 두 설정을 구분합니다.
- 웹 보안: 요청을 어떤 필터 체인에 넣고, 어떤 인증 방식과 접근 규칙을 적용할지 결정합니다. 사용자 정의
SecurityFilterChain빈을 등록하면 Boot의 기본 웹 보안 구성이 물러납니다. - 사용자 인증 정보: 별도 인증 구성이 없으면
UserDetailsServiceAutoConfiguration이user계정과 개발용 임의 비밀번호를 제공할 수 있습니다.UserDetailsService,AuthenticationProvider,AuthenticationManager등의 빈을 등록하면 관련 조건에 따라 이 자동 설정이 물러납니다.
따라서 SecurityFilterChain을 만들었다고 Using generated security password 로그까지 사라지는 것은 아닙니다. 반대로 spring.security.user.password를 지정해 임의 비밀번호 생성을 막아도, 자체 사용자 저장소나 서비스로 교체한 것은 아닙니다.
OAuth2 Client, Resource Server, SAML 관련 의존성이나 JwtDecoder 같은 빈도 기본 사용자 생성 조건에 영향을 줍니다. 빈이 없다는 사실만으로 기본 사용자가 반드시 생성된다고 단정하지 말고 실제 의존성과 조건 평가 결과를 확인해야 합니다.
또한 기본 설정에서는 요청의 Accept 헤더에 따라 폼 로그인으로 이동하거나 HTTP Basic 인증을 요구할 수 있습니다. 브라우저의 302와 API 호출의 401은 같은 보안 설정에서 나올 수 있습니다.
2. 권장 방법: SecurityFilterChain으로 정책을 정의한다
아래는 공개 상태 확인용 GET /public만 허용하고 나머지 요청에는 인증을 요구하는 로컬 학습용 최소 애플리케이션입니다. CSRF와 기본 보안 헤더를 끄지 않습니다. 전체 요청을 permitAll()로 여는 예제도 아닙니다.
프로젝트 준비
Java 17 이상과 Maven을 사용하는 Spring Boot 프로젝트를 준비합니다. Boot의 parent 또는 BOM과 Maven 플러그인 버전을 같은 계열로 맞추고, Spring Security 버전은 Boot의 의존성 관리를 따릅니다.
- Boot 3.5.x:
spring-boot-starter-web,spring-boot-starter-security - Boot 4.0.x:
spring-boot-starter-webmvc,spring-boot-starter-security
Boot 4의 webmvc 스타터와 모듈화는 공식 마이그레이션 가이드를 참고하세요. 아래 코드는 이 두 구성에서 같은 Spring Security API를 사용하도록 작성했습니다. 특정 패치 버전을 최신 버전으로 고정해 안내하는 대신, 프로젝트가 사용하는 계열의 보안 패치를 적용하고 실제 해석된 버전을 확인하세요.
아래 세 파일을 모두 src/main/java/com/example/securitydemo/에 둡니다. 다른 패키지를 사용한다면 세 파일의 package를 함께 수정합니다.
SecurityDemoApplication.java
package com.example.securitydemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityDemoApplication.class, args);
}
}
SecurityConfig.java
package com.example.securitydemo;
import jakarta.servlet.DispatcherType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
.requestMatchers(HttpMethod.GET, "/public").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
// CSRF, 세션 보호와 보안 헤더는 기본 설정을 유지한다.
return http.build();
}
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
UserDetailsService userDetailsService(
PasswordEncoder passwordEncoder,
@Value("${DEMO_PASSWORD}") String password) {
if (password.isBlank()) {
throw new IllegalArgumentException("DEMO_PASSWORD must not be blank");
}
return new InMemoryUserDetailsManager(
User.withUsername("demo")
.password(passwordEncoder.encode(password))
.roles("USER")
.build()
);
}
}
ERROR 디스패치 허용은 애플리케이션의 오류 응답이 다시 로그인 응답으로 바뀌는 것을 피하기 위한 것입니다. 일반 요청으로 /error를 직접 호출하는 것을 공개하는 규칙은 아닙니다. 오류 응답에 민감한 정보가 노출되지 않게 하는 것은 별도 책임입니다.
UserDetailsService 빈은 웹 접근 규칙과 별도로 등록했습니다. 이 빈이 기본 생성 사용자를 대신합니다. 예제의 인메모리 계정은 사용자 저장소 구현법을 설명하기 위한 것으로, 운영용 계정 관리·비밀번호 변경·감사 기능을 대체하지 않습니다. 환경 변수에도 기본 비밀번호를 두지 않았으며 값이 없거나 비어 있으면 시작을 실패시키도록 했습니다.
DemoController.java
package com.example.securitydemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/public")
public String publicPage() {
return "public";
}
@GetMapping("/private")
public String privatePage() {
return "private";
}
@PostMapping("/private")
public String changeSomething() {
return "accepted";
}
}
이 예제는 자동 설정을 제외하지 않습니다. 정상적인 Boot Servlet 자동 설정을 유지하는 구성에서는 사용자 코드의 @EnableWebSecurity를 추가하지 않아도 됩니다. 반면 웹 보안 활성화까지 담당하던 자동 설정을 제외하거나 일반 Spring 환경에서 직접 구성한다면 활성화와 필터 등록 경로를 따로 검토해야 합니다. Boot 버전이나 제외 대상을 보지 않고 “exclude를 쓰면 항상 @EnableWebSecurity 하나만 추가하면 된다”고 일반화할 수는 없습니다.
Security 6과 7에서의 설정 문법
위 코드는 두 계열에서 사용할 수 있는 Lambda DSL입니다. Security 6.5의 7.0 준비 문서는 Lambda DSL을 권장하고, Security 7에서는 이전의 무인자 설정 메서드와 .and() 중심 DSL을 사용할 수 없다고 설명합니다.
옛 WebSecurityConfigurerAdapter 예제나 authorizeRequests().and() 형태를 그대로 복사하지 말고 SecurityFilterChain과 authorizeHttpRequests 기반으로 옮기세요. Lambda DSL 도입 자체가 Security 7의 새 기능인 것은 아닙니다.
3. 실행과 확인: 예상 결과를 실제로 대조한다
Maven Wrapper가 있는 프로젝트 루트에서 실행합니다. 아래 입력 명령은 Bash 기준이며, 로컬 검증용 비밀번호는 직접 정합니다. 입력 값은 화면에 표시하지 않습니다.
read -r -s -p '로컬 검증용 비밀번호: ' DEMO_PASSWORD
printf '\n'
export DEMO_PASSWORD
./mvnw dependency:tree -Dincludes=org.springframework.security
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.address=127.0.0.1 --debug"
Wrapper가 없다면 설치한 Maven의 mvn으로 바꾸고, spring-boot-maven-plugin이 설정되어 있는지 확인합니다. 서버를 127.0.0.1에만 바인딩하는 이유는 이 학습용 계정을 외부에 노출하지 않기 위해서입니다. 운영에서는 HTTPS와 실제 인증 시스템, 엔드포인트별 권한 정책이 필요합니다.
다른 터미널에서 다음을 확인합니다. curl -u demo는 위에서 설정한 비밀번호를 대화형으로 입력받으므로 명령줄에 비밀번호를 적지 않아도 됩니다.
curl -i http://127.0.0.1:8080/public
curl -i -H 'Accept: application/json' http://127.0.0.1:8080/private
curl -i -u demo -H 'Accept: application/json' http://127.0.0.1:8080/private
curl -i -u demo -H 'Accept: application/json' -X POST http://127.0.0.1:8080/private
위 curl 명령으로 확인해야 할 예상 결과는 순서대로 다음과 같습니다. 실제 서버에 curl을 실행한 결과 기록은 아닙니다.
- 공개 GET은
200과public을 반환합니다. - 인증 없는 비공개 GET은
401을 반환합니다. - 올바른 계정으로 비공개 GET을 호출하면
200과private을 반환합니다. - 올바른 계정이어도 CSRF 토큰 없는 POST는
403으로 거부되어야 합니다.
브라우저에서 /private를 열면 기본 로그인 화면으로 이동하는지도 확인할 수 있습니다. 기본 로그인 폼은 CSRF 토큰을 처리하지만, 직접 만든 상태 변경 폼이나 JavaScript 요청에는 해당 요청의 토큰 전달을 구현해야 합니다.
CSRF 공식 문서처럼 안전하지 않은 HTTP 메서드는 기본 보호 대상입니다. “REST API라서”, “세션을 쓰지 않아서”라는 이유만으로 CSRF를 일괄 해제하지 마세요. 브라우저가 쿠키나 HTTP Basic 자격 증명을 자동 전송하는지 등 실제 인증 방식을 함께 판단해야 합니다.
별도 임시 Maven 프로젝트에서는 본문의 세 Java 파일을 사용해 MockMvc 테스트를 실행했습니다. 공개 GET 허용, 비공개 GET의 미인증 거부, 올바른 비밀번호 허용, 잘못된 비밀번호 거부, CSRF 토큰 없는 POST 거부, 유효한 CSRF 토큰을 붙인 POST 허용의 6건이 모두 통과했습니다. 이는 Servlet 필터와 컨트롤러를 MockMvc로 검증한 결과이며 실제 브라우저·네트워크·운영 환경 검증을 대신하지 않습니다.
서비스에 적용할 때는 필요한 역할이 없는 계정의 접근 거부와 관리 엔드포인트 정책도 별도로 테스트합니다. 200 하나만 확인해서는 보안 정책을 검증할 수 없습니다.
4. exclude는 언제 쓰고, Boot 4에서 무엇이 달라졌나
spring.autoconfigure.exclude 또는 @SpringBootApplication(exclude = ...)는 특정 자동 설정을 제외하는 도구입니다. Spring Security 전체를 제거하거나 모든 사용자 정의 보안 빈을 중지하는 전역 스위치가 아닙니다.
기존 SecurityAutoConfiguration 경로를 그대로 쓰지 않는다
Boot 3.5와 Boot 4.0의 공식 API에서 클래스 경로가 다릅니다.
- Boot 3.5:
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration - Boot 4.0:
org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration
그러나 패키지 문자열만 바꾸면 기존 제외 설정과 같은 효과가 나는 것도 아닙니다. Boot 3.5의 SecurityAutoConfiguration은 SpringBootWebSecurityConfiguration을 import하지만, Boot 4.0에는 웹 보안을 담당하는 org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration이 별도 자동 설정으로 존재합니다. Servlet 필터 등록에 관련된 SecurityFilterAutoConfiguration 역시 별도로 구성됩니다.
따라서 Boot 4에서 이름이 익숙한 SecurityAutoConfiguration 하나만 제외하고 웹 보안까지 꺼졌다고 판단하면 안 됩니다. 다음 공식 API를 기준으로 실제 사용하는 버전의 클래스와 조건을 확인하세요.
- Boot 3.5 SecurityAutoConfiguration API
- Boot 4.0 SecurityAutoConfiguration API
- Boot 4.0 ServletWebSecurityAutoConfiguration API
- Boot 4.0 SecurityFilterAutoConfiguration API
기본 사용자 생성만 제외해야 하는 특수한 경우
인증을 별도로 구성했지만 기본 사용자 자동 설정을 명시적으로 제외해야 하는 경우라면 제외 대상은 UserDetailsServiceAutoConfiguration입니다. 일반적인 애플리케이션에서는 먼저 앞의 예제처럼 인증 빈을 제공해 자동 설정이 물러나게 하는 방법을 선택하세요.
정말 제외가 필요한 경우에만 src/main/resources/application.properties에 사용 중인 버전에 해당하는 하나를 적용합니다. 앞의 실행 예제에는 필요하지 않습니다.
Boot 3.5:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
Boot 4.0:
spring.autoconfigure.exclude=org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration
각각의 조건과 역할은 Boot 3.5 API, Boot 4.0 API에서 확인할 수 있습니다. 이 설정만 추가해 놓고 대체 인증 수단을 제공하지 않으면 인증이 동작하지 않거나 구성에 따라 시작이 실패할 수 있습니다. 웹 접근 규칙을 대체하지도 않습니다.
제외 전후에 확인할 것
--debug로 출력되는 조건 평가 보고서에서 어떤 자동 설정이 적용·제외되었는지 확인합니다. 로그가 없다는 이유만으로 필터가 없어졌다고 판단하지 않습니다.- Actuator를 사용하면 관리 엔드포인트의 노출 설정과 접근 권한을 함께 검토합니다. 사용자 정의 체인을 등록하면 Boot의 기본 관리 엔드포인트 보안 정책도 그대로 유지된다고 기대해서는 안 됩니다.
- 다중 체인에서
securityMatcher("/api/**")만 만들고 나머지를 방치하지 않습니다. Security 공식 문서는 어떤 체인에도 일치하지 않는 요청은 Spring Security로 보호되지 않는다고 명시합니다. 필요하다면 범위를 제한하지 않은 마지막 체인을 둡니다. - 인증 실패를 해결하려고
anyRequest().permitAll(), CSRF 전면 해제 또는 필터 자동 등록 제외부터 적용하지 않습니다. 문제를 숨기면서 방어 기능도 제거할 수 있습니다.
5. 자주 보이는 증상과 확인 순서
체인을 작성했는데 임의 비밀번호가 계속 출력된다
웹 체인과 사용자 자동 설정은 별개입니다. 사용자 정의 UserDetailsService 등의 빈이 컴포넌트 스캔 범위에 있는지, 실제로 등록됐는지 확인하세요. PasswordEncoder 빈 하나만 만든 것으로는 사용자 저장소를 대체하지 않습니다.
Boot 4로 올린 뒤 예전 exclude import가 컴파일되지 않는다
패키지 이동과 모듈 의존성을 확인하세요. properties에 오래된 클래스명을 넣은 경우도 원하는 자동 설정이 제외됐다고 가정하지 말고 조건 평가 보고서를 봅니다. 웹 정책 변경이 목적이라면 제외 목록을 늘리기 전에 SecurityFilterChain으로 해결할 수 있는지 검토합니다.
로그인은 되는데 POST만 403이다
CSRF 토큰 누락·불일치와 인가 실패를 구분하세요. 위 예제의 토큰 없는 POST는 의도된 거부입니다. 요청의 인증 상태, 토큰, 세션 및 실제 적용된 체인을 확인한 뒤 수정합니다.
일부 URL만 인증 없이 열린다
permitAll 규칙만 보지 말고 체인 선택에 쓰이는 securityMatcher와 @Order도 확인합니다. 체인 내부의 anyRequest()는 그 체인에 들어온 요청에만 적용됩니다.
정리
기본 웹 보안은 SecurityFilterChain으로, 기본 생성 사용자는 인증 관련 빈으로 각각 대체합니다. 자동 설정 제외는 대체할 책임이 명확할 때만 사용하고, Boot 4에서는 패키지 이동뿐 아니라 웹 보안 자동 설정의 분리도 확인해야 합니다.
함께 읽기
- Spring Security 7.x HttpSecurity와 WebSecurity의 차이점: 웹 보안 구성 요소의 역할을 더 살펴봅니다.
- SpringSecurity FilterChain이 만들어지는 과정 살펴보기: 필터 연결 구조에 관한 기존 글입니다. 오래된 API 예제는 이 글의 적용 버전과 구분해서 읽으세요.
Subscribe via RSS