주의
이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.
서론
- 메서드 호출은 허용하면서도 메서드로 보내는 매개변수가 몇 가지 규칙을 따르는지 확인하고 싶을 수도 있음
- 또는 메서드를 호출한 뒤 호출자가 반환된 값의 승인된 부분만 받을 수 있게 하려는 시나리오도 있을 수 있음
- 이러한 기능을 필터링이라고 하며 다음의 두 범주로 분류함
- 사전 필터링 (prefiltering): 프레임워크가 메서드를 호출하기 전에 매개변수의 값을 필터링
- 사후 필터링 (postfiltering): 프레임워크가 메서드를 호출한 뒤 반환된 값을 필터링
- 필터링은 호출 권한 부여와 다른 방식으로 작동함
- 필터링을 적용하면 프레임워크는 매개변수나 반환된 값이 권한 부여 규칙을 준수하지 않아도 메서드를 호출하며 예외를 던지지 않는 대신 지정한 조건을 준수하지 않는 요소를 필터링함
- 필터링은 컬렉션과 배열에만 적용할 수 있음
- 사전 필터링은 메서드가 객체의 배열이나 컬렉션을 받아야 이용할 수 있음
- 프레임워크는 개발자가 정의한 규칙에 따라 해당 컬렉션이나 배열을 필터링함
- 사후 필터링 또한 메서드가 컬렉션이나 배열을 반환해야 필터링을 적용할 수 있음

1. 메서드 권한 부여를 위한 사전 필터링 적용
- 필터링을 이용하면 누군가가 메서드를 호출할 때 메서드 매개변수로 전송된 값을 검증하도록 프레임워크에 지시할 수 있음
- 프레임워크는 주어진 기준을 충족하지 않는 값을 필터링하고 기준을 충족하는 값으로만 메서드를 호출
- 해당 기능을 사전 필터링이라고 지칭
- 스프링 시큐리티는 Aspect로 필터링을 구현함
- Aspect는 특정 메서드를 가로채고 다른 명령으로 메서드를 보강할 수 있음
- 사전 필터링에서 Aspect는 @PreFilter 어노테이션이 지정된 메서드를 가로채고 정의된 기준에 따라 매개변수로 제공되는 컬렉션 값을 필터링함

- 사전 필터링은 @PreFilter 어노테이션의 값으로 권한 부여 규칙을 설정함
- SpEL 식으로 제공하는 이러한 규칙에서 filterObject로 메서드의 매개변수로 제공하는 컬렉션 또는 배열 내의 모든 요소를 참조함
- Aspect가 새로운 컬렉션이나 배열을 반환하는 것이 아니라 전달된 컬렉션이나 배열을 필터링하는 것이므로 전달된 매개변수가 immutable이면 예외 발생
- 아래 예제는 사용자가 상품을 사고파는 애플리케이션이며 백엔드는 /sell 엔드포인트를 구현
- 사용자가 상품을 판매하려고 하면 애플리케이션 프런트엔드가 해당 엔드포인트를 호출하지만 로그인한 사용자는 본인 상품만 팔 수 있음
- 매개변수로 받은 상품을 파기 위해 서비스 메서드를 호출하는 간단한 시나리오를 구현하기 위해 @PreFilter 어노테이션으로 메서드가 현재 로그인한 사용자의 상품만 받을 수 있도록 구현함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig { | |
@Bean | |
public UserDetailsService userDetailsService() { | |
var uds = new InMemoryUserDetailsManager(); | |
var u1 = User.withUsername("nikolai") | |
.password("12345") | |
.authorities("read") | |
.build(); | |
var u2 = User.withUsername("julien") | |
.password("12345") | |
.authorities("write") | |
.build(); | |
uds.createUser(u1); | |
uds.createUser(u2); | |
return uds; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
} | |
public class Product { | |
private String name; | |
// 특정 owner에 사용자 이름의 값이 들어있음 | |
private String owner; | |
// 중략 | |
} | |
@Service | |
public class ProductService { | |
// 매개변수로 주어진 목록은 인증된 사용자가 소유한 상품만 허용 | |
@PreFilter("filterObject.owner == authentication.name") | |
public List<Product> sellProducts(List<Product> products) { | |
// 상품을 판매하고 판매된 상품 목록 반환 | |
return products; | |
} | |
} | |
@RestController | |
public class ProductController { | |
@Autowired | |
private ProductService productService; | |
@GetMapping("/sell") | |
public List<Product> sellProduct() { | |
List<Product> products = new ArrayList<>(); | |
products.add(new Product("beer", "nikolai")); | |
products.add(new Product("candy", "nikolai")); | |
products.add(new Product("chocolate", "julien")); | |
return productService.sellProducts(products); | |
} | |
} |

2. 메서드 권한 부여를 위한 사후 필터링 적용
- 사후 필터링도 사전 필터링과 비슷하게 Aspect에 의존함
- 해당 Aspect는 메서드를 호출하도록 허용하지만 메서드가 반환되면 반환된 값이 정의된 규칙을 준수하는지 확인
- 사후 필터링도 사전 필터링과 마찬가지로 메서드가 반환하는 컬렉션이나 배열을 변경하며 반환된 컬렉션의 요소가 따라야 하는 기준을 지정할 수 있음
- 사후 필터 Aspect는 반환된 컬렉션이나 배열에서 이러한 규칙을 따르지 않는 요소를 필터링함

- 사후 필터링을 적용하려면 @PostFilter 어노테이션을 이용해야 함
- 어노테이션의 값의 권한 부여 규칙을 SpEL 식으로 지정하면 해당 규칙이 필터링 Aspect에 적용됨
- 또한 사후 필터링도 사전 필터링과 비슷하게 배열과 컬렉션만 대상으로 작업하므로 @Postfilter 어노테이션은 컬렉션이나 배열을 반환하는 메서드에만 지정할 수 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableGlobalMethodSecurity(prePostEnabled = true) | |
public class ProjectConfig { | |
@Bean | |
public UserDetailsService userDetailsService() { | |
var uds = new InMemoryUserDetailsManager(); | |
var u1 = User.withUsername("nikolai") | |
.password("12345") | |
.authorities("read") | |
.build(); | |
var u2 = User.withUsername("julien") | |
.password("12345") | |
.authorities("write") | |
.build(); | |
uds.createUser(u1); | |
uds.createUser(u2); | |
return uds; | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
} | |
public class Product { | |
private String name; | |
private String owner; | |
// 중략 | |
} | |
@Service | |
public class ProductService { | |
// 메서드가 반환한 컬렉션에 객체에 대한 필터링 조건 추가 | |
@PostFilter("filterObject.owner == authentication.name") | |
public List<Product> findProducts() { | |
List<Product> products = new ArrayList<>(); | |
products.add(new Product("beer", "nikolai")); | |
products.add(new Product("candy", "nikolai")); | |
products.add(new Product("chocolate", "julien")); | |
return products; | |
} | |
} | |
@RestController | |
public class ProductController { | |
@Autowired | |
private ProductService productService; | |
@GetMapping("/find") | |
public List<Product> findProducts() { | |
return productService.findProducts(); | |
} | |
} |

3. 스프링 데이터 레포지토리에 필터링 이용
- 스프링 부트 애플리케이션에서 SQL 또는 NoSQL과 같은 데이터베이스에 연결하기 위한 상위 계층으로는 스프링 데이터가 자주 이용됨
- 레포지토리 수준에 필터를 적용하는 두 가지 방법이 있음
- 첫 번째 방식은 @PreFilter와 @PostFilter 어노테이션을 이용하는 것
- 두 번째 방식은 쿼리에 직접 권한 부여 규칙을 통합하는 것
- 레포지토리에 @PreFilter 어노테이션을 적용하는 사전 필터링은 애플리케이션의 다른 계층에 적용하는 것과 같지만 사후 필터링은 상황이 다름
- 레포지토리 메서드에 @PostFilter를 적용하는 것은 기술적으로는 문제가 없지만 성능 관점에서는 대부분 좋지 않은 선택
- 모든 레코드를 가져온 뒤 직접 필터링할 경우 OOM 에러가 발생할 수 있음
- 처음부터 필요한 것만 데이터베이스에서 검색하는 것이 레코드를 필터링하는 것보다 성능 면에서 유리함

참고
스프링 시큐리티 인 액션
반응형
'Spring > 스프링 시큐리티 인 액션' 카테고리의 다른 글
[16장] 전역 메서드 보안: 사전 및 사후 권한 부여 (0) | 2025.06.02 |
---|---|
[15장] OAuth 2: JWT와 암호화 서명 사용 (0) | 2025.06.01 |
[14장] OAuth 2: 리소스 서버 구현 (0) | 2025.06.01 |
[13장] OAuth 2: 권한 부여 서버 구현 (0) | 2025.06.01 |
[12장] OAuth 2가 작동하는 방법 (0) | 2025.05.31 |