본문 바로가기
Java & Spring

SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기

by yamoojin83 2025. 10. 11.

SecurityFilterChain 완전정복: 요청 매칭/인가 룰 제대로 이해하기

Spring Security 6에서는 SecurityFilterChainauthorizeHttpRequests() 조합이 보안 규칙의 중심입니다. 요청 매칭(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()로 명확히 구분
  • 권한 표기: hasRoleROLE_* 프리픽스 유의, 복잡하면 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 현실 설정

👉 9편: 리소스 서버 분리 & API Gateway 연동(Spring Cloud Gateway)

👉 10편: 감사 로깅/Audit: 로그인 실패·권한거부 탐지 규격