이 장에서는 메소드의 매개변수와 반환값을 필터링하는 두 가지 주요 기술을 다룹니다: **사전 필터링(Prefiltering)**과 **사후 필터링(Postfiltering)**입니다.
1. 사전 필터링(Prefiltering)
- 사전 필터링은 메소드가 호출되기 전에 메소드의 매개변수 값이 필터링되는 방식입니다. 주로 메소드가 배열이나 컬렉션을 매개변수로 받을 때 사용됩니다.
- 프레임워크는 정의된 규칙에 따라 매개변수 값들을 검토하고, 조건을 만족하지 않는 값들은 필터링하여 메소드에 전달합니다.
- 예를 들어, 특정 사용자 그룹만 요청을 허용하는 조건을 지정할 수 있습니다. 이때, 필터링은 메소드가 실제로 호출되기 전에 이루어집니다.
2. 사후 필터링(Postfiltering)
- 사후 필터링은 메소드 호출 후 반환된 값에 필터링을 적용하는 방식입니다. 메소드가 배열이나 컬렉션을 반환할 때 유용합니다.
- 반환된 값이 정의된 규칙에 맞지 않으면, 프레임워크는 이를 필터링하여 호출자에게 반환합니다. 이 접근법은 호출 후 반환값을 기준으로 조건을 적용하므로, 메소드가 호출된 후에만 유효합니다.
필터링 vs 권한 부여
- 필터링은 권한 부여와는 다릅니다. 권한 부여는 메소드 호출을 전혀 허용하지 않거나 거부하는 방식으로 작동하는 반면, 필터링은 메소드가 실행되도록 허용하면서, 매개변수나 반환값에 대해 정의된 규칙을 따르지 않는 것만 필터링합니다. 따라서 예외를 발생시키지 않고, 필터링된 값만을 메소드에 전달하거나 반환합니다.
예시
- 사전 필터링 예시: 사용자가 요청한 데이터를 서버에서 받을 때, 사용자가 요청할 수 있는 데이터의 범위가 제한되어 있다면, 사전 필터링을 통해 사용자가 요청한 값이 올바른 범위 내에 있는지 확인하고, 이를 필터링하여 처리합니다.
- 사후 필터링 예시: 메소드가 데이터를 반환할 때, 특정 조건에 맞는 데이터만을 호출자에게 전달하도록 할 수 있습니다. 예를 들어, 반환된 데이터 중 특정 권한을 가진 사용자만 볼 수 있는 정보만 필터링하여 제공하는 방식입니다.
이러한 필터링 기능은 Spring Security의 메소드 보안 기능을 사용하여 구현할 수 있으며, 컬렉션이나 배열에 대해서만 유효합니다.
12.1 사전 필터링 적용 (Prefiltering for Method Authorization)
이 섹션에서는 사전 필터링의 메커니즘을 설명하고, 이를 실제 예제를 통해 구현하는 방법을 다룹니다. 사전 필터링을 사용하면, 메소드의 매개변수로 전달된 값들이 지정된 규칙을 따르는지 확인한 후, 조건을 만족하는 값만 메소드로 전달됩니다.
1. 사전 필터링의 개념
- 사전 필터링은 메소드가 호출되기 전에 매개변수로 제공된 데이터를 검토하여, 정의된 권한 규칙을 따르는 값만 메소드로 전달하는 방법입니다.
- Spring Security는 이를 Aspect를 사용하여 구현합니다. 애스펙트는 메소드 호출을 가로채고, 매개변수 값이 정의된 규칙을 따르지 않으면 이를 필터링합니다.
2. 예제: 제품 판매 시 사용자 소유 제품만 허용
이 예제에서는 사용자가 자신이 소유한 제품만 팔 수 있도록 필터링하는 방법을 보여줍니다. 각 제품은 owner 속성을 가지고 있으며, 이 값은 사용자 이름입니다. 사용자가 제품을 판매할 때, 필터링을 통해 현재 로그인한 사용자만 소유한 제품을 팔 수 있도록 보장합니다.
3. @PreFilter 어노테이션 사용
- @PreFilter 어노테이션을 사용하여 메소드가 받는 매개변수 중 컬렉션 내의 값들만 필터링할 수 있습니다.
- filterObject.owner == authentication.name 식을 사용하여 매개변수로 제공된 제품 중, 현재 인증된 사용자만 소유한 제품을 허용합니다.
4. 프로젝트 구성
- 구성 클래스 (ProjectConfig)에서 테스트 사용자 Nikolai와 Julien을 정의합니다.
- @EnableMethodSecurity 어노테이션을 사용하여 메소드 보안과 필터링 기능을 활성화합니다.
5. ProductService 클래스
@Service
public class ProductService {
@PreFilter("filterObject.owner == authentication.name") // 필터링 조건
public List<Product> sellProducts(List<Product> products) {
return products; // 필터링된 리스트 반환
}
}
- sellProducts 메소드는 로그인한 사용자가 소유한 제품만 받도록 필터링합니다.
6. ProductController 클래스
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.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); // 제품 리스트를 서비스 메소드로 전달
}
}
- /sell 엔드포인트에서는 사용자 Nikolai와 Julien의 제품을 제공하고, 사전 필터링을 통해 사용자에게 소유된 제품만 반환합니다.
7. 필터링 결과
- 사용자가 Nikolai로 인증된 경우, 응답에서 Nikolai가 소유한 제품 두 개만 반환됩니다.
- 사용자가 Julien으로 인증된 경우, Julien이 소유한 제품만 반환됩니다.
8. 불변 컬렉션 문제
- 필터링 애스펙트는 컬렉션을 변경하므로, 불변 컬렉션을 제공하면 예외가 발생합니다. 예를 들어, List.of()로 생성한 불변 리스트를 사용하면, UnsupportedOperationException이 발생합니다. 따라서 가변 컬렉션을 제공해야 합니다.
9. 예외 발생 예시
java.lang.UnsupportedOperationException: null
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:73)
- 이 예외는 불변 컬렉션을 제공했을 때 발생하며, 가변 컬렉션을 사용해야 정상적으로 필터링이 수행됩니다.
결론
사전 필터링은 메소드 호출 전에 매개변수 값을 필터링하여, 정의된 권한 규칙을 따르지 않는 값을 걸러냅니다. 이 방식은 비즈니스 로직과 권한 부여 규칙을 분리하여 코드의 유지보수성을 향상시키며, 필터링 규칙을 애스펙트로 처리함으로써 코드의 명확성을 높입니다.
12.2 메소드 권한 부여를 위한 사후 필터링 적용
이 섹션에서는 사후 필터링을 구현하는 방법을 다룹니다. 사후 필터링은 메소드가 데이터를 반환한 후, 반환된 데이터에 대해 필터링을 적용하여 인증된 사용자에게만 적절한 데이터를 제공하는 방식입니다. 예를 들어, 사용자가 자신이 소유한 제품의 세부 정보를 요청할 때, 백엔드는 해당 사용자가 소유한 제품만을 반환하도록 보장해야 합니다.
사후 필터링 동작
사후 필터링은 메소드가 반환한 배열이나 컬렉션에 대해 필터링을 적용합니다. 애스펙트는 메소드 호출을 허용하고, 메소드가 리턴하면 필터링 규칙을 적용하여 반환된 컬렉션에서 인증된 사용자가 소유한 항목만을 남깁니다. 필터링 규칙은 SpEL(Spring Expression Language)을 사용하여 정의됩니다.
예제 구현
예제는 ProductService 클래스에서 제품 목록을 반환하는 findProducts() 메소드를 작성하여 구현합니다. 이 메소드는 인증된 사용자가 소유한 제품만을 반환하도록 사후 필터링을 적용합니다.
ProductService 클래스
@PostFilter 어노테이션은 반환된 컬렉션에 대해 필터링을 적용하는 데 사용됩니다. 아래의 예제에서 @PostFilter 어노테이션은 filterObject.owner == authentication.principal.username 조건을 통해 인증된 사용자의 제품만 반환하도록 설정됩니다.
@Service
public class ProductService {
@PostFilter("filterObject.owner == authentication.principal.username") // A
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;
}
}
- A: @PostFilter 어노테이션을 통해 반환된 리스트에서 인증된 사용자와 동일한 owner 값을 가진 제품만 필터링합니다.
ProductController 클래스
이 메소드를 호출하기 위한 엔드포인트를 제공하는 ProductController 클래스도 작성합니다. @GetMapping을 사용하여 /find 엔드포인트를 정의하고, 사용자가 인증 후 요청한 제품 목록을 필터링된 상태로 반환합니다.
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/find")
public List<Product> findProducts() {
return productService.findProducts();
}
}
테스트
애플리케이션을 실행한 후, /find 엔드포인트를 호출하여 사후 필터링이 잘 적용되었는지 확인할 수 있습니다. 예를 들어, julien으로 인증된 경우 다음과 같은 결과를 기대할 수 있습니다.
curl -u julien:12345 http://localhost:8080/find
응답 본문은 다음과 같이, julien이 소유한 제품만 포함되어야 합니다.
[
{"name":"chocolate","owner":"julien"}
]
반면, nikolai로 인증된 경우, nikolai가 소유한 제품만 필터링되어 반환됩니다.
주요 특징
- 필터링 조건: @PostFilter는 컬렉션의 각 요소를 검사하고 인증된 사용자와 동일한 owner 속성을 가진 항목만 반환하도록 설정됩니다.
- 보안 처리: authentication.principal.username을 사용하여 현재 인증된 사용자의 이름을 가져와 비교합니다.
- 동작: 필터링은 메소드가 반환한 후에 적용되며, 필터링된 결과만 클라이언트에 전달됩니다.
12.3 Using Filtering in Spring Data Repositories
이 섹션에서는 Spring Data 리포지토리에서 필터링을 어떻게 적용할 수 있는지 다룹니다. Spring Data를 사용하면 SQL이나 NoSQL 데이터베이스에서 데이터를 효율적으로 관리하고 쿼리할 수 있습니다. 필터링을 적용하는 방법은 크게 두 가지로 나눠볼 수 있습니다: @PreFilter와 @PostFilter 어노테이션을 사용하거나, 쿼리 내에서 직접 필터링 조건을 작성하는 방법입니다.
1. @PreFilter와 @PostFilter 어노테이션 사용
- @PreFilter는 메소드가 실행되기 전에 컬렉션이나 배열을 필터링합니다.
- @PostFilter는 메소드 실행 후 반환된 컬렉션이나 배열을 필터링합니다.
하지만 @PostFilter를 Spring Data 리포지토리에서 사용하는 경우 성능 문제가 발생할 수 있습니다. 예를 들어, findAll() 메소드를 사용해 모든 문서를 검색한 후 필터링을 한다면, 데이터베이스에서 모든 레코드를 로드하게 되어 메모리 사용량이 급증하거나 성능이 저하될 수 있습니다. 따라서, 필터링을 리포지토리에서 직접 쿼리로 적용하는 방법이 더 효율적입니다.
2. 쿼리 내에서 직접 필터링 적용
필요한 데이터를 데이터베이스에서 처음부터 정확히 선택하도록 쿼리를 작성하는 것이 더 바람직합니다. 예를 들어, Spring Data JPA를 사용하면, 쿼리 메소드에 @Query 어노테이션을 활용하여 조건을 추가할 수 있습니다. 쿼리 내에서 authentication.name을 사용해 로그인한 사용자와 관련된 데이터만 필터링할 수 있습니다.
예제: Spring Data 리포지토리에서 필터링
설정 클래스
@Configuration
@EnableMethodSecurity
public class ProjectConfig {
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
Product 엔티티
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String owner;
// Getters and setters omitted
}
ProductRepository (SpEL을 사용한 쿼리)
public interface ProductRepository extends JpaRepository<Product, Integer> {
@Query("SELECT p FROM Product p WHERE p.name LIKE %:text% AND p.owner=?#{authentication.name}")
List<Product> findProductByNameContains(String text);
}
ProductController
@RestController
public class ProductController {
private final ProductRepository productRepository;
// Constructor omitted
@GetMapping("/products/{text}")
public List<Product> findProductsContaining(@PathVariable String text) {
return productRepository.findProductByNameContains(text);
}
}
동작 테스트
애플리케이션을 실행한 후, /products/{text} 엔드포인트를 호출하여 특정 제품을 검색하고 필터링된 결과만 반환되는지 확인할 수 있습니다. 예를 들어, Nikolai라는 사용자로 인증 후, 문자 c가 포함된 제품을 검색하면, 인증된 사용자가 소유한 candy 제품만 반환됩니다.
curl -u nikolai:12345 http://localhost:8080/products/c
응답 예시:
[
{"id": 2, "name": "candy", "owner": "nikolai"}
]
이 방법은 Spring Data 리포지토리에서 직접 쿼리 조건을 추가하여, 데이터베이스에서 필요한 데이터만 가져오는 방식으로 성능을 최적화할 수 있습니다.
12.4 Summary
필터링은 메서드의 파라미터나 리턴 값에 대해 유효성을 검증하고, 정의된 기준을 충족하지 않는 값을 제외하는 권한 부여 접근 방식입니다. 필터링은 메서드의 실행을 제한하지 않으며, 주로 메서드의 입력값과 출력값을 제어하는 데 집중합니다.
필터링의 주요 개념:
- @PreFilter 어노테이션은 메서드의 파라미터로 전달된 컬렉션이나 배열에서 특정 조건을 충족하지 않는 요소들을 필터링합니다. 이 어노테이션은 메서드 파라미터가 컬렉션 또는 배열일 때 사용되며, SpEL(Spring Expression Language)을 통해 조건을 정의할 수 있습니다.
- @PostFilter 어노테이션은 메서드가 리턴하는 값들을 필터링합니다. 이 경우 메서드의 리턴 타입은 컬렉션 또는 배열이어야 하며, 리턴된 값들이 어노테이션의 규칙을 따라 필터링됩니다.
Spring Data Repository에서 필터링:
- @PostFilter를 Spring Data 리포지토리 메서드에 사용하는 것은 권장되지 않습니다. 그 이유는 데이터베이스에서 데이터를 필터링하는 것이 애플리케이션에서 필터링하는 것보다 성능이 더 좋기 때문입니다. @PostFilter를 사용하면 먼저 데이터베이스에서 모든 데이터를 검색하고, 그 후에 애플리케이션에서 필터링을 하게 되어 메모리 사용량이나 성능에 문제를 일으킬 수 있습니다.
- 대신, Spring Security를 활용하여 Spring Data와 통합하고, 쿼리 내에서 직접 **Spring Expression Language (SpEL)**을 사용하여 권한에 맞는 데이터를 처음부터 필터링하는 것이 더 효율적입니다.
핵심 요약:
- 필터링은 메서드 입력값과 리턴값을 제한하여 인가된 값만 처리하도록 보장합니다.
- @PreFilter는 메서드 파라미터의 조건을 설정하여 불필요한 값을 필터링하며, @PostFilter는 메서드의 리턴 값을 필터링합니다.
- Spring Data 리포지토리에서 데이터를 필터링할 때는 데이터베이스 레벨에서 필터링을 하는 것이 성능상 유리합니다.
'Spring Security in Action' 카테고리의 다른 글
13장 What are OAuth 2 and OpenID Connect? (1) | 2024.12.01 |
---|---|
11장 Implement authorization at the method level (1) | 2024.12.01 |
10장 Configuring Cross-Origin Resource Sharing(CORS) (0) | 2024.12.01 |
9장 Configuring Cross-Site Request Forgery(CSRF) protection (0) | 2024.12.01 |
8장 Configuring endpoint-level authorization: Applying restrictions (0) | 2024.12.01 |