Spring Security annotations

- Published on
- /9 mins read/
Trong kỹ nghệ phần mềm doanh nghiệp, Bảo mật (Security) không bao giờ là một tính năng phụ trợ được "chắp vá" vào giai đoạn cuối của dự án. Nó là nền tảng cốt tử. Mọi yêu cầu truy cập hệ thống đều phải trả lời hai câu hỏi sống còn:
- Authentication (Xác thực): "Bạn là ai?" (JWT Token, OAuth2 Resource Server, Session Cookie).
- Authorization (Phân quyền): "Bạn được phép làm những gì trên tài nguyên cụ thể này?" (RBAC, ABAC, Multi-Tenant Workspace Isolation).
Spring Security cung cấp cơ chế bảo mật khai báo (Declarative Security) cực kỳ mạnh mẽ thông qua tập hợp các Annotations: @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter, @Secured, @RolesAllowed.
Tuy nhiên, việc sử dụng sai lệch hoặc hiểu nông cạn về cơ chế hoạt động của Spring Security thường dẫn đến những lỗ hổng bảo mật nghiêm trọng:
- Bị rò rỉ dữ liệu giữa các khách hàng (Multi-tenant Data Leakage) do kiểm tra quyền sơ sài ở tầng URL thay vì Method Level.
- Bẫy Side-Effect chết người của
@PostAuthorize: Dữ liệu đã bị ghi vào Database xong xuôi rồi Spring mới ném ngoại lệ chặn quyền! - Thảm họa hiệu năng và nghẽn CPU của
@PostFilterkhi cố gắng lọc hàng chục nghìn bản ghi trong RAM thay vì lọc tại câu lệnh SQL. - Phải viết hàng chục role lặp đi lặp lại vì không biết cách thiết lập Role Hierarchy.
Bài viết này sẽ phân tích toàn diện kiến trúc Spring Security từ tầng Servlet Filter đến Spring AOP Method Interceptors.
# mô hình phòng thủ đa tầng (defense-in-depth): filterchain vs method security
Một kiến trúc bảo mật chuẩn production luôn áp dụng nguyên lý Phòng thủ đa tầng (Defense-in-Depth):
# tại sao chỉ bảo vệ ở securityfilterchain là không đủ?
- URL-based Security chỉ lọc thô: Bạn chỉ có thể cấu hình
/api/v1/orders/**yêu cầu authenticated hoặc có roleUSER. Nhưng URL không thể biết được liệu User ID100có quyền sửa Đơn hàng ID999hay không! - Đa kênh truy cập (Multi-Channel Invocations): Một Business Service không chỉ được gọi từ Web Controller. Nó có thể được gọi từ một Kafka Consumer, một Scheduled Batch Job, hoặc một gRPC Service. Các kênh này hoàn toàn không đi qua Servlet FilterChain!
- Giải pháp: Đặt chốt chặn bảo mật bằng Method Security Annotations ngay trên Interface của Service nghiệp vụ.
# giải mã SpEL & bộ đôi @preauthorize vs @postauthorize
Để kích hoạt Method Security trong Spring Boot 3+, bạn bắt buộc phải khai báo @EnableMethodSecurity trên class cấu hình:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfiguration {
// ...
}# @preauthorize: chặn trước khi phương thức chạy
Spring chặn phương thức trước khi gọi, phân giải biểu thức SpEL (Spring Expression Language). Nếu trả về false, ném ra AccessDeniedException (HTTP 403) ngay lập tức:
@RestController
@RequestMapping("/api/v1/workspaces")
public class WorkspaceController {
// 1. Phân quyền theo Role chuẩn
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteWorkspace(@PathVariable UUID id) { ... }
// 2. Phân giải tham số động (#id) và gọi Bean bảo mật riêng biệt (ABAC)
@PreAuthorize("@workspaceSecurityEvaluator.canAccess(#id, authentication)")
@GetMapping("/{id}")
public WorkspaceDto getWorkspace(@PathVariable UUID id) { ... }
}# tử huyệt của @postauthorize: bẫy side-effect nguy hiểm!
@PostAuthorize cho phép phương thức chạy xong xuôi, sau đó mới kiểm tra quyền dựa trên giá trị trả về (returnObject):
// 🔴 BẪY BẢO MẬT KINH ĐIỂN:
@PostAuthorize("returnObject.ownerId == authentication.name")
@PutMapping("/documents/{id}")
public Document updateDocument(@PathVariable Long id, @RequestBody UpdateRequest request) {
// DÒNG NÀY ĐÃ GHI DỮ LIỆU MỚI VÀO DATABASE VÀ COMMIT THÀNH CÔNG!
return documentService.saveAndFlush(id, request);
// SAU ĐÓ, @PostAuthorize MỚI KIỂM TRA QUYỀN VÀ NÉM AccessDeniedException!
}CAUTION
Quy Tắc Sống Còn: TUYỆT ĐỐI KHÔNG DÙNG @PostAuthorize CHO CÁC THAO TÁC GHI (INSERT, UPDATE, DELETE)! Chỉ sử dụng nó cho các thao tác đọc (GET/Query) khi quyền truy cập phụ thuộc trực tiếp vào nội dung dữ liệu trả về từ Database.
# cạm bẫy hiệu năng của @postfilter & @prefilter
Spring Security cung cấp hai annotation hỗ trợ lọc collection:
@PreFilter: Lọc danh sách đầu vào trước khi method thực thi.@PostFilter: Lọc danh sách trả về sau khi method thực thi.
// 🔴 THẢM HỌA HIỆU NĂNG CPU & RAM:
@PostFilter("filterObject.ownerId == authentication.name")
@GetMapping("/all-orders")
public List<Order> getAllOrders() {
// Database query 1,000,000 bản ghi nạp vào RAM!
return orderRepository.findAll();
// Hibernate tải 1 triệu entity lên Heap -> Spring lặp qua 1 triệu lần để xóa phần tử!
}# tại sao @postfilter là một giải pháp tồi trong môi trường enterprise?
- Lãng phí I/O và RAM: Database phải quét toàn bộ bảng và gửi hàng chục Megabytes dữ liệu qua mạng xuống ứng dụng.
- CPU Churn: Spring Security phải sử dụng Iterator để duyệt qua từng phần tử và đánh giá biểu thức SpEL, ngốn toàn bộ CPU của server.
- Giải pháp chuẩn: Luôn luôn lọc dữ liệu ngay tại câu lệnh SQL (
WHERE owner_id = :userId) thông qua Spring Data JPA hoặc Spring Data Specifications!
# xây dựng custom security expression Bean (ABAC chuẩn enterprise)
Thay vì viết những biểu thức SpEL dài dòng, khó bảo trì trực tiếp trên annotation, kiến trúc sư phần mềm luôn tách logic phân quyền thành một Dedicated Security Evaluator:
package com.company.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component("workspaceGuard")
public class WorkspaceSecurityGuard {
private final WorkspaceMembershipRepository membershipRepo;
public WorkspaceSecurityGuard(WorkspaceMembershipRepository membershipRepo) {
this.membershipRepo = membershipRepo;
}
public boolean hasPermission(UUID workspaceId, String requiredRole, Authentication authentication) {
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
return false;
}
// Trích xuất userId từ JWT Claim sub
String userId = jwt.getSubject();
// Kiểm tra quan hệ thành viên trong Database hoặc Redis Cache
return membershipRepo.findRole(workspaceId, userId)
.map(role -> role.hasHierarchyLevel(requiredRole))
.orElse(false);
}
}# sử dụng thanh lịch trên controller / service
@RestController
@RequestMapping("/api/v1/workspaces/{workspaceId}/projects")
public class ProjectController {
@PreAuthorize("@workspaceGuard.hasPermission(#workspaceId, 'EDITOR', authentication)")
@PostMapping
public ProjectDto createProject(
@PathVariable UUID workspaceId,
@RequestBody CreateProjectRequest request) {
return projectService.create(workspaceId, request);
}
}# thiết lập kế thừa quyền hạn (role hierarchy)
Một vấn đề phổ biến là lập trình viên phải liệt kê hàng loạt Role: @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'ORG_ADMIN', 'MANAGER', 'STAFF')")
Điều này vừa dễ sót quyền, vừa biến code thành một mớ hỗn độn.
# thiết lập rolehierarchy tự động suy diễn quyền
@Configuration
public class RoleHierarchyConfig {
@Bean
public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.withDefaultRolePrefix()
.role("SUPER_ADMIN").implies("ORG_ADMIN")
.role("ORG_ADMIN").implies("MANAGER")
.role("MANAGER").implies("STAFF")
.role("STAFF").implies("USER")
.build();
}
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy);
return expressionHandler;
}
}Nhờ có RoleHierarchy, bạn chỉ cần viết:
@PreAuthorize("hasRole('STAFF')")Và bất kỳ User nào có role MANAGER, ORG_ADMIN, hay SUPER_ADMIN đều tự động được cấp quyền truy cập mà không cần viết thêm một dòng code nào!
# ma trận đánh giá so sánh các annotation bảo mật
| Annotation | Cơ chế hoạt động | Hỗ trợ SpEL? | Ưu điểm | Nhược điểm / Rủi ro |
|---|---|---|---|---|
@PreAuthorize | Chặn trước khi chạy hàm | CÓ (Toàn diện) | Linh hoạt, kiểm tra sâu thuộc tính đối tượng. | Lỗi cú pháp SpEL chỉ phát hiện lúc runtime. |
@PostAuthorize | Chặn sau khi hàm trả về | CÓ | Kiểm tra được thuộc tính trả về từ DB. | 🔴 Nguy cơ Side-Effect nếu áp dụng cho lệnh Ghi! |
@PreFilter | Lọc collection đầu vào | CÓ | Tiện lợi khi xóa hàng loạt ID không hợp lệ. | Chỉ áp dụng được cho mảng/collection mutable. |
@PostFilter | Lọc collection trả về | CÓ | Lọc kết quả tự động. | 🔴 Ác mộng hiệu năng RAM & CPU khi dữ liệu lớn! |
@Secured | Chặn theo Role Spring | KHÔNG | Đơn giản, ngắn gọn, dễ hiểu. | Không hỗ trợ SpEL hay biểu thức logic phức tạp. |
@RolesAllowed | Chuẩn JSR-250 Java EE | KHÔNG | Độc lập framework, theo chuẩn Java quốc tế. | Bị gò bó chỉ trong việc so sánh role tĩnh. |
# tổng kết & checklist thực chiến
- Phòng thủ đa tầng: Sử dụng
SecurityFilterChainđể xác thực danh tính và lọc URL thô; sử dụngMethod Securityđể bảo vệ tài nguyên chi tiết theo nghiệp vụ. - Khai tử
@PostAuthorizetrên CUD: Không bao giờ dùng@PostAuthorizecho Create/Update/Delete; luôn dùng@PreAuthorizeđể fail-fast trước khi can thiệp vào Database. - Đừng lọc bằng
@PostFilter: Hãy đẩy toàn bộ logic lọc phân quyền xuống tầng SQL query (WHERE tenant_id = ?) để tối ưu hóa Index và giảm tải RAM. - Áp dụng Role Hierarchy: Giúp phân cấp quyền hạn tự nhiên, loại bỏ các biểu thức
hasAnyRole()dài dòng. - Đóng gói Logic vào Custom Guard Bean: Giúp SpEL ngắn gọn, sạch sẽ, và dễ dàng viết Unit Test độc lập cho các luật phân quyền phức tạp.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # mô hình phòng thủ đa tầng (defense-in-depth): filterchain vs method security
- # tại sao chỉ bảo vệ ở securityfilterchain là không đủ?
- # giải mã SpEL & bộ đôi @preauthorize vs @postauthorize
- # @preauthorize: chặn trước khi phương thức chạy
- # tử huyệt của @postauthorize: bẫy side-effect nguy hiểm!
- # cạm bẫy hiệu năng của @postfilter & @prefilter
- # tại sao @postfilter là một giải pháp tồi trong môi trường enterprise?
- # xây dựng custom security expression Bean (ABAC chuẩn enterprise)
- # sử dụng thanh lịch trên controller / service
- # thiết lập kế thừa quyền hạn (role hierarchy)
- # thiết lập rolehierarchy tự động suy diễn quyền
- # ma trận đánh giá so sánh các annotation bảo mật
- # tổng kết & checklist thực chiến