@PreAuthorize & AOP proxy — cơ chế method security
Method security hoạt động qua AOP proxy: Spring wrap bean trong CGLIB proxy, intercept method call, eval SpEL trước khi delegate. Bài này bóc đúng cơ chế đó — proxy là gì, vì sao self-call bypass annotation, và 3 cách fix — để bạn không mất 2 giờ debug 'annotation không có hiệu lực'.
TL;DR: @PreAuthorize hoạt động vì Spring không trả thẳng bean thật cho caller — nó trả CGLIB proxy wrap bean thật. Mọi lời gọi method từ bên ngoài đi qua proxy; proxy eval SpEL expression, throw AccessDeniedException nếu fail, rồi mới delegate đến method gốc. @PostAuthorize làm ngược lại — method chạy trước, check sau với returnObject. Pitfall số 1: this.method() bên trong class đi thẳng đến bean thật, bỏ qua proxy — annotation mất tác dụng hoàn toàn, không lỗi, không cảnh báo. Ba fix: tách class, inject self bean, hoặc đẩy annotation lên entry point.
1. Vì sao cần method security — hai entry point, một service
Giả sử ProjectService.deleteAll() chỉ admin mới được gọi. Bạn đã có URL rule:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/projects/all").hasRole("ADMIN")
// ...
);
Nhưng sau vài sprint, service bắt đầu được gọi từ nhiều chỗ ngoài REST:
- Scheduled job mỗi đêm dọn project cũ
- Message listener nhận event từ queue nội bộ
- GraphQL resolver (URL khác
/graphql, URL rule không áp) - Integration test gọi service trực tiếp
URL rule chỉ bảo vệ một cổng vào. Service layer có thể có nhiều cổng. Method security là backstop — đặt rule trực tiếp trên service method, bất kể caller đến từ đâu.
flowchart LR
REST["REST Controller<br/>/api/projects"]
SCHED["Scheduled Job"]
MSG["Message Listener"]
URL["URL Rule<br/>hasRole(ADMIN)"]
SVC["ProjectService<br/>@PreAuthorize(hasRole)"]
DB[(DB)]
REST -->|"qua URL rule"| URL --> SVC
SCHED -->|"bypass URL rule"| SVC
MSG -->|"bypass URL rule"| SVC
SVC --> DB
URL -.->|"403 cho non-admin"| R1[403]
SVC -.->|"AccessDeniedException"| R2[AccessDeniedException]
style URL fill:#fef3c7
style SVC fill:#d1fae5Đây là defense in depth — hai tầng bảo vệ độc lập:
| Tầng | Vị trí check | Phù hợp |
|---|---|---|
URL rule (requestMatchers) | AuthorizationFilter, trước controller | Coarse-grained: cả module admin yêu cầu ROLE_ADMIN |
Method security (@PreAuthorize) | Trong service method, qua AOP proxy | Fine-grained: ownership check, SpEL phức tạp, service gọi từ nhiều entry point |
Hai tầng tồn tại song song, không thay thế nhau. URL rule fail-fast trước (reject 80% case), method security backstop khi URL rule có lỗ hoặc service bị gọi ngoài HTTP.
Xem thêm kiến trúc FilterChain để hiểu URL rule nằm ở AuthorizationFilter — tầng ngoài cùng trước controller.
2. @EnableMethodSecurity — bật pipeline
Trước khi annotation có hiệu lực, phải kích hoạt:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // bat toan bo @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter
public class SecurityConfig {
// ...
}
@EnableMethodSecurity kích hoạt 3 thứ:
MethodSecurityInterceptor— AOP advisor quét bean tìm method có annotation security, đánh dấu cần proxy.AuthorizationManagerbeans — implementation cho từng annotation (PreAuthorizeAuthorizationManager,PostAuthorizeAuthorizationManager).MethodSecurityExpressionHandler— eval SpEL với context đầy đủ (authentication,#paramName,@beanName).
Nếu thiếu @EnableMethodSecurity, mọi annotation method security trở thành no-op — code chạy bình thường như không có check, là silent failure dễ bỏ qua.
Spring Security 6 (Boot 3+) thay @EnableGlobalMethodSecurity cũ bằng @EnableMethodSecurity — nếu migrate từ Boot 2.x cần đổi tên.
3. Cơ chế bên dưới — AOP proxy là gì và cách nó intercept
Một annotation chỉ là metadata trên bytecode — bản thân nó không "chạy" gì. AOP (Aspect-Oriented Programming) là pattern cho phép bổ sung behavior vào method mà không sửa code method. Spring làm cho @PreAuthorize có hiệu lực qua cơ chế này.
3.1 Spring trả proxy, không trả bean thật
Khi container khởi tạo bean, nó quét class tìm annotation AOP (@PreAuthorize, @Transactional, @Async). Phát hiện thấy annotation, container sinh một proxy class wrap class gốc. Caller nhận proxy, không nhận bean thật:
flowchart LR
Caller["Controller / Test / Scheduler"]
Proxy["ProjectService$$EnhancerByCGLIB<br/>(proxy sinh runtime)"]
Real["ProjectService<br/>(bean that)"]
Mgr["AuthorizationManager<br/>eval SpEL"]
Caller -->|"projectService.deleteAll()"| Proxy
Proxy -->|"1. check()"| Mgr
Mgr -->|"granted"| Proxy
Proxy -->|"2. delegate"| Real
Real -->|"return"| Proxy
Proxy -->|"return"| Caller
style Proxy fill:#fef3c7
style Mgr fill:#dbeafePseudo-code proxy Spring sinh:
// Class sinh runtime boi CGLIB — khong co trong source code cua ban
public class ProjectService$$EnhancerByCGLIB extends ProjectService {
private final AuthorizationManager<MethodInvocation> authzManager;
@Override
public void deleteAll() {
// 1. Lay Authentication tu SecurityContext
Supplier<Authentication> authSupplier =
() -> SecurityContextHolder.getContext().getAuthentication();
// 2. Eval SpEL: "hasRole('ADMIN')"
AuthorizationDecision decision = authzManager.check(authSupplier, methodInvocation);
if (!decision.isGranted()) {
throw new AccessDeniedException("Access denied");
}
// 3. Delegate den method that
super.deleteAll();
}
}
Bean projectService bạn inject vào controller là ProjectService$$EnhancerByCGLIB, không phải ProjectService. Method gốc trong bytecode không có check — check nằm ở proxy.
3.2 Hai chiến lược sinh proxy
Spring có hai cách tạo proxy:
| Loại | Khi nào | Cách hoạt động | Hạn chế |
|---|---|---|---|
| CGLIB | Class không có interface (default Boot 2.0+) | Sinh subclass extends class gốc, override method | Không proxy method private, final, static |
| JDK Dynamic Proxy | Class implement interface, config proxy-target-class=false | Sinh class implement cùng interface, route qua InvocationHandler | Chỉ method khai báo trong interface mới được proxy |
Spring Boot 2.0+ default CGLIB (spring.aop.proxy-target-class=true) — consistent, không phụ thuộc interface. Hệ quả thực tế: method @PreAuthorize phải là public (CGLIB cần override).
Xem AOP proxy JDK vs CGLIB để hiểu sâu hơn về cơ chế proxy — cùng một cơ chế phục vụ @Transactional và @Async.
4. @PreAuthorize — check trước, fail fast
@PreAuthorize eval SpEL trước khi method chạy. Nếu check fail, Spring throw AccessDeniedException ngay — method không bao giờ được gọi, nên không có side effect nào xảy ra.
@Service
public class ProjectService {
// Simple role check
@PreAuthorize("hasRole('ADMIN')")
public void deleteAll() {
projectRepo.deleteAll();
}
// Multiple roles
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public Project create(CreateProjectRequest req) {
return projectRepo.save(new Project(req));
}
// Check method argument — can -parameters compiler flag
@PreAuthorize("#userId == authentication.principal.id")
public UserProfile getProfile(Long userId) {
return profileRepo.findByUserId(userId).orElseThrow();
}
// Delegate logic phuc tap sang bean rieng
@PreAuthorize("@projectSecurity.canEdit(#projectId, authentication)")
public void edit(Long projectId, ProjectUpdate update) {
// ...
}
}
4.1 SpEL — biến có sẵn trong expression
@PreAuthorize dùng MethodSecurityExpressionRoot expose các biến:
| Biến | Ý nghĩa |
|---|---|
authentication | Object Authentication hiện tại từ SecurityContext |
principal | Shortcut cho authentication.principal — thường là UserDetails hoặc Jwt |
#paramName | Method argument theo tên (cần -parameters compiler flag hoặc annotation @P) |
@beanName.method(...) | Gọi Spring bean để eval rule phức tạp |
hasRole('ADMIN') tương đương hasAuthority('ROLE_ADMIN') — Spring tự prefix ROLE_. Pitfall: hasRole('ROLE_ADMIN') sẽ tìm authority ROLE_ROLE_ADMIN — double prefix.
4.2 Vì sao cần custom security bean
Khi rule phức tạp (query DB, check ownership), đẩy logic ra bean riêng — SpEL string ngắn lại, logic test được:
@Component("projectSecurity")
@RequiredArgsConstructor
public class ProjectSecurityChecker {
private final ProjectRepository projectRepo;
// Method nay test duoc don lap, khong phu thuoc Spring Security context
public boolean canEdit(Long projectId, Authentication auth) {
return projectRepo.findById(projectId)
.map(p -> {
boolean isOwner = p.getOwner().getEmail().equals(auth.getName());
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
return isOwner || isAdmin;
})
.orElse(false);
}
}
// SpEL ngan, logic trong bean
@PreAuthorize("@projectSecurity.canEdit(#projectId, authentication)")
public void edit(Long projectId, ProjectUpdate update) { ... }
5. @PostAuthorize — check sau, dùng returnObject
@PostAuthorize eval SpEL sau khi method đã chạy xong, với biến đặc biệt returnObject = giá trị method trả về:
// Fetch project, sau do check ownership
@PostAuthorize("returnObject.owner.email == authentication.name or hasRole('ADMIN')")
public Project findById(Long id) {
return projectRepo.findById(id).orElseThrow();
}
Pseudo-code proxy cho @PostAuthorize:
@Override
public Project findById(Long id) {
Project result = super.findById(id); // 1. method chay truoc
AuthorizationDecision d = authzManager.check(
() -> SecurityContextHolder.getContext().getAuthentication(),
new MethodInvocationResult(invocation, result) // 2. eval voi returnObject
);
if (!d.isGranted()) {
throw new AccessDeniedException("Access denied");
}
return result; // 3. tra ve neu pass
}
5.1 Caveat — side effect đã persist khi deny
Method đã chạy xong trước khi check. Hệ quả:
- DB query đã thực thi (tốn một query không cần thiết nếu cuối cùng deny).
- Log, cache, message publish đã xảy ra — không rollback khi
AccessDeniedExceptionthrow. @Transactionalrollback DB write nếu có, nhưng side effect ngoài transaction (Kafka event, file I/O) vẫn persist.
Nguyên tắc:
- Read-only operation:
@PostAuthorizeđược — DB query rồi filter ownership, không side effect nguy hiểm. - Write operation: dùng
@PreAuthorizevới custom bean query check trước — tránh persist rồi rollback.
6. Self-call bypass — pitfall số 1
Đây là bug subtle nhất của method security: annotation không có hiệu lực, không có exception, không có log cảnh báo.
6.1 Hiện tượng
@Service
public class ProjectService {
// Method public, khong co annotation
public void importProjects(List<ProjectRequest> requests) {
requests.forEach(req -> this.create(req)); // self-call qua "this"
}
@PreAuthorize("hasRole('ADMIN')")
public Project create(ProjectRequest req) {
return projectRepo.save(new Project(req));
}
}
User không có ROLE_ADMIN gọi importProjects(100 requests) → 100 project được tạo thành công. @PreAuthorize không một lần chặn.
6.2 Vì sao bypass — cơ chế
sequenceDiagram
participant Caller as Controller
participant Proxy as ProjectService$$Proxy
participant Real as ProjectService (real)
Caller->>Proxy: importProjects(requests)
Note over Proxy: importProjects khong co annotation<br/>proxy delegate ngay, khong check
Proxy->>Real: super.importProjects(requests)
Note over Real: this = real object<br/>KHONG phai proxy
Real->>Real: this.create(req)
Note over Real: goi thang vao real.create()<br/>bo qua proxy hoan toan
Real->>Real: projectRepo.save(...)Khi importProjects chạy trên real object, từ khóa this trỏ đến real object — không phải proxy. JVM resolve this.create(req) thành ProjectService.create(req) trực tiếp qua virtual dispatch trên real object. Proxy không có cơ hội intercept vì call không đi qua nó.
Đây là giới hạn cố hữu của proxy-based AOP: annotation chỉ có hiệu lực khi call đến từ bên ngoài proxy. Self-call luôn đi vòng trong real object.
6.3 Ba cách fix
Fix 1 — Tách class (khuyến nghị):
@Service
@RequiredArgsConstructor
public class ProjectImporter {
private final ProjectService projectService; // inject proxy
public void importProjects(List<ProjectRequest> requests) {
// Goi qua proxy → @PreAuthorize chay
requests.forEach(req -> projectService.create(req));
}
}
@Service
public class ProjectService {
@PreAuthorize("hasRole('ADMIN')")
public Project create(ProjectRequest req) {
return projectRepo.save(new Project(req));
}
}
Clean nhất — Single Responsibility: ProjectImporter orchestrate, ProjectService CRUD đơn lẻ với check. Không workaround proxy.
Fix 2 — Inject self bean:
@Service
public class ProjectService {
private final ApplicationContext ctx;
private ProjectService self; // proxy reference
public ProjectService(ApplicationContext ctx) { this.ctx = ctx; }
@PostConstruct
public void init() {
self = ctx.getBean(ProjectService.class); // lay proxy tu container
}
public void importProjects(List<ProjectRequest> requests) {
requests.forEach(req -> self.create(req)); // qua proxy → check chay
}
@PreAuthorize("hasRole('ADMIN')")
public Project create(ProjectRequest req) {
return projectRepo.save(new Project(req));
}
}
Workaround hợp lệ nhưng code phức tạp hơn. Dùng khi không muốn tách class.
Fix 3 — Đẩy annotation lên entry point:
@Service
public class ProjectService {
// Check tai entry point — caller ngoai di qua proxy, check chay
@PreAuthorize("hasRole('ADMIN')")
public void importProjects(List<ProjectRequest> requests) {
requests.forEach(this::create); // self-call OK vi check da chay tai entry point
}
public Project create(ProjectRequest req) { // khong can annotation
return projectRepo.save(new Project(req));
}
}
Phù hợp khi importProjects luôn yêu cầu cùng role với create. Nếu create cần được gọi riêng với rule khác, dùng Fix 1.
Verify proxy đã wrap bean chưa:
@Test
void projectServiceIsProxied(@Autowired ProjectService service) {
// Neu false → bean khong co annotation security → bug config
assertTrue(AopUtils.isAopProxy(service), "ProjectService phai la AOP proxy");
assertTrue(AopUtils.isCglibProxy(service), "Phai la CGLIB proxy");
}
7. Pitfall thường gặp
Pitfall 1 — Annotation trên method private:
// SAI: CGLIB khong the override method private
@PreAuthorize("hasRole('ADMIN')")
private void internalDelete() { ... }
CGLIB sinh subclass và override method — không override được private. Annotation trở thành no-op. Luôn dùng public (hoặc protected) cho method có annotation security.
Pitfall 2 — hasRole('ROLE_ADMIN') double prefix:
// SAI: tim authority "ROLE_ROLE_ADMIN" — khong ai co
@PreAuthorize("hasRole('ROLE_ADMIN')")
hasRole('ADMIN') tự thêm prefix ROLE_, tức là tìm authority ROLE_ADMIN. hasAuthority('ROLE_ADMIN') không thêm prefix — explicit. Dùng một trong hai, không trộn.
Pitfall 3 — Quên @EnableMethodSecurity:
Annotation no-op hoàn toàn, không báo lỗi. Test với @WithMockUser(roles = "USER") gọi method yêu cầu ADMIN — nếu không throw AccessDeniedException thì bạn đang thiếu enable.
Pitfall 4 — @PostAuthorize trên write operation:
// SAI: DB da ghi truoc khi check
@PostAuthorize("returnObject.owner.email == authentication.name")
public Project create(CreateProjectRequest req) {
return projectRepo.save(new Project(req)); // side effect da persist
}
Dùng @PreAuthorize với custom bean cho write operation.
8. Deep Dive
Spring Security:
- Method Security — annotation, expression handler,
AuthorizationManager. - Expression-Based Access Control — SpEL syntax đầy đủ,
hasPermission,returnObject.
Spring Framework AOP:
- Proxying Mechanism — JDK vs CGLIB, self-invocation limitation.
- AOP Reference — understanding proxy-based AOP constraints.
Source code:
PreAuthorizeAuthorizationManager— implementation@PreAuthorizetrong Spring Security 6.AbstractSecurityInterceptor— CGLIB proxy internals.
Liên hệ các bài khác
- AOP proxy JDK vs CGLIB: cùng cơ chế proxy phục vụ
@Transactionalvà@Async— self-call bypass áp dụng như nhau cho tất cả annotation AOP, không chỉ security. - FilterChain & URL rule: URL rule là tầng ngoài cùng trong defense in depth — bài này giải thích vì sao method security là tầng thứ hai không thừa.
- @PreFilter/@PostFilter & PermissionEvaluator: filter collection per element và fine-grained permission-based authz qua
hasPermission().
Tóm tắt
- Defense in depth: URL rule và method security tồn tại song song — URL rule fail-fast coarse, method security backstop khi service gọi từ nhiều entry point.
@EnableMethodSecuritykích hoạt pipeline. Thiếu annotation → mọi check no-op silent.- Cơ chế: Spring sinh CGLIB proxy wrap bean. Caller nhận proxy. Proxy eval SpEL qua
AuthorizationManagertrước khi delegate đến method gốc. Bean thật trong bytecode không có check. @PreAuthorize: check trước method — fail fast, không side effect. Default cho mọi use case.@PostAuthorize: check sau method vớireturnObject— phù hợp read-only ownership check. Tránh dùng cho write operation vì side effect đã persist.- Self-call bypass:
this.method()trỏ đến real object, bypass proxy → annotation không chạy. Ba fix: tách class (khuyến nghị), inject self bean, đẩy annotation lên entry point. - Method
@PreAuthorizephảipublic— CGLIB không overrideprivate.
Tự kiểm tra
Q1Annotation @PreAuthorize chỉ là metadata trên bytecode — tự nó không chạy được gì. Giải thích cơ chế Spring làm cho annotation này có hiệu lực. Nhắc đến proxy, AuthorizationManager, và SecurityContext.▸
@PreAuthorize chỉ là metadata trên bytecode — tự nó không chạy được gì. Giải thích cơ chế Spring làm cho annotation này có hiệu lực. Nhắc đến proxy, AuthorizationManager, và SecurityContext.Spring dùng AOP proxy. Khi container khởi tạo bean có annotation security, nó sinh một CGLIB proxy class mở rộng (extends) class gốc. Caller nhận proxy — không nhận bean thật.
Proxy override mọi method có annotation. Khi caller gọi deleteAll() trên proxy, proxy:
- Lấy
AuthenticationtừSecurityContextHolder.getContext().getAuthentication(). - Gọi
AuthorizationManager.check(authSupplier, methodInvocation)để eval SpEL expression (ví dụhasRole('ADMIN')). - Nếu
decision.isGranted() == false, interceptor throwAccessDeniedException. Method gốc không bao giờ được gọi. - Nếu pass → gọi
super.deleteAll()(delegate đến bean thật).
Bean thật trong bytecode không có check. Toàn bộ logic security nằm trong proxy được sinh runtime. Verify: AopUtils.isCglibProxy(projectService) == true sau khi inject bean.
Q2Đoạn code sau có bug: user ROLE_USER gọi được importProjects() và tạo thành công 50 project mặc dù create() có @PreAuthorize("hasRole('ADMIN')"). Tại sao? Fix bằng cách nào?
public void importProjects(List<ProjectRequest> reqs) { reqs.forEach(r -> this.create(r)); }▸
ROLE_USER gọi được importProjects() và tạo thành công 50 project mặc dù create() có @PreAuthorize("hasRole('ADMIN')"). Tại sao? Fix bằng cách nào?public void importProjects(List<ProjectRequest> reqs) { reqs.forEach(r -> this.create(r)); }Self-call bypass. importProjects() không có annotation → proxy delegate ngay đến real object. Trong real object, this trỏ đến chính real object — không phải proxy. this.create(r) gọi thẳng vào ProjectService.create() trên real object, bỏ qua proxy hoàn toàn. Annotation @PreAuthorize nằm trên proxy method — không được gọi đến.
Hệ quả: 50 project tạo thành công, không một lần check, không exception, không log cảnh báo.
Fix 1 — Tách class (khuyến nghị): tạo ProjectImporter inject ProjectService qua constructor. importProjects() chuyển sang ProjectImporter, gọi projectService.create(r) — đi qua proxy, check chạy.
Fix 2 — Annotation tại entry point: đặt @PreAuthorize("hasRole('ADMIN')") trên importProjects() thay vì create(). Caller ngoài gọi importProjects() qua proxy nên check chạy. Self-call this.create() không cần annotation.
Fix 3 — Inject self bean: dùng @PostConstruct lấy proxy từ container (ctx.getBean(ProjectService.class)), gọi self.create(r) thay vì this.create(r).
Q3So sánh @PreAuthorize và @PostAuthorize về thời điểm check, side effect khi deny, và khi nào nên dùng cái nào.▸
@PreAuthorize và @PostAuthorize về thời điểm check, side effect khi deny, và khi nào nên dùng cái nào.@PreAuthorize eval SpEL trước khi method chạy. Nếu fail: method không được gọi, không có side effect nào. Đây là default cho mọi use case — fail fast, an toàn nhất.
@PostAuthorize eval SpEL sau khi method chạy xong, với biến returnObject = giá trị trả về. Nếu fail: AccessDeniedException throw, nhưng DB query đã thực thi, log đã ghi, message đã publish — không có gì rollback ngoài DB transaction.
Khi nào dùng:
- Read-only + check ownership theo return value:
@PostAuthorize("returnObject.owner.email == authentication.name")— fetch project rồi check owner. Chấp nhận được vì side effect chỉ là DB read. Tuy nhiên query filter at DB (findByIdAndOwnerEmail()) tốt hơn — tránh load resource user không có quyền. - Write operation: luôn dùng
@PreAuthorize— check trước khi persist bất cứ thứ gì. - Check dựa trên method args:
@PreAuthorize("#userId == authentication.principal.id")— biết đủ thông tin trước khi chạy, không cần return value.
Nguyên tắc gọn: @PreAuthorize default. @PostAuthorize chỉ khi rule phụ thuộc return value và operation là read-only.
Q4CGLIB proxy không thể proxy method nào? Kể ra 3 loại và giải thích vì sao mỗi loại không proxy được.▸
CGLIB sinh subclass và override method để intercept. Ba loại không override được:
- Method
private: Java không cho phép subclass override methodprivate— chúng không visible từ subclass. CGLIB subclass không thể override → annotation no-op. - Method
final:finalmethod không thể bị override theo spec Java. CGLIB cố override sẽ throw error hoặc annotation bị bỏ qua tuỳ version. Spring Security sẽ cảnh báo hoặc fail startup nếu phát hiện. - Method
static: static method thuộc class, không thuộc instance — không có virtual dispatch, không override được qua subclass. Annotation security trên static method vô nghĩa.
Hệ quả thực tế: luôn đặt @PreAuthorize trên method public (hoặc protected) không final không static. Verify runtime bằng AopUtils.isAopProxy(bean) — nếu bean không được proxy thì annotation security không chạy.
Q5Viết test JUnit 5 / Spring Boot verify rằng: (1) ProjectService là CGLIB proxy, (2) user ROLE_USER gọi deleteAll() bị AccessDeniedException, (3) user ROLE_ADMIN gọi được. Dùng @WithMockUser.▸
ProjectService là CGLIB proxy, (2) user ROLE_USER gọi deleteAll() bị AccessDeniedException, (3) user ROLE_ADMIN gọi được. Dùng @WithMockUser.Test kiểm tra 3 điều — proxy đã wrap, negative case đúng role, positive case đúng role:
@SpringBootTest
class ProjectServiceMethodSecurityTest {
@Autowired
private ProjectService projectService;
// (1) Verify proxy da wrap bean
@Test
void projectService_isWrappedByCglibProxy() {
assertTrue(AopUtils.isAopProxy(projectService),
"ProjectService phai la AOP proxy — neu false thi @PreAuthorize no-op");
assertTrue(AopUtils.isCglibProxy(projectService),
"Phai la CGLIB proxy (Boot default proxy-target-class=true)");
}
// (2) ROLE_USER bi tu choi
@Test
@WithMockUser(roles = "USER")
void deleteAll_withUserRole_throwsAccessDenied() {
assertThatThrownBy(() -> projectService.deleteAll())
.isInstanceOf(AccessDeniedException.class);
}
// (3) ROLE_ADMIN duoc phep
@Test
@WithMockUser(roles = "ADMIN")
void deleteAll_withAdminRole_succeeds() {
assertThatCode(() -> projectService.deleteAll())
.doesNotThrowAnyException();
}
}Tại sao test (1) quan trọng: nếu AopUtils.isAopProxy() == false, test (2) sẽ fail theo chiều ngược — method chạy mà không throw exception, tức annotation no-op. Biết trước proxy đúng giúp phân biệt "check không chạy" vs "check chạy nhưng logic sai".
@WithMockUser setup Authentication với username user và authorities ROLE_USER/ROLE_ADMIN vào SecurityContext trước khi test method chạy. Proxy đọc context đó khi eval SpEL.
Bài tiếp theo: @PreFilter/@PostFilter & PermissionEvaluator
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