Lọc Dữ Liệu Method Security và PermissionEvaluator
Khám phá @PreFilter, @PostFilter, rủi ro tải dữ liệu in-memory và triển khai phân quyền chi tiết với PermissionEvaluator trong Spring Security 6.
TL;DR: @PreFilter và @PostFilter hỗ trợ lọc từng phần tử trong tập dữ liệu đầu vào hoặc đầu ra theo biểu thức SpEL. Tuy nhiên, @PostFilter tải toàn bộ bản ghi từ cơ sở dữ liệu lên bộ nhớ rồi mới loại bỏ phần tử vi phạm, dẫn đến nguy cơ tràn bộ nhớ heap (OOM) và gây áp lực thu gom rác (GC) lớn trên các tập dữ liệu thực tế. Với danh sách lớn, giải pháp chuẩn mực là đẩy điều kiện lọc xuống câu lệnh SQL (WHERE). Để phân quyền chi tiết theo từng đối tượng nghiệp vụ (Domain Object Security), Spring Security cung cấp PermissionEvaluator giúp cô lập logic kiểm tra quyền vào một lớp Java độc lập, kết hợp cùng kiến trúc AuthorizationManager hiện đại trong Spring Security 6.
Tại sao việc lọc tập dữ liệu bằng @PostFilter lại tiềm ẩn rủi ro hiệu năng?
Trong các ứng dụng thực tế, bài toán phổ biến là người dùng thông thường chỉ được xem các dự án mà họ là thành viên, trong khi quản trị viên có quyền xem toàn bộ. Annotation @PostFilter cho phép đặt điều kiện lọc ngay trên phương thức truy vấn:
// Spring se duyet tung phan tu trong collection sau khi method chay xong
@PostFilter("filterObject.owner.email == authentication.name or hasRole('ADMIN')")
public List<Project> findAll() {
return projectRepository.findAll(); // Truy van toan bo bang du lieu
}
Trong biểu thức SpEL trên:
authentication.nameđại diện cho định danh người dùng hiện tại lấy từSecurityContextHolder.filterObjectlà biến ngữ cảnh đại diện cho từng phần tử đang được duyệt qua trong danh sách kết quả.
Cơ chế load-then-filter và áp lực bộ nhớ
AOP Proxy bọc ngoài phương thức findAll(). Sau khi phương thức gốc hoàn thành và trả về danh sách, PostFilterAuthorizationMethodInterceptor sẽ duyệt qua từng phần tử, đánh giá SpEL và gọi Iterator.remove() đối với phần tử không thỏa mãn:

Nếu bảng dữ liệu có 10.000 bản ghi nhưng người dùng chỉ sở hữu 5 bản ghi:
- ORM (Hibernate) vẫn thực thi
SELECT * FROM projects, kéo toàn bộ 10.000 dòng qua mạng. - 10.000 đối tượng Java được khởi tạo và chiếm dụng khoảng 20 MB bộ nhớ heap.
- Interceptor loại bỏ 9.995 đối tượng vừa tạo để trả về 5 đối tượng.
- Bộ thu gom rác (Garbage Collector) phải dọn dẹp hàng nghìn đối tượng ngắn ngày.
Khi có 100 yêu cầu đồng thời, hệ thống sẽ đối mặt với đợt tăng đột biến 2 GB heap, gây nguy cơ treo ứng dụng do OutOfMemoryError.
Chiến lược lựa chọn phương án lọc dữ liệu
Chỉ sử dụng @PostFilter khi tập dữ liệu có kích thước nhỏ cố định (dưới 100 phần tử như Top 10, Menu điều hướng) hoặc quy tắc nghiệp vụ quá phức tạp không thể viết bằng SQL. Khi tập dữ liệu không giới hạn kích thước, bắt buộc lọc bằng câu lệnh truy vấn cơ sở dữ liệu.
| Đặc điểm đối chiếu | @PostFilter (In-memory) | Lọc tại Database (WHERE) |
|---|---|---|
| Vị trí xử lý | Sau khi phương thức trả về kết quả | Ngay trong câu lệnh SQL tại Database Engine |
| Mức tiêu hao Heap | Tỉ lệ thuận với tổng số bản ghi trong bảng | Tỉ lệ thuận với số bản ghi người dùng sở hữu |
| Sử dụng Database Index | Không tận dụng được index cho quyền truy cập | Tối ưu hóa triệt để qua chỉ mục index |
| Hỗ trợ phân trang | Phá vỡ kích thước trang (Page size bị hụt) | Tương thích hoàn hảo với Pageable |
Giải pháp tối ưu tại tầng cơ sở dữ liệu:
public interface ProjectRepository extends JpaRepository<Project, Long> {
// Loc chinh xac tai database qua menh de WHERE
@Query("""
SELECT p FROM Project p
WHERE p.owner.email = :email
OR :email MEMBER OF p.memberEmails
""")
List<Project> findAccessibleProjects(@Param("email") String email);
}
Cơ chế hoạt động của @PreFilter và giải pháp thay thế ném ngoại lệ là gì?
Ngược lại với @PostFilter, annotation @PreFilter can thiệp vào tham số đầu vào là Collection trước khi phương thức nghiệp vụ bắt đầu thực thi:
// Loc bo nhung yeu cau khong hop le truoc khi xu ly
@PreFilter("filterObject.amount <= 1000000 or hasRole('ADMIN')")
public void processTransfers(List<TransferRequest> transfers) {
transfers.forEach(this::executeTransfer);
}
Hiện tượng Silent Strip và rủi ro nghiệp vụ
@PreFilter âm thầm xóa bỏ các phần tử không thỏa mãn khỏi danh sách đầu vào mà không đưa ra cảnh báo. Nếu phía client gửi lên danh sách 10 giao dịch nhưng có 2 giao dịch bị loại bỏ, API vẫn trả về mã trạng thái 200 OK. Phía gọi ngầm hiểu toàn bộ 10 giao dịch đã thành công, dẫn đến sai lệch dữ liệu tài chính nghiêm trọng.
Thay thế bằng @PreAuthorize và SpEL Collection Projection
Để thông báo lỗi rõ ràng, ta sử dụng cú pháp lọc tập hợp của SpEL (.?[...]) trong @PreAuthorize để từ chối toàn bộ yêu cầu nếu phát hiện bất kỳ phần tử nào vi phạm:
// Nem AccessDeniedException neu co bat ky phan tu nao vuot han muc
@PreAuthorize("#transfers.?[amount > 1000000].size() == 0 or hasRole('ADMIN')")
public void processTransfers(List<TransferRequest> transfers) {
transfers.forEach(this::executeTransfer);
}
Biểu thức #transfers.?[amount > 1000000] lọc ra danh sách các giao dịch vượt hạn mức. Nếu kích thước danh sách này lớn hơn 0, Spring Security lập tức ném AccessDeniedException và chặn đứng luồng xử lý.
Kiến trúc AuthorizationManager trong Spring Security 6 thay thế hệ thống cũ như thế nào?
Spring Security 6 đã loại bỏ hoàn toàn hệ thống bầu chọn phức tạp AccessDecisionManager và các AccessDecisionVoter của phiên bản 5, thay thế bằng giao diện hàm tinh gọn AuthorizationManager<T>:
@FunctionalInterface
public interface AuthorizationManager<T> {
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}
Các ưu điểm vượt trội của AuthorizationManager
- Giao diện hàm linh hoạt (Functional Interface): Dễ dàng triển khai thông qua cú pháp Lambda hoặc Method Reference mà không cần kế thừa nhiều lớp phức tạp.
- Hỗ trợ Generic an toàn kiểu:
Tcó thể làMethodInvocationcho bảo mật phương thức, hoặcRequestAuthorizationContextcho phân quyền URL. - Cơ chế tải trễ (Lazy Authentication): Nhận vào
Supplier<Authentication>, chỉ thực hiện truy vấn thông tin định danh khi quy tắc bảo mật thực sự cần kiểm tra, tối ưu hóa các endpoint công khai. - Kết hợp linh hoạt qua allOf và anyOf: Cho phép ghép nối các điều kiện phân quyền độc lập mà không cần tạo wrapper class.
// Ket hop quy tac: Hoac la ADMIN hoac la chu so huu du an
AuthorizationManager<MethodInvocation> projectRule = AuthorizationManagers.anyOf(
AuthorityAuthorizationManager.hasRole("ADMIN"),
new ProjectOwnerAuthorizationManager(projectRepository)
);
Triển khai phân quyền chi tiết qua PermissionEvaluator và biểu thức hasPermission ra sao?
Khi ứng dụng mở rộng với nhiều thực thể nghiệp vụ (Project, Task, Comment) và các cấp độ thao tác đa dạng (READ, WRITE, DELETE), việc nhúng toàn bộ logic kiểm tra vào SpEL sẽ khiến code khó bảo trì:
// SpEL qua dai, kho doc va khong the viet Unit Test doc lap
@PreAuthorize("""
hasRole('ADMIN') or
(@projectRepository.findById(#id).orElse(null)?.ownerEmail == authentication.name)
""")
public void updateProject(Long id, ProjectRequest request) { ... }
Giao diện PermissionEvaluator của Spring Security giải quyết bài toán này bằng cách chuẩn hóa cú pháp kiểm tra quyền hạn theo mô hình: (Target Object/ID, Permission).
public interface PermissionEvaluator {
boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission);
boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission);
}
Xây dựng lớp AppPermissionEvaluator chuẩn mực
@Component
public class AppPermissionEvaluator implements PermissionEvaluator {
private final ProjectRepository projectRepository;
public AppPermissionEvaluator(ProjectRepository projectRepository) {
this.projectRepository = projectRepository;
}
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
if (target instanceof Project project) {
return checkProjectPermission(auth, project, (String) permission);
}
return false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
if ("Project".equals(targetType)) {
return projectRepository.findById((Long) targetId)
.map(project -> checkProjectPermission(auth, project, (String) permission))
.orElse(false);
}
return false;
}
private boolean checkProjectPermission(Authentication auth, Project project, String permission) {
if (hasRole(auth, "ROLE_ADMIN")) return true;
String username = auth.getName();
boolean isOwner = project.getOwnerEmail().equals(username);
boolean isMember = project.getMemberEmails().contains(username);
return switch (permission) {
case "READ" -> isOwner || isMember;
case "WRITE", "DELETE" -> isOwner;
default -> false;
};
}
private boolean hasRole(Authentication auth, String role) {
return auth.getAuthorities().stream()
.anyMatch(granted -> granted.getAuthority().equals(role));
}
}
Sau khi triển khai, các phương thức nghiệp vụ chỉ cần sử dụng biểu thức SpEL ngắn gọn và rõ nghĩa:
@Service
public class ProjectService {
// Truyen ID va loai doi tuong
@PreAuthorize("hasPermission(#projectId, 'Project', 'WRITE')")
public void updateTitle(Long projectId, String newTitle) { ... }
// Truyen truc tiep thuc the da co san de tranh truy van duplicate
@PreAuthorize("hasPermission(#project, 'DELETE')")
public void remove(Project project) { ... }
}
Vì sao biểu thức hasPermission có thể luôn trả về false và cách chẩn đoán lỗi?
Một lỗi thường gặp khi triển khai PermissionEvaluator là mọi lời gọi hasPermission đều bị từ chối với mã lỗi 403 Forbidden, dù logic kiểm tra hoàn toàn chính xác.
Mô hình chẩn đoán ba tầng xác định nguyên nhân:

Nguyên nhân cốt lõi là Spring Security sử dụng DenyAllPermissionEvaluator làm cấu hình mặc định. Để kích hoạt evaluator tùy biến, bạn bắt buộc phải đăng ký bean MethodSecurityExpressionHandler:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
AppPermissionEvaluator permissionEvaluator
) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
Trình tự chẩn đoán lỗi khi gặp 403 Forbidden
- Kiểm tra
@EnableMethodSecurity: Đảm bảo annotation này đã hiện diện trên lớp cấu hình chính. - Kiểm tra đăng ký Handler Bean: Xác nhận
MethodSecurityExpressionHandlerđã được đưa vào Application Context và đã gọisetPermissionEvaluator. - Đặt điểm dừng (Breakpoint): Kiểm tra xem phương thức
hasPermission()trong evaluator có được kích hoạt khi request đi qua hay không.
Các annotation tiền thân @Secured và @RolesAllowed khác biệt gì so với @PreAuthorize?
Trong các hệ thống kế thừa (Legacy Systems), bạn có thể bắt gặp các annotation phân quyền cũ hơn. Việc hiểu rõ bản chất giúp bạn bảo trì và chuyển đổi mã nguồn hiệu quả:
| Tiêu chí | @PreAuthorize | @RolesAllowed | @Secured |
|---|---|---|---|
| Nguồn gốc | Spring Security 3+ | Chuẩn JSR-250 (Jakarta) | Spring Security 1.x / 2.x |
| Kích hoạt | Mặc định trong @EnableMethodSecurity | jsr250Enabled = true | securedEnabled = true |
| Cú pháp tiền tố | Nhận ADMIN trong hasRole('ADMIN') | Nhận trực tiếp ADMIN | Bắt buộc tiền tố ROLE_ADMIN |
| Hỗ trợ SpEL | Đầy đủ (Method args, Bean, Logic) | Không hỗ trợ | Không hỗ trợ |
Khuyến nghị hiện đại: Luôn sử dụng @PreAuthorize làm lựa chọn mặc định duy nhất cho các dự án mới nhằm đảm bảo tính linh hoạt tối đa.
Những điểm then chốt cần ghi nhớ là gì?
- Luôn phân biệt ranh giới giữa lọc dữ liệu in-memory (
@PostFilter) và lọc tại cơ sở dữ liệu (WHERE). - Không sử dụng
@PostFiltertrên các API có phân trang (Pageable) vì sẽ làm sai lệch kích thước trang. - Tránh cạm bẫy Silent Strip của
@PreFilterbằng cách sử dụng@PreAuthorizevới SpEL.?[...]để chủ động ném ngoại lệ khi có dữ liệu vi phạm. - Tận dụng
PermissionEvaluatorđể đóng gói logic phân quyền đối tượng nghiệp vụ phức tạp, giúp mã nguồn tầng Service luôn trong sáng. - Bắt buộc đăng ký
MethodSecurityExpressionHandlerđể liên kết custom evaluator vào Spring Security Context.
Bạn đã làm chủ cơ chế lọc và PermissionEvaluator chưa?
- Q1Tại sao việc sử dụng @PostFilter trên một bảng dữ liệu lớn (ví dụ 100.000 dòng) lại bị coi là một anti-pattern nghiêm trọng về hiệu năng?
- Q2Hiện tượng 'Silent Strip' khi sử dụng @PreFilter trên danh sách tham số đầu vào gây ra rủi ro gì?
- Q3Cú pháp SpEL nào cho phép kiểm tra danh sách đầu vào và ném ngoại lệ ngay lập tức nếu có bất kỳ phần tử nào vi phạm điều kiện số tiền?
- Q4Nếu triển khai Custom PermissionEvaluator nhưng quên cấu hình MethodSecurityExpressionHandler, điều gì sẽ xảy ra khi gọi hasPermission trong SpEL?
- Q5Tại sao kiến trúc AuthorizationManager trong Spring Security 6 lại ưu tiên nhận tham số Supplier<Authentication> thay vì trực tiếp đối tượng Authentication?
Bài tiếp theo: CORS — Cơ Chế Bảo Mật Trình Duyệt và Cấu Hình
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
@PreAuthorize & AOP proxy — cơ chế method security
Bài tiếpCORS — Same-Origin Policy, Preflight và Spring Security
Lộ trình Junior Java Backend Developer
Chặng 18/21 · Authentication & Authorization — bài 15/19 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này