이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- AuthorizationManager와 Method Security는 어떤 인가 결정을 맡는가?
- URL 권한, method 권한, repository ownership 조건은 어떻게 함께 설계해야 하는가?
- Method Security가 적용되지 않는 self-invocation, batch, 내부 호출 경계는 어떻게 검증하는가?
개요
AuthorizationManager는 인증된 주체가 특정 secure object에 접근할 수 있는지 결정하는 Spring Security의 인가 추상화다. 요청 기반 인가와 method 기반 인가 모두 이 모델로 이해할 수 있다.
Method Security는 Controller URL을 벗어난 Service 호출에도 권한 정책을 붙일 수 있게 한다. 같은 Service가 REST API, batch, message listener, admin tool에서 재사용될 때 URL 권한만으로는 부족하기 때문에 중요하다.
다만 Method Security가 모든 데이터 접근을 안전하게 만드는 것은 아니다. 권한 표현식이 호출 허용을 결정하더라도 Repository가 너무 넓게 조회하면 IDOR나 tenant leak이 남을 수 있다.
이 문서의 핵심은 “권한 annotation을 붙였는가”가 아니라 “권한 결정과 데이터 조회 범위가 같은 정책을 공유하는가”다.
공격 시나리오
- 공격자는 인증된 사용자 token으로 다른 사용자의 resource id를 넣는다.
- URL matcher는 통과하고 method security가 없는 내부 Service method를 통해 같은 기능을 호출한다.
- self-invocation 때문에 같은 bean 내부 method의
@PreAuthorize가 적용되지 않는 경로를 찾는다. - batch나 message listener가 Service를 호출하면서 인증/인가 context 없이 실행된다.
- 권한 표현식이 role만 확인하고 resource owner를 확인하지 않는다.
- Repository가 tenant/member 조건 없이
findById로 데이터를 가져온다.
취약한 요청/응답 예시
@PreAuthorize("hasRole('USER')")
public Order getOrder(long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(OrderNotFoundException::new);
}GET /api/orders/991 HTTP/1.1
Authorization: Bearer member-1004-tokenrole은 맞지만 주문 소유자가 다르면 접근하면 안 된다. role check만으로는 객체 수준 권한을 해결하지 못한다.
주제별 핵심 판단
- 인증은 로그인 여부이고 인가는 resource와 action에 대한 허용 결정이다.
- URL authorization은 endpoint 진입을 제어한다.
- Method Security는 service method 호출을 제어한다.
- 객체 수준 권한은 principal과 resource owner를 비교해야 한다.
- policy service는 권한 표현식을 테스트 가능한 코드로 분리한다.
- Repository 조건은 권한 정책의 마지막 데이터 경계다.
- self-invocation과 비프록시 호출은 Method Security 적용 여부를 검증해야 한다.
개선된 요청/응답 예시
@PreAuthorize("@orderPolicy.canRead(authentication, #orderId)")
public Order getOrder(long orderId) {
MemberId memberId = principalResolver.currentMemberId();
return orderRepository.findByIdAndMemberId(orderId, memberId)
.orElseThrow(OrderNotFoundException::new);
}public boolean canRead(Authentication authentication, long orderId) {
MemberId memberId = principalResolver.memberId(authentication);
return orderRepository.existsByIdAndMemberId(orderId, memberId);
}policy와 repository가 같은 owner boundary를 사용한다. 실패 응답은 정보 노출을 줄이기 위해 404를 선택할 수 있다.
방어 설계
- endpoint별 action을
read,create,update,delete,approve,refund처럼 명확히 나눈다. - role check와 resource ownership check를 분리한다.
- 복잡한 SpEL은 policy service로 이동해 단위 테스트한다.
- Service method는 URL 밖 호출도 고려해 보안 경계를 둔다.
- Repository는 tenant/member/resource 조건을 포함한다.
- self-invocation이 있는 구조는 method security가 적용되는지 테스트하거나 구조를 분리한다.
- batch와 internal job은 service account 권한과 대상 범위를 명시한다.
- 권한 거부 event에는 resource, action, reason을 남긴다.
Spring/HTTP 예시
@Configuration
@EnableMethodSecurity
class MethodSecurityConfig {
}@Service
public class OrderPolicy {
public boolean canApprove(Authentication authentication, long orderId) {
MemberId memberId = principalResolver.memberId(authentication);
return orderRepository.existsApprovableOrder(orderId, memberId);
}
}@PreAuthorize("@orderPolicy.canApprove(authentication, #orderId)")
public void approve(long orderId) {
orderRepository.markApproved(orderId);
}권한 정책을 별도 bean으로 두면 리뷰와 테스트가 쉬워진다. 다만 실제 데이터 변경 query도 같은 범위를 지키는지 확인해야 한다.
운영 로그와 감사 지점
level=WARN event=authorization_denied result=blocked actor=member:1004 resource=order:991 action=approve reason=owner_mismatch layer=method traceId=0cf29a91level=INFO event=authorization_policy_changed result=allowed policy=order.approve reviewer=security:kim tests=OrderPolicyTest traceId=0cf29a91- 권한 거부는 layer, resource, action, reason을 남긴다.
- policy 변경은 테스트와 reviewer를 남긴다.
- batch/service account 작업도 actor와 target scope를 남긴다.
- repository condition 변경은 보안 리뷰 대상으로 본다.
- self-invocation 우회가 발견되면 구조 변경과 회귀 테스트를 남긴다.
실전 판단 기준
hasRole('USER')만으로 객체 접근을 허용하면 부족하다.- method security는 프록시 기반 적용 경계를 이해해야 한다.
- Repository가 넓게 조회하면 policy가 있어도 후속 코드 실수로 유출될 수 있다.
- admin role은 모든 action 허용이 아니라 action별 정책이 필요하다.
- 내부 API와 batch도 권한 모델 밖에 두면 우회 경로가 된다.
- 권한 거부 로그가 없으면 공격과 사용자 오류를 구분하기 어렵다.
테스트 포인트
- 비소유자가 resource read/update/delete를 할 수 없는지 확인한다.
- 권한 없는 role이 기능 action을 수행할 수 없는지 확인한다.
- policy service 단위 테스트와 method security 통합 테스트를 모두 둔다.
- self-invocation 경로에서 annotation이 적용되는지 확인한다.
- batch/service account가 허용된 scope 밖 resource를 처리하지 않는지 확인한다.
- repository query가 tenant/member 조건을 포함하는지 확인한다.
위험 신호!
- role check만 있고 resource owner check가 없다.
- 권한 표현식이 길고 테스트가 없다.
findById결과를 바로 반환한다.- Method Security가 활성화되어 있는지 테스트하지 않는다.
- batch와 admin tool이 같은 Service를 권한 없이 호출한다.
- 권한 거부 event가 자유 문장으로만 남는다.
확인 질문
확인 질문
- AuthorizationManager가 맡는 판단은 무엇인가?
- 인증된 주체가 특정 요청이나 method 호출을 수행해도 되는지 결정하는 것이다.
- Method Security만으로 부족한 이유는 무엇인가?
- 데이터 조회 조건과 비프록시 호출, batch/internal 경계가 별도로 남기 때문이다.
- 좋은 권한 설계의 증거는 무엇인가?
- policy test, non-owner integration test, repository owner condition, authorization_denied log다.