Contents
see List왜 URL 권한 규칙을 먼저 문서화해야 하는가
Spring Boot API에 Spring Security를 붙이면 인증이 필요한 요청과 공개 요청의 경계가 애플리케이션 동작 전체를 좌우합니다. 로그인, 회원가입, 헬스체크처럼 누구나 접근해야 하는 엔드포인트와 주문 수정, 관리자 조회, 파일 다운로드처럼 권한이 필요한 엔드포인트가 한 설정 안에 섞이면 작은 예외 하나가 장애나 보안 사고로 이어집니다. 따라서 코드를 쓰기 전에 엔드포인트를 공개, 인증 필요, 역할 필요, 내부 전용으로 나누고 이 분류가 테스트로 보장되도록 설계하는 것이 좋습니다.
최근 Spring Security 기반 설정에서는 SecurityFilterChain 빈에서 authorizeHttpRequests와 requestMatchers를 조합하는 방식이 일반적입니다. 여기서 중요한 점은 규칙의 순서입니다. 더 구체적인 경로를 먼저 쓰고, 마지막에는 anyRequest().authenticated() 또는 denyAll()처럼 기본 정책을 명확히 둡니다. 기본 정책이 느슨하면 새 API가 추가될 때 의도하지 않게 공개될 수 있고, 반대로 너무 강하면 배포 후 프론트엔드의 정적 리소스나 헬스체크가 막혀 장애처럼 보일 수 있습니다.
권한 분류 기준
- 공개 API: 로그인, 토큰 재발급 요청 진입점, 회원가입, 비밀번호 재설정 요청, 공개 상품 목록처럼 인증 전에도 필요한 기능입니다.
- 인증 API: 내 정보 조회, 주문 생성, 결제 준비처럼 사용자 식별은 필요하지만 별도 역할까지는 필요하지 않은 기능입니다.
- 역할 API: 관리자 통계, 운영자 강제 취소, 내부 정산 데이터처럼 특정 권한이 있어야 하는 기능입니다.
- 운영 API: /actuator/health, /actuator/prometheus 등 운영 시스템이 호출하는 엔드포인트입니다. 외부 공개 여부와 세부 정보 노출 수준을 따로 결정해야 합니다.
역할 이름은 화면 메뉴명보다 업무 권한을 기준으로 잡는 편이 오래 갑니다. 예를 들어 ROLE_ADMIN 하나로 모든 관리 기능을 처리하면 나중에 고객센터, 정산 담당자, 물류 담당자를 분리할 때 설정이 커집니다. 처음부터 ORDER_READ, ORDER_WRITE, SETTLEMENT_READ처럼 권한을 세분화하고, 사용자의 역할이 여러 권한을 포함하도록 매핑하면 API 정책을 더 안정적으로 운영할 수 있습니다.
SecurityFilterChain 예제
아래 예시는 REST API 서버에서 세션 로그인 대신 토큰 인증을 사용한다고 가정한 기본 구조입니다. 핵심은 공개 경로를 앞에 두고, 관리자 경로는 별도 권한을 요구하며, 나머지는 인증된 사용자만 접근하도록 닫는 것입니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/actuator/health").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/refresh").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("ADMIN_ACCESS")
.requestMatchers(HttpMethod.POST, "/api/orders/**").authenticated()
.anyRequest().authenticated())
.build();
}
}
permitAll은 보안 필터를 완전히 우회하는 설정이 아니라 해당 요청을 허용하는 인가 규칙입니다. 로그인 페이지, 공개 API, 정적 자원처럼 보안 헤더나 필터 체인의 일관성이 필요한 요청은 보통 permitAll이 관리하기 쉽습니다. 반면 완전히 필터 밖으로 빼야 하는 내부 정적 파일 같은 예외는 별도 검토가 필요합니다. 대부분의 API 서버에서는 우선 permitAll로 공개 범위를 표현하고, 정말 필요한 경우에만 필터 제외를 사용합니다.
운영 환경에서 자주 생기는 실수
- 프론트엔드 개발 편의를 위해 /api/** 전체를 임시 permitAll로 열어 두고 배포 전에 되돌리지 않는 문제입니다.
- OPTIONS 프리플라이트 요청을 고려하지 않아 브라우저에서만 401 또는 403이 발생하는 문제입니다. CORS 설정과 보안 설정을 함께 확인해야 합니다.
- 관리자 API를 URL만 /admin으로 숨기고 실제 권한 검사를 서비스 계층에만 맡기는 문제입니다. URL 계층과 메서드 계층 중 최소 한 곳에서는 명확한 차단이 있어야 합니다.
- 헬스체크에 데이터베이스 상세 오류나 내부 구성 정보가 그대로 노출되는 문제입니다. 공개 헬스체크는 단순 상태만 보여주고 상세 정보는 내부망이나 인증 뒤에 두는 편이 안전합니다.
테스트로 권한 규칙 고정하기
권한 설정은 사람이 눈으로 리뷰하는 것만으로 부족합니다. 새 컨트롤러가 추가될 때 기본 정책이 제대로 적용되는지, 공개 API가 계속 공개 상태인지, 관리자 API가 일반 사용자에게 막히는지 테스트가 있어야 합니다. 특히 리팩터링이나 Spring Security 버전 변경 후에는 설정 DSL이 컴파일되어도 실제 매칭 순서가 기대와 다를 수 있으므로 MockMvc 테스트를 권장합니다.
@SpringBootTest
@AutoConfigureMockMvc
class SecurityRulesTest {
@Autowired
MockMvc mvc;
@Test
void publicProductListIsOpen() throws Exception {
mvc.perform(get("/api/products"))
.andExpect(status().isOk());
}
@Test
void adminApiRequiresAdminAuthority() throws Exception {
mvc.perform(get("/api/admin/orders"))
.andExpect(status().isUnauthorized());
mvc.perform(get("/api/admin/orders")
.with(user("staff").authorities(new SimpleGrantedAuthority("USER_ACCESS"))))
.andExpect(status().isForbidden());
}
}
테스트 이름은 기술 구현보다 정책 의도를 드러내야 합니다. publicProductListIsOpen, adminApiRequiresAdminAuthority처럼 읽히면 나중에 실패했을 때 어떤 보안 경계가 깨졌는지 바로 알 수 있습니다. 테스트 데이터가 많아지면 권한 매트릭스를 표 형태로 정리하고, 같은 규칙을 파라미터 테스트로 반복하는 방식도 좋습니다.
마무리 체크리스트
- 새 API를 만들 때 공개, 인증, 역할, 운영 중 하나로 분류했는지 확인합니다.
- SecurityFilterChain에서는 구체적인 requestMatchers를 먼저 두고 마지막 기본 정책을 닫아 둡니다.
- permitAll은 공개 요청을 명시하는 도구로 사용하고, 필터 제외는 꼭 필요한 경우에만 제한적으로 사용합니다.
- 헬스체크와 운영 엔드포인트는 외부 공개 범위와 상세 정보 노출 수준을 분리합니다.
- 공개 API, 인증 API, 관리자 API의 기대 응답을 MockMvc 또는 통합 테스트로 고정합니다.