Skip to content

8.3 Bảo vệ một API bằng resource server

Resource server thực chất chỉ là một bộ lọc nằm phía trước API, đảm bảo rằng các yêu cầu truy cập vào tài nguyên cần ủy quyền phải bao gồm access token hợp lệ với scope phù hợp. Spring Security cung cấp một triển khai resource server theo chuẩn OAuth2 mà bạn có thể thêm vào API hiện có bằng cách thêm dependency sau vào file cấu hình build của dự án:

xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Bạn cũng có thể thêm dependency resource server bằng cách chọn “OAuth2 Resource Server” trong Spring Initializr khi tạo project.

Sau khi thêm dependency, bước tiếp theo là khai báo rằng các yêu cầu POST đến /ingredients cần scope "writeIngredients" và các yêu cầu DELETE đến /ingredients cần scope "deleteIngredients". Đoạn trích sau từ lớp SecurityConfig của dự án cho thấy cách cấu hình:

java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
  ...
    .antMatchers(HttpMethod.POST, "/api/ingredients")
      .hasAuthority("SCOPE_writeIngredients")
    .antMatchers(HttpMethod.DELETE, "/api//ingredients")
      .hasAuthority("SCOPE_deleteIngredients")
  ...
}

Đối với từng endpoint, phương thức .hasAuthority() được gọi để chỉ định scope yêu cầu. Lưu ý rằng các scope được thêm tiền tố "SCOPE_" để khớp với các scope OAuth2 trong access token được gửi kèm theo yêu cầu truy cập tài nguyên.

Trong cùng lớp cấu hình đó, chúng ta cũng cần bật chế độ resource server, như sau:

java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    ...
      .and()
        .oauth2ResourceServer(oauth2 -> oauth2.jwt())
  ...
}

Phương thức oauth2ResourceServer() ở đây nhận một lambda để cấu hình resource server. Ở đây, ta chỉ bật JWT token (thay vì opaque token), cho phép resource server kiểm tra nội dung của token để xác định các quyền (claim) bảo mật mà token chứa. Cụ thể, nó sẽ kiểm tra xem token có chứa scope "writeIngredients" và/hoặc "deleteIngredients" cho hai endpoint mà chúng ta đã bảo vệ.

Tuy nhiên, server sẽ không tin tưởng token một cách mù quáng. Để chắc chắn rằng token được tạo bởi một authorization server đáng tin cậy thay mặt cho người dùng, resource server sẽ xác minh chữ ký của token bằng khóa công khai (public key) tương ứng với khóa riêng (private key) đã dùng để ký token đó. Chúng ta cần cấu hình resource server biết nơi để lấy khóa công khai. Thuộc tính sau sẽ chỉ định URL của bộ JWK từ authorization server để resource server có thể truy xuất public key:

yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: http://localhost:9000/oauth2/jwks

Giờ đây resource server của chúng ta đã sẵn sàng! Build ứng dụng Taco Cloud và khởi chạy nó. Sau đó bạn có thể thử với curl như sau:

bash
$ curl localhost:8080/ingredients \
  -H"Content-type: application/json" \
  -d'{"id":"CRKT", "name":"Legless Crickets", "type":"PROTEIN"}'

Yêu cầu này sẽ thất bại với mã phản hồi HTTP 401. Lý do là vì endpoint đã được cấu hình yêu cầu scope "writeIngredients", và chúng ta chưa cung cấp access token hợp lệ với scope đó trong yêu cầu.

Để gửi yêu cầu thành công và thêm một nguyên liệu mới, bạn cần lấy access token bằng quy trình đã sử dụng trong phần trước, đảm bảo rằng bạn yêu cầu các scope "writeIngredients""deleteIngredients" khi chuyển hướng trình duyệt đến authorization server. Sau đó, cung cấp access token trong header "Authorization" bằng curl như sau (thay "$token" bằng access token thực tế):

bash
$ curl localhost:8080/ingredients \
  -H"Content-type: application/json" \
  -d'{"id":"SHMP", "name":"Coconut Shrimp", "type":"PROTEIN"}' \
  -H"Authorization: Bearer $token"

Lần này nguyên liệu mới sẽ được tạo. Bạn có thể xác minh điều đó bằng cách sử dụng curl hoặc HTTP client yêu thích của bạn để thực hiện yêu cầu GET đến endpoint /ingredients như sau:

bash
$ curl localhost:8080/ingredients
[
  {
    "id": "FLTO",
    "name": "Flour Tortilla",
    "type": "WRAP"
  },

  ...

  {
    "id": "SHMP",
    "name": "Coconut Shrimp",
    "type": "PROTEIN"
  }
]

Nguyên liệu Coconut Shrimp giờ đây sẽ được hiển thị ở cuối danh sách các nguyên liệu được trả về từ endpoint /ingredients. Thành công rồi!

Hãy nhớ rằng access token sẽ hết hạn sau 5 phút. Nếu bạn để token hết hạn, các yêu cầu sẽ bắt đầu trả về mã HTTP 401 một lần nữa. Nhưng bạn có thể lấy một access token mới bằng cách gửi yêu cầu đến authorization server sử dụng refresh token mà bạn nhận được kèm theo access token (thay thế "$refreshToken" bằng refresh token thực tế), như sau:

bash
$ curl localhost:9000/oauth2/token \
  -H"Content-type: application/x-www-form-urlencoded" \
  -d"grant_type=refresh_token&refresh_token=$refreshToken" \
  -u taco-admin-client:secret

Với access token mới tạo, bạn có thể tiếp tục tạo thêm nhiều nguyên liệu mới theo ý mình.

Bây giờ chúng ta đã bảo vệ được endpoint /ingredients, có lẽ cũng là lúc áp dụng các kỹ thuật tương tự để bảo vệ những endpoint nhạy cảm khác trong API. Ví dụ, endpoint /orders không nên mở cho bất kỳ loại yêu cầu nào, kể cả GET, vì điều đó sẽ cho phép hacker dễ dàng lấy thông tin khách hàng. Tôi sẽ để phần bảo mật cho endpoint /orders và phần còn lại của API tùy bạn quyết định.

Việc quản trị ứng dụng Taco Cloud bằng curl rất hữu ích để thử nghiệm và tìm hiểu cách mà OAuth2 token được dùng để cấp quyền truy cập tài nguyên. Nhưng cuối cùng thì chúng ta muốn có một ứng dụng client thực sự có thể được dùng để quản lý nguyên liệu. Giờ hãy chuyển sự chú ý sang việc tạo một client có hỗ trợ OAuth, có thể lấy access token và gửi yêu cầu đến API.

Released under the MIT License.