Skip to content

5.4 Áp dụng bảo mật ở cấp độ phương thức

Mặc dù việc thiết lập bảo mật ở cấp độ các yêu cầu web là khá dễ hiểu, nhưng không phải lúc nào các ràng buộc bảo mật cũng nên được áp dụng ở đó. Đôi khi, việc xác minh người dùng đã xác thực và có đủ quyền nên được thực hiện ngay tại điểm mà hành động bảo mật sẽ diễn ra.

Ví dụ, giả sử vì mục đích quản trị, chúng ta có một lớp dịch vụ bao gồm một phương thức để xóa tất cả đơn hàng khỏi cơ sở dữ liệu. Sử dụng một OrderRepository được tiêm vào, phương thức đó có thể trông như sau:

java
public void deleteAllOrders() {
  orderRepository.deleteAll();
}

Bây giờ, giả sử chúng ta có một controller gọi phương thức deleteAllOrders() khi nhận được một yêu cầu POST, như sau:

java
@Controller
@RequestMapping("/admin")
public class AdminController {

private OrderAdminService adminService;

  public AdminController(OrderAdminService adminService) {
    this.adminService = adminService;
  }

  @PostMapping("/deleteOrders")
  public String deleteAllOrders() {
    adminService.deleteAllOrders();
    return "redirect:/admin";
  }

}

Sẽ rất dễ dàng để điều chỉnh SecurityConfig như sau để đảm bảo rằng chỉ những người dùng có thẩm quyền mới có thể thực hiện yêu cầu POST đó:

java
.authorizeRequests()
  ...
  .antMatchers(HttpMethod.POST, "/admin/**")
        .access("hasRole('ADMIN')")
  ....

Điều này rất tuyệt và sẽ ngăn không cho bất kỳ người dùng không được ủy quyền nào gửi yêu cầu POST đến /admin/deleteOrders, vốn có thể dẫn đến việc tất cả đơn hàng bị xóa khỏi cơ sở dữ liệu.

Tuy nhiên, giả sử có một phương thức controller khác cũng gọi deleteAllOrders(). Khi đó, bạn sẽ phải thêm nhiều matcher hơn để bảo vệ các yêu cầu từ những controller khác cần được bảo vệ.

Thay vào đó, chúng ta có thể áp dụng bảo mật trực tiếp trên phương thức deleteAllOrders() như sau:

java
`@PreAuthorize` ("hasRole('ADMIN')")
public void deleteAllOrders() {
  orderRepository.deleteAll();
}

Chú thích @PreAuthorize nhận vào một biểu thức SpEL, và nếu biểu thức đánh giá là false, phương thức sẽ không được gọi. Ngược lại, nếu biểu thức trả về true, phương thức sẽ được phép thực thi. Trong trường hợp này, @PreAuthorize kiểm tra xem người dùng có quyền ROLE_ADMIN hay không. Nếu có, phương thức sẽ được gọi và tất cả đơn hàng sẽ bị xóa. Nếu không, việc gọi sẽ bị dừng ngay lập tức.

Trong trường hợp @PreAuthorize chặn lời gọi, Spring Security sẽ ném ra AccessDeniedException. Đây là một ngoại lệ không được kiểm tra, vì vậy bạn không cần phải bắt nó trừ khi bạn muốn xử lý nó theo cách tùy chỉnh. Nếu không bị bắt, ngoại lệ này sẽ được truyền lên và cuối cùng sẽ bị chặn bởi các bộ lọc của Spring Security, sau đó sẽ xử lý bằng cách hiển thị trang lỗi HTTP 403 hoặc chuyển hướng đến trang đăng nhập nếu người dùng chưa xác thực.

Để @PreAuthorize hoạt động, bạn cần bật tính năng bảo mật cấp độ phương thức toàn cục. Bạn có thể làm điều đó bằng cách thêm chú thích @EnableGlobalMethodSecurity vào lớp cấu hình bảo mật như sau:

java
@Configuration
@EnableGlobalMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  ...
}

Bạn sẽ thấy @PreAuthorize là một chú thích hữu ích cho hầu hết các nhu cầu bảo mật ở cấp độ phương thức. Tuy nhiên, bạn cũng nên biết rằng có một chú thích tương tự nhưng hoạt động sau khi phương thức được gọi là @PostAuthorize. Chú thích này hoạt động gần giống @PreAuthorize, ngoại trừ việc biểu thức sẽ chỉ được đánh giá sau khi phương thức mục tiêu được gọi và trả về. Điều này cho phép bạn sử dụng giá trị trả về của phương thức trong biểu thức kiểm tra quyền.

Ví dụ, giả sử chúng ta có một phương thức dùng để lấy đơn hàng theo ID. Nếu bạn muốn giới hạn chỉ admin hoặc chủ nhân của đơn hàng mới có thể truy cập, bạn có thể dùng @PostAuthorize như sau:

java
@PostAuthorize("hasRole('ADMIN') || " +
    "returnObject.user.username == authentication.name")
public TacoOrder getOrder(long id) {
...
}

Trong trường hợp này, returnObject là đối tượng TacoOrder được trả về từ phương thức. Nếu thuộc tính user.username của đơn hàng đó bằng với authentication.name, tức là tên người dùng hiện tại, thì truy cập sẽ được phép.

Nhưng chờ đã! Làm sao bạn có thể bảo vệ một phương thức khỏi bị gọi nếu điều kiện kiểm tra quyền lại phụ thuộc vào giá trị trả về của chính phương thức đó? Vấn đề "gà và trứng" này được giải quyết bằng cách cho phép phương thức được gọi, sau đó ném ra AccessDeniedException nếu biểu thức trả về false.

Released under the MIT License.