8.1 Giới thiệu về OAuth 2
Giả sử chúng ta muốn tạo một ứng dụng quản trị nội bộ mới để quản lý ứng dụng Taco Cloud. Cụ thể hơn, giả sử rằng chúng ta muốn ứng dụng mới này có thể quản lý các nguyên liệu có sẵn trên trang web chính của Taco Cloud.
Trước khi bắt đầu viết mã cho ứng dụng quản trị, chúng ta cần thêm một số endpoint mới vào API của Taco Cloud để hỗ trợ việc quản lý nguyên liệu. REST controller trong đoạn mã sau cung cấp ba endpoint để liệt kê, thêm mới và xóa nguyên liệu.
Danh sách 8.1 Một controller để quản lý nguyên liệu có sẵn
package tacos.web.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import tacos.Ingredient;
import tacos.data.IngredientRepository;
@RestController
@RequestMapping(path="/api/ingredients", produces="application/json")
@CrossOrigin(origins="*")
public class IngredientController {
private IngredientRepository repo;
@Autowired
public IngredientController(IngredientRepository repo) {
this.repo = repo;
}
@GetMapping
public Iterable<Ingredient> allIngredients() {
return repo.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Ingredient saveIngredient(@RequestBody Ingredient ingredient) {
return repo.save(ingredient);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteIngredient(@PathVariable("id") String ingredientId) {
repo.deleteById(ingredientId);
}
}Tuyệt vời! Giờ tất cả những gì chúng ta cần làm là bắt đầu xây dựng ứng dụng quản trị, gọi các endpoint trên ứng dụng chính Taco Cloud khi cần để thêm hoặc xóa nguyên liệu.
Nhưng khoan đã—API đó hiện chưa được bảo mật. Nếu ứng dụng backend của chúng ta có thể gửi các yêu cầu HTTP để thêm hoặc xóa nguyên liệu, thì bất kỳ ai khác cũng có thể làm điều đó. Ngay cả khi dùng công cụ dòng lệnh curl, ai đó cũng có thể thêm một nguyên liệu mới như sau:
$ curl localhost:8080/ingredients \
-H"Content-type: application/json" \
-d'{"id":"FISH","name":"Stinky Fish", "type":"PROTEIN"}'Họ thậm chí có thể dùng curl để xóa nguyên liệu hiện có như sau:
curl localhost:8080/ingredients/GRBF -X DELETEAPI này là một phần của ứng dụng chính và hiện công khai với thế giới; thực tế, endpoint GET được giao diện người dùng của ứng dụng chính sử dụng trong file home.html. Do đó, rõ ràng chúng ta cần bảo vệ ít nhất các endpoint POST và DELETE.
Một lựa chọn là sử dụng xác thực HTTP Basic để bảo vệ các endpoint /ingredients. Điều này có thể được thực hiện bằng cách thêm @PreAuthorize vào các phương thức xử lý như sau:
@PostMapping
@PreAuthorize("#{hasRole('ADMIN')}")
public Ingredient saveIngredient(@RequestBody Ingredient ingredient) {
return repo.save(ingredient);
}
@DeleteMapping("/{id}")
@PreAuthorize("#{hasRole('ADMIN')}")
public void deleteIngredient(@PathVariable("id") String ingredientId) {
repo.deleteById(ingredientId);
}Hoặc, các endpoint có thể được bảo mật trong cấu hình bảo mật như sau:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/ingredients").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/ingredients/**").hasRole("ADMIN")
...
}Có nên sử dụng tiền tố
"ROLE_"hay khôngCác authority trong Spring Security có thể ở nhiều dạng khác nhau, bao gồm vai trò (role), quyền (permission) và (như chúng ta sẽ thấy sau này) phạm vi OAuth2 (scope). Vai trò (role), cụ thể, là một dạng authority đặc biệt với tiền tố
"ROLE_". Khi làm việc với các phương thức hoặc biểu thức SpEL xử lý trực tiếp vai trò, nhưhasRole(), tiền tố"ROLE_"được suy diễn tự động. Do đó, một lời gọi đếnhasRole("ADMIN")sẽ kiểm tra nội bộ xem có authority tên"ROLE_ADMIN"hay không. Bạn không cần (và thực tế không nên) dùng tiền tố"ROLE_"khi gọi các phương thức này vì điều đó sẽ gây ra tiền tố kép"ROLE_ROLE_ADMIN". Các phương thức và hàm khác của Spring Security xử lý authority một cách tổng quát hơn cũng có thể được dùng để kiểm tra vai trò. Nhưng trong các trường hợp đó, bạn phải thêm tiền tố"ROLE_"một cách tường minh. Ví dụ, nếu bạn chọn sử dụnghasAuthority()thay vìhasRole(), bạn cần truyền vào"ROLE_ADMIN"thay vì"ADMIN".
Dù theo cách nào, để có thể gửi yêu cầu POST hoặc DELETE đến /ingredients, người gửi yêu cầu cũng phải cung cấp thông tin xác thực có quyền "ROLE_ADMIN". Ví dụ, khi dùng curl, thông tin xác thực có thể được chỉ định bằng tham số -u, như sau:
$ curl localhost:8080/ingredients \
-H"Content-type: application/json" \
-d'{"id":"FISH","name":"Stinky Fish", "type":"PROTEIN"}' \
-u admin:l3tm31nMặc dù HTTP Basic có thể khóa API, nhưng nó lại khá... ừm... cơ bản. Nó yêu cầu client và API phải chia sẻ thông tin xác thực người dùng, điều này có thể dẫn đến việc trùng lặp dữ liệu. Hơn nữa, mặc dù thông tin xác thực HTTP Basic được mã hóa Base64 trong header của yêu cầu, nếu một hacker chặn được yêu cầu đó, họ có thể dễ dàng lấy được, giải mã và sử dụng cho mục đích xấu. Nếu điều đó xảy ra, mật khẩu sẽ cần phải được thay đổi, đồng nghĩa với việc tất cả các client cũng phải cập nhật và xác thực lại.
Điều gì sẽ xảy ra nếu thay vì yêu cầu người dùng admin phải xác thực trong mỗi yêu cầu, API chỉ cần một token chứng minh họ có quyền truy cập tài nguyên? Điều này tương tự như một vé tham dự sự kiện thể thao. Để vào sân, người kiểm soát không cần biết bạn là ai; họ chỉ cần biết bạn có vé hợp lệ. Nếu có, bạn được phép vào.
Đó cũng là cách mà ủy quyền OAuth 2 hoạt động. Các client yêu cầu một access token—tương tự như "valet key"—từ một máy chủ ủy quyền, với sự cho phép rõ ràng từ người dùng. Token đó cho phép client tương tác với API thay mặt người dùng đã cấp quyền. Token có thể hết hạn hoặc bị thu hồi bất cứ lúc nào mà không cần thay đổi mật khẩu của người dùng. Trong trường hợp đó, client chỉ cần yêu cầu một access token mới để tiếp tục hoạt động thay mặt người dùng. Quy trình này được minh họa trong hình 8.1.

Hình 8.1 Luồng ủy quyền OAuth 2 theo mã ủy quyền (authorization code)
OAuth 2 là một đặc tả bảo mật rất phong phú với nhiều cách sử dụng khác nhau. Luồng được mô tả trong hình 8.1 được gọi là authorization code grant. Các luồng khác được hỗ trợ trong đặc tả OAuth 2 bao gồm:
- Implicit grant — Giống như authorization code grant, implicit grant chuyển hướng trình duyệt của người dùng đến máy chủ ủy quyền để lấy sự đồng ý từ người dùng. Nhưng khi chuyển hướng trở lại, thay vì trả về mã ủy quyền trong yêu cầu, access token được cấp ngay trong yêu cầu đó. Mặc dù ban đầu được thiết kế cho các client JavaScript chạy trong trình duyệt, luồng này hiện không được khuyến nghị sử dụng nữa và authorization code grant được ưa chuộng hơn.
- User credentials (hoặc password) grant — Trong luồng này, không có chuyển hướng nào xảy ra và có thể không cần trình duyệt web. Thay vào đó, client thu thập thông tin xác thực của người dùng và đổi lấy access token một cách trực tiếp. Luồng này có vẻ phù hợp với các client không dựa trên trình duyệt, nhưng các ứng dụng hiện đại thường yêu cầu người dùng truy cập một trang web để thực hiện authorization code grant, nhằm tránh phải xử lý trực tiếp thông tin đăng nhập của người dùng.
- Client credentials grant — Luồng này giống với user credentials grant, ngoại trừ việc client sử dụng thông tin xác thực của chính nó thay vì của người dùng để đổi lấy access token. Tuy nhiên, token được cấp trong trường hợp này bị giới hạn phạm vi và không thể sử dụng để thay mặt người dùng.
Đối với mục đích của chúng ta, chúng ta sẽ tập trung vào luồng authorization code grant để nhận về một access token dưới dạng JSON Web Token (JWT). Điều này sẽ bao gồm việc tạo một số ứng dụng hoạt động cùng nhau, bao gồm:
- Máy chủ ủy quyền (authorization server) — Nhiệm vụ của máy chủ ủy quyền là lấy sự cho phép từ người dùng thay mặt cho ứng dụng client. Nếu người dùng cấp quyền, máy chủ ủy quyền sẽ cung cấp access token cho client để truy cập API một cách xác thực.
- Máy chủ tài nguyên (resource server) — Đây là tên gọi khác của một API được bảo vệ bởi OAuth 2. Mặc dù resource server là một phần của chính API, nhưng để tiện cho việc thảo luận, chúng ta sẽ coi đó là hai khái niệm riêng biệt. Resource server sẽ từ chối truy cập tài nguyên nếu yêu cầu không có access token hợp lệ với phạm vi quyền thích hợp. Trong trường hợp của chúng ta, API Taco Cloud mà chúng ta bắt đầu ở chương 7 sẽ đóng vai trò là resource server, sau khi được thêm cấu hình bảo mật.
- Ứng dụng client — Đây là ứng dụng muốn tiêu thụ API nhưng cần sự cho phép để làm điều đó. Chúng ta sẽ xây dựng một ứng dụng quản trị đơn giản cho Taco Cloud để có thể thêm các nguyên liệu mới.
- Người dùng — Đây là con người thực tế sử dụng ứng dụng client và cấp quyền cho ứng dụng truy cập API của resource server thay mặt họ.
Trong luồng authorization code grant, một chuỗi các chuyển hướng trình duyệt giữa client và authorization server sẽ xảy ra khi client lấy access token. Quá trình bắt đầu bằng việc client chuyển hướng trình duyệt của người dùng đến authorization server, yêu cầu các quyền cụ thể (hay còn gọi là “scope”). Authorization server sau đó yêu cầu người dùng đăng nhập và chấp thuận các quyền được yêu cầu. Sau khi người dùng đồng ý, authorization server sẽ chuyển hướng trình duyệt trở lại client với một mã mà client có thể dùng để đổi lấy access token. Sau khi có được token, client có thể sử dụng nó để tương tác với API của resource server bằng cách thêm vào header "Authorization" trong mỗi yêu cầu.
Mặc dù chúng ta chỉ tập trung vào một cách sử dụng cụ thể của OAuth 2, bạn được khuyến khích tìm hiểu sâu hơn về chủ đề này bằng cách đọc đặc tả OAuth 2 tại https://oauth.net/2/ hoặc tham khảo một trong các cuốn sách chuyên sâu về chủ đề này.
- OAuth 2 in Action: [https://www.manning.com/books/oauth-2-in-action]https://www.manning.com/books/oauth-2-in-action
- Microservices Security in Action: [https://www.manning.com/books/microservices-security-in-action]https://www.manning.com/books/microservices-security-in-action
- API Security in Action: [https://www.manning.com/books/api-security-in-action]https://www.manning.com/books/api-security-in-action
You might also want to have a look at a liveProject called “Protecting User Data with Spring Security and OAuth2”(https://www.manning.com/liveproject/protecting-user-data-with-spring-security-and-oauth2).
Trong vài năm, một dự án có tên là Spring Security for OAuth đã cung cấp hỗ trợ cho cả OAuth 1.0a và OAuth 2. Dự án này tách biệt với Spring Security nhưng được phát triển bởi cùng một nhóm phát triển. Tuy nhiên, trong những năm gần đây, nhóm Spring Security đã tích hợp các thành phần client và resource server vào chính Spring Security.
Về phía authorization server, đã có quyết định rằng nó sẽ không được bao gồm trong Spring Security. Thay vào đó, các nhà phát triển được khuyến khích sử dụng các authorization server từ các nhà cung cấp khác nhau như Okta, Google, và những nhà cung cấp khác. Nhưng, do nhu cầu từ cộng đồng nhà phát triển, nhóm Spring Security đã bắt đầu một dự án có tên là Spring Authorization Server.² Dự án này được gắn nhãn “experimental” (thử nghiệm) và được định hướng để sau này do cộng đồng điều hành, nhưng nó là một cách tuyệt vời để bắt đầu với OAuth 2 mà không cần đăng ký sử dụng một trong các implementation authorization server khác.
Trong phần còn lại của chương này, chúng ta sẽ xem cách sử dụng OAuth 2 với Spring Security. Trong quá trình đó, chúng ta sẽ tạo hai dự án mới: một dự án authorization server và một dự án client, đồng thời sẽ chỉnh sửa dự án Taco Cloud hiện tại của chúng ta sao cho API của nó hoạt động như một resource server. Chúng ta sẽ bắt đầu bằng cách tạo một authorization server sử dụng dự án Spring Authorization Server.
