Method security filtering — @PreFilter, @PostFilter, AuthorizationManager, PermissionEvaluator
Lọc collection theo authority với @PreFilter/@PostFilter, cảnh giác @PostFilter trên large dataset (load-then-filter in-memory), AuthorizationManager API Spring Security 6, custom PermissionEvaluator cho fine-grained domain object authz, @Secured/@RolesAllowed legacy.
TL;DR: @PreFilter / @PostFilter cho phép lọc từng element của collection đầu vào / đầu ra theo SpEL expression — nhưng @PostFilter load tất cả record trước rồi mới filter in-memory, gây memory spike và GC pressure khi dataset lớn. Với collection vượt ~100 phần tử nên filter tại DB (thêm WHERE vào query). AuthorizationManager là API Spring Security 6 thay thế AccessDecisionManager cũ — functional, generic, compose qua allOf/anyOf. PermissionEvaluator cho phép khai báo hasPermission(target, "WRITE") trong SpEL và tập trung logic ownership check vào một class test được. @Secured/@RolesAllowed là annotation legacy — chỉ cần nhận ra khi đọc code cũ.
Bài này tiếp nối AOP proxy và @PreAuthorize — nếu chưa rõ vì sao annotation hoạt động qua proxy, hãy đọc bài đó trước.
1. Scenario — tại sao cần filter collection?
Giả sử TaskFlow có endpoint trả danh sách project của toàn team. Nhưng rule business yêu cầu: USER thường chỉ thấy project họ là thành viên, ADMIN thấy tất cả. Có 3 cách implement — và mỗi cách có trade-off về performance khác nhau:
flowchart LR
A["findAll()"] --> B{"Filter strategy"}
B -->|"@PostFilter in-memory"| C["Load ALL rows<br/>then drop in Java"]
B -->|"Query at DB"| D["WHERE owner=? or member=?<br/>1 round-trip, 1/10 data"]
B -->|"Custom AuthorizationManager"| E["AOP check per invocation<br/>no per-element cost"]
C -->|"Large dataset"| F["Memory spike<br/>GC pressure"]
D -->|"Any size"| G["Constant overhead"]Biết khi nào dùng cách nào là phần cốt lõi bài này.
2. @PostFilter — lọc collection kết quả
@PostFilter cho phép khai báo rule lọc ngay trên method: Spring chạy method → nhận collection kết quả → eval SpEL expression filterObject (mỗi element) → giữ element nếu true, drop nếu false.
// Moi element: chi giu neu user la owner
@PostFilter("filterObject.owner.email == authentication.name")
public List<Project> findAll() {
return repo.findAll(); // load tat ca truoc
}
authentication.name là tên user hiện tại từ SecurityContextHolder. filterObject là biến Spring đặt cho từng element khi eval.
2.1 Cơ chế bên dưới — load hết, drop sau
AOP proxy wrap method findAll(). Sau khi method thật trả về List<Project>, proxy gọi PostFilterAuthorizationMethodInterceptor — nó duyệt qua từng element, eval SpEL, gọi list.remove() cho element trả về false. Collection bị mutate in-place:
sequenceDiagram participant Caller participant Proxy as findAll() Proxy participant Real as findAll() real participant Filter as PostFilterInterceptor Caller->>Proxy: findAll() Proxy->>Real: super.findAll() Real-->>Proxy: List[10000 rows] Proxy->>Filter: filterCollection(list, SpEL, auth) Filter->>Filter: eval per element -- remove false Filter-->>Proxy: List[5 rows] Proxy-->>Caller: List[5 rows]
Vấn đề rõ ràng: 10 000 row đã được load khỏi DB lên JVM heap, chỉ 5 row được trả về caller. 9 995 object sống ngắn trên heap → GC pressure. Network traffic DB → app vô ích.
2.2 Cảnh giác @PostFilter với large dataset
// SAI -- load toan bo bang len heap roi moi drop
@PostFilter("filterObject.owner.email == authentication.name")
public List<Project> findAll() {
return repo.findAll();
}
// DUNG -- dieu kien ownership nam ngay trong SQL
public interface ProjectRepository extends JpaRepository<Project, Long> {
List<Project> findByOwnerEmail(String ownerEmail);
// Hoac voi member check:
@Query("SELECT p FROM Project p WHERE p.owner.email = :email OR :email MEMBER OF p.memberEmails")
List<Project> findAccessibleByEmail(@Param("email") String email);
}
Rule of thumb: @PostFilter chỉ chấp nhận được với collection bound nhỏ cố định — dưới ~100 phần tử (top-10, recent-5, menu). Dataset không biết trước kích thước, hoặc hot path nhiều request/giây, thì bắt buộc filter tại DB: khi query có WHERE, DB engine dùng index và chỉ trả về row user có quyền — heap không bị spike.
Cost cụ thể: với 10 000 rows, mỗi Project object ~2 KB, @PostFilter tốn 10 000 × 2 KB = 20 MB heap chỉ để filter ra 5 row. Với 100 concurrent request cùng lúc, đó là 2 GB heap spike.
| Kích thước collection | Quyết định |
|---|---|
| Dưới ~100 element cố định | @PostFilter chấp nhận được |
| Vượt ~100 element, hoặc không biết trước | Filter tại DB — thêm WHERE vào query |
| Hot path nhiều request/giây | Filter tại DB bắt buộc — @PostFilter nhân overhead theo RPS |
2.3 Khi nào @PostFilter vẫn hợp lý
@PostFilter có chỗ đứng khi:
- Collection nhỏ cố định (top-10, recent-5, danh sách menu).
- Logic filter quá phức tạp để biểu diễn trong SQL (rule nhiều branch, gọi bean Java).
- Prototype, internal tool — không phải hot path production.
// OK: findTop10 tra ve toi da 10 phan tu — small, fixed bound
@PostFilter("filterObject.isPublic or filterObject.owner.email == authentication.name")
public List<Project> findTop10Recent() {
return repo.findTop10ByOrderByCreatedAtDesc();
}
3. @PreFilter — lọc collection đầu vào
@PreFilter strip các element khỏi input collection trước khi method chạy. Spring eval filterObject cho mỗi element, remove phần tử trả về false.
@PreFilter("filterObject.amount <= 1000000 or hasRole('ADMIN')")
public void processTransfers(List<TransferRequest> transfers) {
// transfers da bi strip truoc khi vao day
transfers.forEach(this::doTransfer);
}
3.1 Vấn đề silent strip
@PreFilter remove element âm thầm — caller không biết một số phần tử bị loại. Nếu caller gửi 10 transfer và mong tất cả được xử lý nhưng 3 bị strip, caller vẫn nghĩ "10 thành công" trong khi thực tế chỉ có 7.
Pattern tốt hơn — throw rõ ràng thay vì silent strip:
// Thay vi @PreFilter silent strip:
@PreAuthorize("#transfers.?[amount > 1000000].size() == 0 or hasRole('ADMIN')")
public void processTransfers(List<TransferRequest> transfers) {
transfers.forEach(this::doTransfer);
}
SpEL ?[...] filter collection inline — .size() == 0 kiểm tra không có element nào vi phạm rule. Nếu có phần tử vi phạm, Spring ném AccessDeniedException rõ ràng thay vì drop ngầm. Caller biết ngay request bị từ chối và lý do.
3.2 Bảng so sánh @PreFilter vs @PostFilter vs query at DB
@PreFilter | @PostFilter | Query at DB | |
|---|---|---|---|
| Thời điểm filter | Trước method (strip input) | Sau method (strip output) | Trong SQL query |
| Load dữ liệu thừa | Không (input collection từ caller) | Có (load all từ DB) | Không |
| Silent behavior | Strip âm thầm — caller không biết | Strip âm thầm | Không trả row → empty/404 |
| Phù hợp dataset lớn | Không phụ thuộc kích thước input | Không — memory spike | Có |
| Recommend | Hiếm — prefer throw exception | Small collection only | Default cho mọi size |
4. AuthorizationManager — API Spring Security 6
Spring Security 6 refactor toàn bộ authorization pipeline thành một interface đơn giản:
@FunctionalInterface
public interface AuthorizationManager<T> {
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}
T là context — MethodInvocation cho method security, RequestAuthorizationContext cho URL rule, hoặc custom type cho domain logic. Supplier<Authentication> cho phép defer fetch authentication chỉ khi cần (tối ưu cho rule permit anonymous — không cần load user nếu không check).
4.1 Vì sao thay AccessDecisionManager cũ
Spring Security 5 dùng pattern:
AccessDecisionManager
├── AccessDecisionVoter[] — mỗi voter vote: GRANTED / DENIED / ABSTAIN
└── Strategy: AffirmativeBased / ConsensusBased / UnanimousBased
Nhược điểm: verbose (viết custom rule cần 1 voter class + register), strategy chỉ có 3 mode cố định (không compose linh hoạt), API kế thừa từ Spring Security 2.x thiếu generics.
Spring Security 6 đơn giản hoá bằng functional interface — compose qua allOf/anyOf:
import org.springframework.security.authorization.AuthorizationManagers;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
// OR: ADMIN hoac owner deu duoc
AuthorizationManager<MethodInvocation> rule = AuthorizationManagers.anyOf(
AuthorityAuthorizationManager.hasRole("ADMIN"),
new ProjectOwnerAuthorizationManager(repo)
);
// AND: phai ca authenticated va co plan PAID
AuthorizationManager<MethodInvocation> paidRule = AuthorizationManagers.allOf(
AuthenticatedAuthorizationManager.authenticated(),
new PaidPlanAuthorizationManager(userRepo)
);
4.2 Custom AuthorizationManager implementation
public class ProjectOwnerAuthorizationManager
implements AuthorizationManager<MethodInvocation> {
private final ProjectRepository repo;
public ProjectOwnerAuthorizationManager(ProjectRepository repo) {
this.repo = repo;
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authSupplier,
MethodInvocation invocation
) {
Long projectId = (Long) invocation.getArguments()[0];
String username = authSupplier.get().getName();
boolean isOwner = repo.findById(projectId)
.map(p -> p.getOwner().getEmail().equals(username))
.orElse(false);
return new AuthorizationDecision(isOwner);
}
}
Sử dụng trong URL rule cùng AuthorizationManagers.anyOf:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/projects/{projectId}/**").access(
AuthorizationManagers.anyOf(
AuthorityAuthorizationManager.hasRole("ADMIN"),
new ProjectOwnerAuthorizationManager(projectRepo)
)
)
.anyRequest().authenticated()
);
access(AuthorizationManager) là overload mới Spring Security 6 — thay thế access("SpEL string") deprecated.
5. Custom PermissionEvaluator — ownership check fine-grained
PermissionEvaluator là interface Spring Security cho authorization theo dạng (domain object, permission):
public interface PermissionEvaluator {
// Truyen object truc tiep
boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission);
// Truyen ID + type string — load object trong evaluator
boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission);
}
SpEL hỗ trợ function hasPermission(target, permission) và hasPermission(targetId, targetType, permission) map vào interface này.
5.1 Vì sao cần PermissionEvaluator?
Khi app có nhiều domain object (Project, Task, Comment, Team) và mỗi object có nhiều permission level (READ/WRITE/DELETE/INVITE), khai báo rule trực tiếp trong SpEL sẽ rất dài và không test được:
// SpEL dài, khong test duoc rieng
@PreAuthorize("""
hasRole('ADMIN') or
(hasRole('USER') and #projectId != null and
@projectRepo.findById(#projectId).map(p -> p.getOwner().getEmail()).orElse('').equals(authentication.name))
""")
public void update(Long projectId, ProjectUpdate update) { ... }
PermissionEvaluator tập trung logic vào một class Java test được:
// SpEL ngan, ro rang
@PreAuthorize("hasPermission(#projectId, 'Project', 'WRITE')")
public void update(Long projectId, ProjectUpdate update) { ... }
5.2 Implement PermissionEvaluator
@Component
@RequiredArgsConstructor
public class AppPermissionEvaluator implements PermissionEvaluator {
private final ProjectRepository projectRepo;
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
if (target instanceof Project p) {
return checkProject(auth, p, (String) permission);
}
return false;
}
@Override
public boolean hasPermission(
Authentication auth,
Serializable targetId,
String targetType,
Object permission
) {
return switch (targetType) {
case "Project" -> projectRepo.findById((Long) targetId)
.map(p -> checkProject(auth, p, (String) permission))
.orElse(false);
default -> false;
};
}
private boolean checkProject(Authentication auth, Project p, String permission) {
boolean isAdmin = hasRole(auth, "ADMIN");
boolean isOwner = p.getOwner().getEmail().equals(auth.getName());
boolean isMember = p.getMembers().stream()
.anyMatch(m -> m.getEmail().equals(auth.getName()));
return switch (permission) {
case "READ" -> isAdmin || isOwner || isMember;
case "WRITE" -> isAdmin || isOwner;
case "DELETE" -> isAdmin;
default -> false;
};
}
private boolean hasRole(Authentication auth, String role) {
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_" + role));
}
}
5.3 Register evaluator vào expression handler
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler expressionHandler(
AppPermissionEvaluator evaluator
) {
DefaultMethodSecurityExpressionHandler handler =
new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(evaluator);
return handler;
}
}
Không có bean này, hasPermission(...) trong SpEL luôn trả về false — silent failure nguy hiểm.
5.4 Sử dụng trong annotation
@Service
@RequiredArgsConstructor
public class ProjectService {
private final ProjectRepository repo;
// Truyen ID — evaluator tu load object
@PreAuthorize("hasPermission(#projectId, 'Project', 'READ')")
public Project findById(Long projectId) {
return repo.findById(projectId).orElseThrow();
}
// Truyen object truc tiep — bo qua 1 DB query trong evaluator
@PreAuthorize("hasPermission(#project, 'WRITE')")
public void update(Project project, ProjectUpdate update) { ... }
@PreAuthorize("hasPermission(#projectId, 'Project', 'DELETE')")
public void delete(Long projectId) {
repo.deleteById(projectId);
}
}
5.5 Pitfall — hasPermission return false khi quên register
Nếu thiếu MethodSecurityExpressionHandler bean (section 5.3), Spring dùng DenyAllPermissionEvaluator mặc định → mọi hasPermission(...) đều trả false → mọi request bị 403 dù user có quyền. Kiểm tra bằng test:
@Test
@WithMockUser(username = "[email protected]", roles = "USER")
void findById_owner_shouldSucceed() {
Long id = seedProject("[email protected]");
// Neu 403 o day: kiem tra MethodSecurityExpressionHandler bean da duoc register chua
assertThatCode(() -> projectService.findById(id)).doesNotThrowAnyException();
}
6. @Secured và @RolesAllowed — annotation legacy
Trước @PreAuthorize, Spring có 2 annotation đơn giản hơn. Học để nhận ra khi đọc code cũ — không khuyến nghị viết mới.
6.1 @Secured — Spring 1.x era
// Bat: @EnableMethodSecurity(securedEnabled = true)
@Secured("ROLE_ADMIN")
public void deleteAll() { ... }
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public Project create(CreateProjectRequest req) { ... }
Chỉ nhận role string (phải có prefix ROLE_). Không hỗ trợ SpEL, không truy cập method args, không call bean.
6.2 @RolesAllowed — JSR-250 chuẩn Jakarta
// Bat: @EnableMethodSecurity(jsr250Enabled = true)
@RolesAllowed("ADMIN")
public void deleteAll() { ... }
@RolesAllowed({"USER", "ADMIN"})
public Project create(...) { ... }
@PermitAll
public List<Project> listPublic() { ... }
@DenyAll
public void deprecatedEndpoint() { ... }
Là chuẩn Jakarta EE — không cần prefix ROLE_. Portable giữa Spring và CDI container khác. Cũng không có SpEL.
6.3 Khi nào chọn cái nào
| Annotation | Khi nên chọn |
|---|---|
@PreAuthorize | Default — SpEL, method args, custom bean |
@Secured | Code legacy Spring 2-3, không cần SpEL |
@RolesAllowed | Cần portable giữa Spring và CDI/Jakarta |
99% project mới dùng @PreAuthorize độc quyền. Hai annotation kia chỉ cần nhận ra khi đọc code legacy.
Cơ chế bên dưới — vì sao @PostFilter bad cho large dataset
Bản chất vấn đề nằm ở vị trí của PostFilterAuthorizationMethodInterceptor trong luồng AOP. Interceptor chạy sau khi JpaRepository.findAll() trả về — tức là sau khi Hibernate đã sinh SQL, DB đã execute, kết quả đã được map thành object và đưa lên heap Java.
flowchart TB SQL["SQL: SELECT * FROM projects<br/>(DB nhan query, tra 10000 row)"] Hydrate["Hibernate hydrate<br/>10000 Project object len heap"] AOP["PostFilter AOP interceptor<br/>eval SpEL per element<br/>drop 9995 object"] GC["GC phai thu 9995 object<br/>vua tao xong"] Return["Tra 5 object cho caller"] SQL --> Hydrate --> AOP --> GC --> Return
Đây là lý do @PostFilter tệ về performance khi dataset lớn — nó không thể "can thiệp" vào SQL generation. Interceptor chỉ chạy sau khi result set đã được materialise hoàn toàn trên JVM heap.
Ngược lại, filter tại DB bằng Spring Data query method (findByOwnerEmail) hoặc @Query với WHERE thêm điều kiện vào SQL trước khi DB execute — DB engine dùng index để trả đúng row, không load thừa.
@PostFilter kết hợp với Pageable là double anti-pattern: findAll(Pageable) load N row theo page → filter in-memory → page trả về ít hơn N row. Caller thấy "page 1 có 3 item" dù page size=20, không hiểu vì sao. Fix dứt khoát: đưa điều kiện ownership vào query, không dùng @PostFilter trên paginated endpoint.
Liên hệ các bài khác
- AOP proxy và @PreAuthorize: bài này giải thích vì sao annotation hoạt động qua proxy, self-call bypass, CGLIB vs JDK proxy — đọc trước để hiểu cơ sở của
@PreFilter/@PostFiltercùng pipeline AOP interceptor. - CORS — Same-Origin và preflight: bài tiếp theo — bảo vệ app khỏi cross-origin request không hợp lệ; CORS config thường đi kèm với method security trong Spring Security filter chain.
- @PreAuthorize & AOP proxy: cùng pipeline interceptor với
@PreFilter/@PostFilter; bài đó mổ cơ chế proxy còn bài này đào sâu performance trade-off khi filter collection và ownership pattern.
Tóm tắt
@PostFilterload tất cả element rồi filter in-memory — chỉ phù hợp collection dưới ~100 phần tử. Dataset lớn nên filter tại DB (findByOwnerEmail,@QueryvớiWHERE).@PreFilterstrip element đầu vào âm thầm — caller không biết item bị loại. Prefer@PreAuthorizevới SpEL?[...]để throw exception rõ ràng thay vì silent drop.AuthorizationManager<T>(Spring Security 6) thayAccessDecisionManager(SS5) — functional interface, generic, compose quaallOf/anyOf, lazy authentication.PermissionEvaluatortập trung logic ownership vào class Java test được —hasPermission(#id, 'Project', 'WRITE')trong SpEL gọi evaluator theo (targetId, targetType, permission).- Quên register
MethodSecurityExpressionHandlerbean →hasPermissionluôn false → mọi request bị 403 (silent failure). @Secured/@RolesAllowedlà legacy — không SpEL, không method args. Dùng@PreAuthorizecho code mới.
Tự kiểm tra
Q1Giải thích vì sao @PostFilter("filterObject.owner.email == authentication.name") gây vấn đề performance khi table projects có 50 000 row. Vấn đề nằm ở đâu trong luồng xử lý?▸
@PostFilter("filterObject.owner.email == authentication.name") gây vấn đề performance khi table projects có 50 000 row. Vấn đề nằm ở đâu trong luồng xử lý?@PostFilter hoạt động ở tầng AOP interceptor — chạy sau khi method thật trả về. Tức là SQL SELECT * FROM projects đã được thực thi, DB đã trả về 50 000 row, Hibernate đã hydrate 50 000 Project object lên heap Java. Chỉ lúc đó interceptor mới eval SpEL từng element và gọi list.remove() cho những element không khớp.
Hệ quả cụ thể với 50 000 row: giả sử mỗi Project ~2 KB, stack heap tạm tối thiểu là 50 000 × 2 KB = 100 MB, chỉ để filter ra vài chục row người dùng own. Với nhiều concurrent request, heap spike nhân theo số lượng — 100 request đồng thời = 10 GB heap chỉ cho việc filter. GC phải thu gom 49 900+ object vừa được tạo xong.
Vấn đề căn bản: @PostFilter không thể can thiệp vào SQL generation vì interceptor chạy sau JpaRepository đã materialise kết quả hoàn toàn. Fix dứt khoát là dùng Spring Data query method hoặc @Query với WHERE owner_email = :email — DB engine dùng index, chỉ trả về row người dùng có quyền, không load thừa gì cả.
Q2Service sau đây có @PreFilter. User gửi 5 transfer (3 dưới 1 triệu, 2 trên 5 triệu). Điều gì xảy ra? Tại sao đây là bad practice và bạn sẽ fix thế nào?▸
@PreFilter. User gửi 5 transfer (3 dưới 1 triệu, 2 trên 5 triệu). Điều gì xảy ra? Tại sao đây là bad practice và bạn sẽ fix thế nào?Spring eval SpEL filterObject.amount <= 1000000 cho từng element trước khi processTransfers chạy. 2 transfer vượt 1 triệu bị remove âm thầm khỏi list — method chỉ nhận 3 element. Caller không biết 2 transfer bị loại, không có exception, không có log warning — silent strip.
Bad practice vì: caller gửi 5 transfer mong đợi tất cả được xử lý (hoặc tất cả thất bại). Nhận response 200 OK trong khi thực tế chỉ 3 được xử lý là hành vi gây nhầm lẫn và khó debug. Đặc biệt nguy hiểm với financial operation.
Fix — dùng @PreAuthorize với SpEL collection projection để throw exception rõ ràng:
@PreAuthorize("#transfers.?[amount > 1000000].size() == 0 or hasRole('ADMIN')")
?[...] filter collection inline, .size() == 0 kiểm tra không có element nào vi phạm. Nếu có phần tử vi phạm, Spring ném AccessDeniedException ngay trước khi method chạy — caller biết rõ request bị từ chối và có thể xử lý đúng (retry với amount nhỏ hơn, hoặc yêu cầu nâng quyền).
Q3Spring Security 6 thay AccessDecisionManager bằng AuthorizationManager. Nêu 3 lý do kỹ thuật và minh hoạ bằng code compose rule "ADMIN hoặc owner project".▸
AccessDecisionManager bằng AuthorizationManager. Nêu 3 lý do kỹ thuật và minh hoạ bằng code compose rule "ADMIN hoặc owner project".3 lý do kỹ thuật:
1. Functional interface — loại bỏ boilerplate: AccessDecisionVoter yêu cầu implement 3 method (vote, supports(ConfigAttribute), supports(Class)). AuthorizationManager chỉ cần 1 method — implement bằng lambda hoặc method reference.
2. Generic type-safe: AuthorizationManager<T> với T = MethodInvocation hoặc RequestAuthorizationContext — không cast Object. Lỗi type bị bắt lúc compile, không phải runtime ClassCastException.
3. Composition tự nhiên qua allOf/anyOf thay vì 3 strategy cố định của AccessDecisionManager (Affirmative/Consensus/Unanimous). Compose rule phức tạp không cần wrapper class:
Code compose "ADMIN hoặc owner":
AuthorizationManagers.anyOf(AuthorityAuthorizationManager.hasRole("ADMIN"), new ProjectOwnerAuthorizationManager(repo))
Sử dụng trong URL rule: http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/projects/{id}/**").access(rule)) — access(AuthorizationManager) là overload Spring Security 6, thay thế access("SpEL string") deprecated. Bonus: Supplier<Authentication> lazy — chỉ fetch user khi rule thực sự cần kiểm tra identity, tối ưu cho rule permit anonymous.
Q4Bạn implement PermissionEvaluator và register vào Spring context nhưng mọi hasPermission(...) trong @PreAuthorize đều trả false dù user có quyền. Debug thế nào?▸
PermissionEvaluator và register vào Spring context nhưng mọi hasPermission(...) trong @PreAuthorize đều trả false dù user có quyền. Debug thế nào?Nguyên nhân phổ biến nhất: thiếu MethodSecurityExpressionHandler bean hoặc bean không được Spring Security pickup. Khi không có bean này, Spring Security dùng DenyAllPermissionEvaluator mặc định — luôn trả false cho mọi hasPermission call, bất kể logic trong evaluator của bạn.
Kiểm tra:
1. Xác nhận config class có @EnableMethodSecurity — thiếu annotation này thì toàn bộ method security là no-op.
2. Xác nhận có @Bean MethodSecurityExpressionHandler tạo DefaultMethodSecurityExpressionHandler và gọi handler.setPermissionEvaluator(evaluator).
3. Đặt breakpoint hoặc log trong hasPermission của evaluator — nếu không vào đây thì evaluator chưa được register. Nếu vào nhưng trả false thì là logic bug.
4. Chạy test đơn giản: @WithMockUser(username="owner") void test() với project seed đúng owner — expect no exception. Nếu 403 → confirm evaluator chưa active. Fix: thêm MethodSecurityExpressionHandler bean vào config class có @EnableMethodSecurity.
Q5So sánh 3 cách implement "user chỉ thấy project họ có quyền": @PostFilter, query filter tại DB, và custom PermissionEvaluator với @PreAuthorize. Cho biết khi nào chọn mỗi cách.▸
@PostFilter, query filter tại DB, và custom PermissionEvaluator với @PreAuthorize. Cho biết khi nào chọn mỗi cách.3 cách và trade-off:
1. @PostFilter in-memory: load tất cả element từ DB lên heap, eval SpEL từng element, drop phần tử không khớp. Đơn giản khai báo nhưng không scale — memory tỷ lệ tuyến tính với tổng số row, không phải số row user có quyền. Chỉ phù hợp collection nhỏ cố định (dưới ~100 element, không phải hot path).
2. Query filter tại DB (findByOwnerEmail hoặc @Query WHERE): thêm điều kiện ownership vào SQL — DB engine dùng index, chỉ trả về row user có quyền. Heap không bị spike, không load dữ liệu thừa. Recommend cho mọi trường hợp có dataset không giới hạn. Trade-off: cần pass user identity xuống repository layer (qua parameter hoặc @AuthenticationPrincipal).
3. Custom PermissionEvaluator với @PreAuthorize: không dùng cho filter list mà dùng cho single-resource access check (findById, update, delete). Evaluator nhận targetId, load object trong evaluator, check ownership/membership. Phù hợp khi rule phức tạp (nhiều permission level, logic branch theo plan/role/membership). Trade-off: mỗi call là 1 DB query trong evaluator — cần cache cho hot path.
Decision: filter list trên dataset không biết kích thước → query at DB. Filter list có bound nhỏ cố định (top-10, menu, recent) → @PostFilter được. Single-resource access check với rule phức tạp → PermissionEvaluator. Không bao giờ dùng @PostFilter trên paginated endpoint hoặc table lớn.
Bài tiếp theo: CORS — Same-Origin & preflight
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên