agent skills를 찾아보다가 Spring Security에 대한 Skills를 읽어보게 되었는데 처음 보는 구문이 나와 찾아보게되었다.
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<UserDto> listUsers() {
return userService.findAll();
}
@PreAuthorize("@authz.isOwner(#id, authentication)")
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
바로 @PreAuthorize 애노테이션이다.
Method-Level 권한 모델링
다음과 같이 Security Config에서 request-level에서 권한을 모델링하는 방법과 더불어 메서드 수준에서 인가(Authorization) 설정하게 해주는 방법이다.
// request-level
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
애노테이션 기반의 메서드 보안을 적용한다면, 애노테이션이 없는 메서드는 보호되지 않는다. 즉, method-level 애노테이션만을 사용하는 것이 아니라, 반드시 HttpSecurity를 통해 기본적인 방어는 필요하다.
동작 방식
Method Security는 Spring AOP 사용하여 구현된다. 다음과 같은 메서드가 있다고 하자.
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
스프링은 실제 구현체가 아닌 프록시를 호출한다. 따라서 readCustomer가 호출되면 다음과 같은 과정을 거친다.
- 프록시인 myCustomService가 호출된다.
- 프록시는 여러 interceptor 중 @PreAuthorize에 해당하는 인터셉터인
AuthorizationManagerBeforeMethodInterceptor를 찾아 실행한다. - 2에서 찾은
AuthorizationManagerBeforeMethodInterceptor는PreAuthorizeAuthorizationManager에게 인가 판단을 맡긴다. - 가장 중요한 부분이다. @PreAuthorize의 ()안의 문자열을 SpEL 표현식으로 해석한다.
이 작업을MethodSecurityExpressionHandler가 표현식을 파싱하고,EvaluationContext인MethodSecurityExpressionRoot를 만든다. 이 context 안에는 현재 메서드 정보, 파라미터, Authentication등의 정보가 들어간다. - 이제 실제 표현식의 평가가 일어나고, 통과한다면 실제 메서드가 실행된다.
- 실패한다면, 메서드는 실행되지 않고
AccessDeniedException이 발생한다.
이때, Exception 외에AuthorizationDeniedEvent가 발행되어 로깅, 모니터링, 감사 목적으로도 활용이 가능하다고 한다.AccessDeniedException은 시큐리티 필터체인의ExceptionTranslationFilter가 잡아서 403을 반환한다. 만약, HTTP 요청이 아니고, 스케줄런 배치 등의 상황에서 이 예외가 발생한다면, 직접 처리해야한다.(If the method is not being called in the context of an HTTP request, you will likely need to handle theAccessDeniedExceptionyourself)

사용법
아무 @Configuration 클래스에 @EnableMethodSecurity를 추가해주면 애노테이션을 사용할 수 있다.
프록시를 기반으로 구현되기에 Bean의 메서드에서만 해당 애노테이션이 사용 가능하다.(컨트롤러/서비스 메서드가 실제로 실행되기 전에 가로채서 표현식을 평가하기에)
역할/권한 검사
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
@PreAuthorize("hasAuthority('user:delete')")
@PreAuthorize("hasAnyAuthority('user:read', 'user:write')")
인증 여부 검사
@PreAuthorize("isAuthenticated()")
@PreAuthorize("isAnonymous()")
@PreAuthorize("isFullyAuthenticated()")
파라미터 기반 검사
@PreAuthorize("#id == authentication.principal.id")
public void deleteUser(Long id) { ... }
커스텀 Bean 호출
@PreAuthorize("@authz.isOwner(#id, authentication)")
...
@Component("authz")
public class Authz {
public boolean isOwner(Long id, Authentication authentication) {
Long currentUserId = ((CustomPrincipal) authentication.getPrincipal()).getId();
return currentUserId.equals(id);
}
}
이때 @authz는 Bean name이고, isOwner는 직접 구현해야한다.
주의
- 같은 메서드에 동일한 애노테이션을 여러 번 사용할 수 없다.
// X
@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAuthority('user:delete')")
// O
@PreAuthorize("hasRole('ADMIN') and hasAuthority('user:delete')")
관련된 애노테이션
@PostAuthorize
메서드 실행 후 반환값까지 보고 인가@PreFilter
메서드 실행 전 컬렉션 인자 필터링@PostFilter
메서드 실행 후 컬렉션 반환값 필터링@Secured- JSR-250 계열 (
@RolesAllowed등)
Spring Security는각각에 대해 별도의 인터셉터를 가진다.
장점
- URL 기반(request-level) 제어의 한계를 보완하고, 리소스/도메인 기반 권한(ownership, 조직, 상태 등)을 표현할 수 있다.
- 권한 확인 로직으로 서비스 코드가 길어지는 점을 개선시킬 수 있다는 장점이 있다.
- 동일 권한 로직을 Bean으로 추출해 재사용 및 테스트 용이성이 좋다.
단점
- 조건이 복잡해질 경우, SpEL의 가독성/유지보수성이 낮아진다. 이 경우 서비스 로직에서 처리하는 것이 좋다.
- 내부에서 DB를 호출한다면, 요청마다 비용이 발생한다. 이에 대한 비용은 개발자의 책임이다.
- AOP 기반 제약 존재 → 동일 클래스 내부 호출(self-invocation)에서는 적용되지 않을 수 있다.
https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html
Method Security :: Spring Security
There are some scenarios where you may not wish to throw an AuthorizationDeniedException when a method is invoked without the required permissions. Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases
docs.spring.io
'스프링' 카테고리의 다른 글
| AWS S3: Post Presign Url이 만료되지 않는 문제 (0) | 2026.01.20 |
|---|---|
| 스프링 JWT(3): JWTFilter (3) | 2025.01.09 |
| 스프링 JWT(2): JWTUtil & LoginFilter (0) | 2025.01.07 |
| 스프링 JWT(1): 프로젝트 생성 (2) | 2025.01.06 |