SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기
Spring Security 6에서는 SecurityFilterChain과 authorizeHttpRequests() 조합이 보안 규칙의 중심입니다. 요청 매칭(request matcher) → 인가 규칙 → 예외 처리 순서로 명확하게 설계하면, 예상치 못한 허용/차단을 피할 수 있습니다. 이 글에서는 실전에서 바로 쓰는 설정 패턴을 코드와 함께 정리합니다.
1) 최소 예제: “더 구체적인 규칙을 먼저, anyRequest는 맨 마지막”
정적 리소스/로그인/헬스체크는 허용, 나머지는 인증 필요. 규칙 순서가 중요합니다.
@Configuration
@EnableMethodSecurity // @PreAuthorize 등 메서드 보안 활성화(선택)
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
HandlerMappingIntrospector introspector) throws Exception {
// MVC 매처(스프링 MVC 경로 인식) 빌더
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector).servletPath("/");
http
.csrf(csrf -> csrf.ignoringRequestMatchers(mvc.pattern("/api/**"))) // API는 보통 CSRF 미적용
.authorizeHttpRequests(auth -> auth
// <-- 구체 & 상단 배치
.requestMatchers(
mvc.pattern("/"),
mvc.pattern("/login"),
mvc.pattern("/css/**"),
mvc.pattern("/js/**"),
mvc.pattern("/actuator/health")
).permitAll()
.requestMatchers(mvc.pattern("/admin/**")).hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll() // 로그인/토큰발급 등
// <-- 마지막에 포괄 규칙
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.logout(lo -> lo.logoutSuccessUrl("/"))
.headers(h -> h.frameOptions(frame -> frame.sameOrigin())); // H2 콘솔 등
return http.build();
}
}
- 순서: 먼저 매칭된 규칙이 적용됩니다.
anyRequest()는 항상 맨 끝에 두세요. - MVC vs Ant:
MvcRequestMatcher는 스프링 MVC의 경로 인식을 활용합니다. 문자열 패턴으로 간단히 갈 때는requestMatchers("/admin/**")처럼 써도 되지만, MVC 매처를 쓰면 경로 해석이 더 정확합니다.
2) API와 웹을 “서로 다른 체인”으로 분리(@Order)
API는 무상태(Stateless) + 토큰 인증, 웹은 세션/폼 로그인처럼 성격이 다를 때 두 개의 SecurityFilterChain으로 분리하는 게 깔끔합니다.
@Configuration
@EnableMethodSecurity
public class MultiChainSecurity {
@Bean @Order(1) // 먼저 평가: /api/** 전용
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults()); // 또는 Bearer/JWT 리소스 서버
return http.build();
}
@Bean @Order(2) // 그 외 웹
SecurityFilterChain web(HttpSecurity http,
HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector);
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(mvc.pattern("/"), mvc.pattern("/login"), mvc.pattern("/css/**")).permitAll()
.requestMatchers(mvc.pattern("/admin/**")).hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
@Order 값이 낮을수록 우선합니다. 먼저 securityMatcher("/api/**")가 API 요청을 잡아가고, 나머지는 웹 체인으로 내려갑니다.
3) 매처 종류 정리와 선택 기준
requestMatchers("/path/**"): 간단 문자열 패턴(내부적으로 AntPath 사용). 빠르게 작성할 때 편함.MvcRequestMatcher: MVC 경로 인식(경로 변수/서블릿 경로 고려). 스프링 MVC 기준으로 맞추고 싶을 때.regexMatcher(): 정규식이 꼭 필요할 때.securityMatcher(): 아예 해당 체인에 들어올지를 1차로 거릅니다(다중 체인 구성 시 필수).
원칙: “먼저 거칠 체인(securityMatcher) → 체인 내부의 세부 매칭(requestMatchers)” 순서로 사고하세요.
4) 권한 표현: hasRole vs hasAuthority
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // ROLE_ADMIN으로 해석
.requestMatchers("/reports/**").hasAuthority("RPT_RW") // 문자열 권한 직접 사용
.anyRequest().authenticated()
)
hasRole("ADMIN")는 내부적으로"ROLE_ADMIN"을 요구합니다.- 도메인 권한이 많다면
hasAuthority("...")로 세분화하는 편이 유연합니다.
5) CSRF, Remember-me, 예외 처리
http
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/**")) // REST API는 보통 비활성(토큰 사용)
.rememberMe(rm -> rm.key("long-secret").tokenValiditySeconds(1209600)) // 14일
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> res.sendError(401))
.accessDeniedHandler((req, res, e) -> res.sendError(403))
);
세션 기반 로그인(웹)은 CSRF 유지, API는 무상태로 CSRF 비활성화가 일반적입니다.
6) 메서드 보안과의 관계
@EnableMethodSecurity를 켜면 @PreAuthorize("hasRole('ADMIN')") 같은 검사가 컨트롤러/서비스 단에서 추가로 동작합니다. URL 인가 + 메서드 인가를 둘 다 통과해야 접근이 허용됩니다(AND 관계).
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> list() { ... }
7) 테스트: 규칙이 의도대로 동작하는지 빠르게 검증
spring-security-test를 사용하면 MockMvc로 인가 동작을 확인할 수 있습니다.
@WebMvcTest(controllers = AdminController.class)
@Import(SecurityConfig.class)
class SecurityRuleTest {
@Autowired MockMvc mvc;
@Test
void admin_is_forbidden_without_role() throws Exception {
mvc.perform(get("/admin/panel")).andExpect(status().is3xxRedirection()); // 로그인 요구
}
@Test
@WithMockUser(roles = "ADMIN")
void admin_is_ok_with_role() throws Exception {
mvc.perform(get("/admin/panel")).andExpect(status().isOk());
}
}
8) 실전 체크리스트
- 규칙 순서: 특정 경로 허용/차단 → 마지막에
anyRequest() - 다중 체인: API/웹을 분리하고
@Order,securityMatcher()로 명확히 구분 - 권한 표기:
hasRole은ROLE_*프리픽스 유의, 복잡하면hasAuthority - CSRF: 웹(세션)은 유지, API(무상태)는 비활성 + 토큰
- 테스트:
spring-security-test로 인가 케이스를 자동화
👉 1편: SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기
👉 2편: PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책
👉 3편: AuthenticationProvider & UserDetailsService 커스터마이징
👉 4편: 세션 기반 로그인 vs Stateless: CSRF/Remember-me/세션관리
👉 5편: JWT 심화: 만료/클레임/키 회전 + /44 글과의 연결 전략
👉 6편: OAuth2 로그인(Google/GitHub) + JWT 브릿지
👉 7편: 권한 모델링: ROLE vs 권한 문자열, @PreAuthorize, 도메인 권한
👉 8편: CORS·XSS·헤더 보안: SPA/REST 현실 설정
'Java & Spring' 카테고리의 다른 글
| AuthenticationProvider & UserDetailsService 커스터마이징 (0) | 2025.10.12 |
|---|---|
| PasswordEncoder와 회원가입: BCrypt·Pepper·비밀번호 정책 (1) | 2025.10.11 |
| Spring Security 6: JWT 로그인/리프레시 토큰 (0) | 2025.10.08 |
| springdoc-openapi로 API 문서 1분 셋업 (0) | 2025.10.05 |
| Jackson 직·역직렬화 어노테이션 핵심(@Json*) (0) | 2025.10.05 |