@PreAuthorize & AOP proxy — cơ chế method security
Hiểu rõ cơ chế AOP Proxy của @PreAuthorize và @PostAuthorize trong Spring Security, xử lý triệt để lỗi bypass self-call và kiểm thử an toàn.
TL;DR: @PreAuthorize hoạt động dựa trên cơ chế Spring AOP Proxy: container không bàn giao đối tượng gốc cho phía gọi mà bao bọc nó trong một CGLIB proxy. Mọi lời gọi phương thức từ bên ngoài đều phải đi qua proxy để thẩm định biểu thức SpEL, ném AccessDeniedException nếu thiếu quyền trước khi chuyển tiếp vào phương thức gốc. Ngược lại, @PostAuthorize thực thi phương thức trước rồi mới đối soát kết quả trả về (returnObject). Cạm bẫy nguy hiểm nhất là việc tự gọi nội bộ (this.method()) sẽ đi thẳng vào thực thể gốc, bỏ qua hoàn toàn lớp bọc proxy khiến các annotation bảo mật bị vô hiệu hóa trong im lặng.
Tại sao hệ thống cần lớp bảo vệ Method Security độc lập?
Quy tắc phân quyền URL tại authorizeHttpRequests (requestMatchers) là lớp phòng thủ ngoài cùng bảo vệ các HTTP Controller. Tuy nhiên, trong kiến trúc thực tế, tầng Service thường được kích hoạt từ nhiều kênh khác:

Các kênh truy cập phi HTTP gồm có:
- Các Scheduled Cron Job tự động dọn dẹp dữ liệu.
- Các Message Listener lắng nghe sự kiện từ Kafka hoặc RabbitMQ.
- Các lời gọi từ GraphQL Resolver hoặc gRPC protocol.
- Các bài kiểm thử tích hợp (Integration Test) gọi trực tiếp Service Bean.
Method Security là chốt chặn phòng thủ chiều sâu (Defense in Depth), đặt quy tắc bảo mật trực tiếp trên chính phương thức nghiệp vụ, bất kể nguồn gọi đến từ đâu.
Annotation @EnableMethodSecurity kích hoạt những thành phần nào?
Trong Spring Security 6 (Spring Boot 3+), ta kích hoạt phân quyền phương thức bằng cách gắn @EnableMethodSecurity vào cấu hình:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enable @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter
public class MethodSecurityConfig {
// Security filter chain definition
}
Annotation này đăng ký ba thành phần hạ tầng cốt lõi:
MethodSecurityInterceptor: Advisor quét các Bean để phát hiện annotation và tạo Proxy.AuthorizationManagerbeans: Quản lý logic thẩm định quyền hạn cho từng loại annotation (PreAuthorizeAuthorizationManager).MethodSecurityExpressionHandler: Trình xử lý biểu thức SpEL hỗ trợ truy xuấtauthentication, tham số phương thức#paramvà các Spring Bean@beanName.
Cơ chế AOP Proxy chặn và điều phối lời gọi phương thức ra sao?
Khi bạn tiêm một Service có chứa @PreAuthorize vào Controller, Spring IoC không tiêm trực tiếp đối tượng gốc mà sinh ra một lớp CGLIB Proxy kế thừa từ class của bạn:
// CGLIB Proxy pseudo-code at runtime
public class ProjectService$$EnhancerByCGLIB extends ProjectService {
private final AuthorizationManager<MethodInvocation> authzManager;
@Override
public void deleteAll() {
// 1. Fetch Authentication from SecurityContext
Supplier<Authentication> authSupplier =
() -> SecurityContextHolder.getContext().getAuthentication();
// 2. Evaluate SpEL: hasRole('ADMIN')
AuthorizationDecision decision = authzManager.check(authSupplier, invocation);
if (!decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
// 3. Delegate call to real target object
super.deleteAll();
}
}
Do cơ chế kế thừa và ghi đè phương thức (Override) của CGLIB, phương thức được bảo vệ bắt buộc phải là public. Nếu gắn annotation lên phương thức private hoặc final, lớp con không thể override và annotation sẽ bị bỏ qua.
Phân quyền sớm với @PreAuthorize và SpEL diễn ra như thế nào?
@PreAuthorize kiểm tra biểu thức SpEL trước khi phương thức nghiệp vụ được thực thi. Nếu không đủ quyền, ngoại lệ AccessDeniedException lập tức phát sinh, đảm bảo không có bất kỳ thao tác ghi cơ sở dữ liệu nào diễn ra:
@Service
public class ProjectService {
@PreAuthorize("hasRole('ADMIN')")
public void deleteAll() {
projectRepository.deleteAll();
}
// Match method argument with authenticated user id
@PreAuthorize("#userId == authentication.principal.id")
public UserProfile getProfile(Long userId) {
return profileRepository.findByUserId(userId).orElseThrow();
}
// Delegate complex logic to custom security bean
@PreAuthorize("@projectSecurity.canEdit(#projectId, authentication)")
public void updateProject(Long projectId, ProjectUpdate update) {
// Business update logic
}
}
Tách logic kiểm tra quyền phức tạp sang một Spring Bean độc lập:
@Component("projectSecurity")
@RequiredArgsConstructor
public class ProjectSecurityChecker {
private final ProjectRepository projectRepository;
public boolean canEdit(Long projectId, Authentication auth) {
return projectRepository.findById(projectId)
.map(p -> p.getOwner().getEmail().equals(auth.getName())
|| auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")))
.orElse(false);
}
}
Khi nào nên sử dụng @PostAuthorize và đâu là rủi ro tiềm ẩn?
@PostAuthorize cho phép thẩm định quyền hạn sau khi phương thức đã thực thi xong, sử dụng biến ngữ cảnh returnObject:
@PostAuthorize("returnObject.owner.email == authentication.name or hasRole('ADMIN')")
public Project getProjectById(Long id) {
return projectRepository.findById(id).orElseThrow();
}
Phương thức đã chạy xong trước khi việc kiểm tra diễn ra. Vì vậy, chỉ dùng @PostAuthorize cho các tác vụ chỉ đọc (Read-only). Tuyệt đối không dùng cho các phương thức ghi dữ liệu, gửi email hay bắn message sang Kafka vì các thao tác này không thể hoàn tác khi ngoại lệ phân quyền xảy ra.
Hiện tượng Self-call Bypass phát sinh do đâu và cách khắc phục?
Đây là lỗi kinh điển phổ biến nhất trong kiến trúc Spring AOP:
@Service
public class ProjectService {
public void importProjects(List<ProjectRequest> requests) {
// Internal self-call via this bypasses proxy
requests.forEach(req -> this.create(req));
}
@PreAuthorize("hasRole('ADMIN')")
public Project create(ProjectRequest req) {
return projectRepository.save(new Project(req));
}
}
Khi một client gọi importProjects(), lời gọi đi qua proxy. Nhưng bên trong phương thức, từ khóa this trỏ trực tiếp vào thực thể gốc (Target Object) chứ không phải Proxy:

Kết quả là create() được gọi trực tiếp bỏ qua hoàn toàn lớp bọc CGLIB, dẫn đến việc người dùng không có quyền ADMIN vẫn tạo được hàng loạt dự án mà không hề bị chặn.
Các giải pháp khắc phục triệt để:
- Tách thành hai Service độc lập (Khuyến nghị chuẩn OOP): Chuyển
importProjectssangProjectImportServicevà tiêmProjectServicevào. Mọi lời gọi lúc này đều đi qua CGLIB Proxy. - Đẩy Annotation lên Entry Point: Gắn
@PreAuthorize("hasRole('ADMIN')")trực tiếp lênimportProjects(). - Tiêm chính nó (Self-injection): Dùng
ApplicationContext.getBean(ProjectService.class)để lấy thể hiện Proxy thay vì gọi quathis.
Những cạm bẫy phổ biến khi thiết lập Method Security là gì?
- Gắn annotation lên phương thức private: CGLIB không thể ghi đè phương thức private, annotation hoàn toàn vô hiệu.
- Nhầm lẫn hasRole('ROLE_ADMIN'): Phương thức
hasRole()tự động thêm tiền tốROLE_, dẫn đến việc tìm kiếm chuỗi saiROLE_ROLE_ADMIN. - Quên khai báo @EnableMethodSecurity: Toàn bộ các annotation bảo mật trên Service sẽ trở thành vô dụng mà không báo lỗi.
Những điểm then chốt cần ghi nhớ là gì?
- Method Security bổ sung tầng bảo vệ chiều sâu cho các lời gọi nghiệp vụ phi HTTP.
- Spring AOP sử dụng CGLIB Proxy để thẩm định SpEL trước (
@PreAuthorize) hoặc sau (@PostAuthorize) khi gọi phương thức gốc. - Các phương thức được gắn annotation bảo mật bắt buộc phải là
public. - Cảnh giác cao độ với hiện tượng Self-call Bypass khi gọi phương thức cùng class thông qua
this.
Bạn đã làm chủ cơ chế @PreAuthorize và AOP Proxy chưa?
- Q1Tại sao lời gọi nội bộ this.create() bên trong cùng một Service class lại làm mất tác dụng của @PreAuthorize gắn trên create()?
- Q2Giải pháp kiến trúc nào được khuyến nghị nhất để khắc phục triệt để hiện tượng Self-call Bypass?
- Q3Tại sao không nên sử dụng @PostAuthorize cho các phương thức có hành vi ghi dữ liệu (Write operations)?
- Q4Điều gì sẽ xảy ra nếu lập trình viên viết @PreAuthorize("hasRole('ROLE_ADMIN')")?
- Q5Annotation nào bắt buộc phải có trên lớp cấu hình để kích hoạt tính năng kiểm tra quyền hạn cấp phương thức trong Spring Boot 3?
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?
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.
Tổng Quan Module: Phân Quyền và Bảo Mật Web
Bài tiếpLọc Dữ Liệu Method Security và PermissionEvaluator
Lộ trình Junior Java Backend Developer
Chặng 18/21 · Authentication & Authorization — bài 14/19 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này