Authorization: role, authority, 401 vs 403
Tiếp đi: phân quyền (authorization).
Bài trước Spring Security đã biết bạn là ai. Nhưng đăng nhập thành công không có nghĩa là được truy cập mọi thứ. Bài này giải quyết ba bài toán:
- User thường không được gọi API của Admin.
- User chỉ làm được những hành động mình có quyền.
- User A không được xem hay xóa đơn hàng của User B.
1. Bài toán
| Username | Role |
|---|---|
| john | USER |
| admin | ADMIN |
| API của shop-app | Yêu cầu |
|---|---|
GET /api/public/products | Public |
GET /api/orders | USER hoặc ADMIN |
DELETE /api/admin/users/10 | ADMIN |
HTTP Request
|
Authentication (credentials/token hợp lệ?) -- không --> 401
|
Authorization (có quyền?) -- không --> 403
|
Controller
2. Vấn đề
- Cấu hình hiện tại chỉ có
anyRequest().authenticated(): John đăng nhập là gọi được cả API admin. - Dùng role cho mọi thứ thì role phình ra:
ADMIN,ADMIN_BUT_NO_DELETE... - Chỉ kiểm role/permission thì vẫn hổng: John có quyền
ORDER_READnên đọc được đơn của Alice.
3. Giải pháp: Role, Authority và nhiều tầng kiểm tra
Role là vai trò (ADMIN, MANAGER, USER). Permission là một hành động cụ thể (ORDER_READ, ORDER_CREATE, ORDER_DELETE). Gán permission cho role, gán role cho user: đó là RBAC.
ADMIN → ORDER_READ, ORDER_CREATE, ORDER_DELETE
MANAGER → ORDER_READ, ORDER_CREATE
USER → ORDER_READ
Trong Spring Security, cả hai đều là GrantedAuthority (một chuỗi). Role chỉ là authority có tiền tố ROLE_.
| Method | Authority cần có |
|---|---|
hasRole("ADMIN") | ROLE_ADMIN (tự thêm prefix) |
hasAuthority("ROLE_ADMIN") | ROLE_ADMIN |
hasAuthority("ORDER_READ") | ORDER_READ (đúng nguyên chuỗi) |
hasAnyRole("ADMIN","MANAGER") | Một trong hai |
Các tầng kiểm tra bổ sung cho nhau:
| Tầng | Ví dụ |
|---|---|
| Filter chain (URL) | /api/admin/** yêu cầu ADMIN |
| Method security | @PreAuthorize("hasAuthority('ORDER_READ')") |
| Business logic | Order phải thuộc user hiện tại |
| Database query | findByIdAndUserId() |
4. Code từng bước
Bước 1: Dependency. Không cần thêm gì, đã có spring-boot-starter-security.
Bước 2: Luật theo URL
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/orders/**").hasAuthority("ORDER_DELETE")
.requestMatchers("/api/orders/**").authenticated()
.anyRequest().denyAll())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
denyAll() cuối cùng: endpoint nào quên khai báo sẽ bị chặn (fail closed), an toàn hơn authenticated().
Để user có cả role lẫn permission, UserDetailsService trả authorities:
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities("ROLE_" + user.getRole(), "ORDER_READ") // ví dụ; thực tế đọc từ bảng permission
.build();
Lưu ý: .roles() và .authorities() ghi đè nhau, gọi cái sau cùng thắng.
Bước 3: Method security và kiểm tra ownership
@Service
public class OrderService {
private final OrderRepository orderRepository;
// constructor ...
@PreAuthorize("hasAuthority('ORDER_READ')")
@Transactional(readOnly = true)
public OrderDto getOrder(Long orderId, Long currentUserId) {
Order order = orderRepository.findByIdAndUserId(orderId, currentUserId)
.orElseThrow(() -> new EntityNotFoundException("Order not found"));
return OrderDto.from(order);
}
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
public void deleteOrder(Long orderId) { ... }
}
@Component("orderSecurity")
public class OrderSecurity {
private final OrderRepository orderRepository;
// constructor ...
public boolean isOwner(Long orderId, Authentication authentication) {
Long userId = Long.valueOf(authentication.getName()); // giả định principal name là user id
return orderRepository.existsByIdAndUserId(orderId, userId);
}
}
currentUserId phải lấy từ thông tin xác thực server đã kiểm chứng, không phải từ query parameter client gửi.
Bước 4: Test bằng curl
curl -i localhost:8080/api/admin/users
# 401 (không gửi credentials)
curl -i -u john:<password> localhost:8080/api/admin/users
# 403 (john là USER)
curl -i -u john:<password> localhost:8080/api/orders/101
# 404 (đơn 101 của alice, query theo userId nên không tìm thấy)
5. Cách hoạt động
Request
|
AuthorizationFilter
| RequestMatcherDelegatingAuthorizationManager
| duyệt luật requestMatchers theo thứ tự, luật đầu tiên khớp
| → AuthorityAuthorizationManager.check(authentication, request)
|
+-- granted ----------------> DispatcherServlet → Controller
| |
| proxy AOP @PreAuthorize (AuthorizationManagerBeforeMethodInterceptor)
| |
| OrderService
+-- denied → AccessDeniedException
|
ExceptionTranslationFilter
├── user là anonymous → AuthenticationEntryPoint → 401
└── đã xác thực → AccessDeniedHandler → 403
Spring Security 6 thống nhất mọi quyết định quyền bằng AuthorizationManager<T>, thay cho AccessDecisionManager/voter cũ:
@FunctionalInterface
public interface AuthorizationManager<T> {
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}
Tự viết một cái cho luật URL đặc biệt, ví dụ chỉ cho gọi API báo cáo trong giờ hành chính:
AuthorizationManager<RequestAuthorizationContext> officeHours = (auth, ctx) ->
new AuthorizationDecision(LocalTime.now().isAfter(LocalTime.of(8, 0))
&& LocalTime.now().isBefore(LocalTime.of(18, 0)));
.requestMatchers("/api/reports/**").access(officeHours)
6. So sánh
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| Ý nghĩa thật | Chưa xác thực / credentials sai | Đã biết bạn là ai nhưng không đủ quyền |
| Ví dụ | Thiếu token, token hết hạn, sai chữ ký | USER gọi API ADMIN |
| Ai xử lý | AuthenticationEntryPoint | AccessDeniedHandler |
| Client nên làm | Đăng nhập lại / refresh token | Không retry, báo không có quyền |
| URL rules | @PreAuthorize | |
|---|---|---|
| Vị trí | Filter, trước DispatcherServlet | Proxy quanh bean |
| Biết tham số method? | Không | Có (#orderId) |
| Hợp với | Luật thô theo path | Luật nghiệp vụ, ownership |
7. Khi nào nên dùng / không nên dùng
- URL rules cho ranh giới lớn (public / admin), luôn kết thúc bằng
anyRequest(). @PreAuthorizeở Service cho luật gắn với dữ liệu.- Ownership nên đẩy xuống query (
findByIdAndUserId), trả 404 để không lộ tài nguyên người khác. - Không chỉ dựa vào ẩn nút trên frontend. Backend phải tự kiểm tra.
John có ORDER_READ rồi, sao vẫn bảo là lỗ hổng khi John xem order 101?
Đúng là John qua được bước kiểm tra permission, và chính vì thế mới là lỗ hổng. Dữ liệu:
| order id | user_id | total |
|---|---|---|
| 100 | 1 (john) | 500 |
| 101 | 2 (alice) | 800 |
Nếu chỉ có @PreAuthorize("hasAuthority('ORDER_READ')"), John đổi id trên URL thành 101 là đọc được đơn của Alice. Đây là Broken Object Level Authorization (BOLA), đứng đầu OWASP API Security Top 10. Role/permission trả lời "được đọc đơn hàng không", còn ownership trả lời "được đọc đơn này không". Phải có cả hai.
Khi nào nên trả 404 thay vì 403?
Bạn hiểu đúng hướng. Với tài nguyên chỉ chủ sở hữu được thấy (đơn hàng cá nhân), trả 404 khi không tồn tại hoặc không thuộc user, để kẻ tấn công không dò được id nào có thật. Với chức năng mà việc tồn tại không phải bí mật (trang admin), trả 403 cho rõ ràng. Quan trọng là nhất quán trong cả API.
Phỏng vấn hỏi authorization trong Spring Security thì trả lời sao?
Q1. hasRole() khác hasAuthority() thế nào?
🇻🇳 Tiếng Việt:
Cả hai đều so
GrantedAuthority.hasRole("ADMIN")tự thêm tiền tốROLE_nên tìmROLE_ADMIN;hasAuthority()so đúng chuỗi. Thường dùng role cho nhóm quyền thô và authority cho permission chi tiết nhưORDER_DELETE.
🇬🇧 English:
Both check
GrantedAuthorityvalues.hasRole("ADMIN")adds the defaultROLE_prefix and looks forROLE_ADMIN, whilehasAuthority()checks the exact string. I use roles for coarse groups and authorities for fine-grained permissions likeORDER_DELETE.
Q2. 401 khác 403?
🇻🇳 Tiếng Việt:
401 nghĩa là chưa xác thực hợp lệ: thiếu hoặc sai credentials, token hết hạn. 403 nghĩa là đã xác thực nhưng không đủ quyền. Trong Spring,
ExceptionTranslationFiltergọiAuthenticationEntryPointcho 401 vàAccessDeniedHandlercho 403.
🇬🇧 English:
401 means valid authentication is missing or invalid, for example no token or an expired token. 403 means the user is authenticated but lacks permission. In Spring,
ExceptionTranslationFilteruses theAuthenticationEntryPointfor 401 and theAccessDeniedHandlerfor 403.
Q3. Làm sao ngăn user truy cập tài nguyên của người khác?
🇻🇳 Tiếng Việt:
Role-based authorization chưa đủ, phải có resource-level authorization: kiểm tra ownership ở Service hoặc ngay trong query như
findByIdAndUserId, lấy user id từ principal đã xác thực chứ không từ input client. Có thể dùng@PreAuthorizegọi bean như@orderSecurity.isOwner(...).
🇬🇧 English:
Role-based checks are not enough; we need resource-level authorization. I check ownership in the service layer or directly in the query, such as
findByIdAndUserId, using the user id from the authenticated principal, never from client input.@PreAuthorizecan also call a bean like@orderSecurity.isOwner(...).
Q4. AuthorizationManager là gì?
🇻🇳 Tiếng Việt:
Là interface trung tâm của authorization trong Spring Security 6, thay thế
AccessDecisionManagervà voter. Nó nhậnAuthenticationvà đối tượng cần bảo vệ (request hoặc method invocation), trả vềAuthorizationDecision.AuthorizationFiltervà method security đều dùng nó.
🇬🇧 English:
It is the central authorization interface in Spring Security 6, replacing
AccessDecisionManagerand voters. It takes theAuthenticationand the secured object, such as a request or a method invocation, and returns anAuthorizationDecision. BothAuthorizationFilterand method security use it.
Cách nhớ ngắn gọn
- Role = authority có
ROLE_;hasRoletự thêm prefix. - 401 = bạn là ai? 403 = tôi biết bạn, nhưng không.
- URL rule cho ranh giới, @PreAuthorize cho nghiệp vụ, query cho ownership.
- Kết thúc bằng
anyRequest().denyAll()hoặcauthenticated(); luật cụ thể trước.
Tự kiểm tra
hasRole("ADMIN") kiểm tra authority chuỗi gì?
ROLE_ADMIN.
USER gọi DELETE /api/admin/users/10 với credentials đúng thì nhận mã gì?
403 Forbidden.
Vì sao @PreAuthorize không chạy khi gọi this.method()?
Self-invocation không đi qua Spring proxy, nên interceptor không được gọi.
ADMIN xóa mọi đơn, USER chỉ xóa đơn của mình. Viết biểu thức?
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)").
BOLA là gì?
Broken Object Level Authorization: user truy cập object của người khác bằng cách đổi id, vì server chỉ kiểm quyền chung mà không kiểm ownership.