Authorization: role, authority, 401 vs 403

4 câu hỏi~30 phútTừ khóa: Role, Permission, GrantedAuthority, hasRole, hasAuthority, requestMatchers, @PreAuthorize, AuthorizationManager, RBAC, BOLA, ownership

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:

  1. User thường không được gọi API của Admin.
  2. User chỉ làm được những hành động mình có quyền.
  3. User A không được xem hay xóa đơn hàng của User B.

1. Bài toán

UsernameRole
johnUSER
adminADMIN
API của shop-appYêu cầu
GET /api/public/productsPublic
GET /api/ordersUSER hoặc ADMIN
DELETE /api/admin/users/10ADMIN
HTTP Request
     |
Authentication   (credentials/token hợp lệ?)  -- không --> 401
     |
Authorization    (có quyền?)                  -- không --> 403
     |
Controller

2. Vấn đề

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_.

MethodAuthority 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ầngVí dụ
Filter chain (URL)/api/admin/** yêu cầu ADMIN
Method security@PreAuthorize("hasAuthority('ORDER_READ')")
Business logicOrder phải thuộc user hiện tại
Database queryfindByIdAndUserId()

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 Unauthorized403 Forbidden
Ý nghĩa thậtChư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ýAuthenticationEntryPointAccessDeniedHandler
Client nên làmĐăng nhập lại / refresh tokenKhông retry, báo không có quyền
URL rules@PreAuthorize
Vị tríFilter, trước DispatcherServletProxy quanh bean
Biết tham số method?KhôngCó (#orderId)
Hợp vớiLuật thô theo pathLuật nghiệp vụ, ownership

7. Khi nào nên dùng / không nên dùng

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 iduser_idtotal
1001 (john)500
1012 (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ìm ROLE_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 GrantedAuthority values. hasRole("ADMIN") adds the default ROLE_ prefix and looks for ROLE_ADMIN, while hasAuthority() checks the exact string. I use roles for coarse groups and authorities for fine-grained permissions like ORDER_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, ExceptionTranslationFilter gọi AuthenticationEntryPoint cho 401 và AccessDeniedHandler cho 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, ExceptionTranslationFilter uses the AuthenticationEntryPoint for 401 and the AccessDeniedHandler for 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 @PreAuthorize gọ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. @PreAuthorize can 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ế AccessDecisionManager và voter. Nó nhận Authentication và đối tượng cần bảo vệ (request hoặc method invocation), trả về AuthorizationDecision. AuthorizationFilter và method security đều dùng nó.

🇬🇧 English:

It is the central authorization interface in Spring Security 6, replacing AccessDecisionManager and voters. It takes the Authentication and the secured object, such as a request or a method invocation, and returns an AuthorizationDecision. Both AuthorizationFilter and method security use it.

Cách nhớ ngắn gọn


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.