Building Microservices Application – Phần 3: Xác thực API bằng Oauth 2.0

Bài viết được sự cho phép của tác giả Edward Thien Hoang

Trong các bài viết trước chúng ta đã làm quen với Netflix Eureka, Ribbon, Zuul và Hystrix. Trong bài này hãy cùng làm quen với việc xác thực API sử dụng OAuth 2.0

Các bạn có thể tham khảo bài viết tiếng Anh tại: http://callistaenterprise.se/blogg/teknik/2015/04/27/building-microservices-part-3-secure-APIs-with-OAuth/

  Building Microservices Application - Phần 1: Sử dụng Netflix Eureka, Ribbon và Zuul
  Building Microservices Application - Phần 2: Xử lý "Chain of Failures" dùng Circuit Breaker Pattern với Netflix Hystrix

1. GIỚI THIỆU VỀ OAUTH VÀ SINGLE SIGN ON

Các hệ thống phân tán (decentralized system) ngày càng trở lên phổ biến và xác thực là một khía cạnh quan trọng của tất cả chúng. Xác thực Single Sign On (SSO) ngày càng trở nên cần thiết hơn bao giờ hết. Ngày nay, hầu hết các trang web đều yêu cầu xác thực để truy cập tới các tính năng và nội dung của nó. Với số lượng các trang web và dịch vụ đang tăng lên, một hệ thống đăng nhập tập trung (centralized login system) trở nên cần thiết.

SSO giải quyết một vấn đề lớn: làm thế nào để quản được số lượng người dùng đang tăng lên trên toàn bộ hệ thống gồm nhiều ứng dụng và dịch vụ. Các framework chẳng hạn như OpenID Connect và các dịch vụ chẳng hạn như Auth0 làm cho việc tích hợp Single Sign On vào các ứng dụng mới hoặc đã có của bạn trở nên dễ dàng hơn nhiều. Nếu bạn đang triển khai xác thực trên một ứng dụng hay dịch vụ mới hãy xem xét tích hợp SSO.

Đọc thêm tại: https://techmaster.vn/posts/34688/xac-thuc-single-sign-on-la-gi-va-no-hoat-dong-nhu-the-nao

OAuth2 là một chuẩn mở để ủy quyền/phân quyền (authorization), OAuth2 cũng là nền tảng của OpenID Connect, nó cung cấp OpenID (xác thực – authentication) ở phía trên của OAuth2 (ủy quyền – authorization) để có một giải pháp bảo mật hoàn chỉnh.

Đọc thêm tại: https://techmaster.vn/posts/34473/authentication-va-authorization-openid-vs-oauth2-vs-saml

Tiếp theo hãy vào phần áp dụng

2. KIẾN TRÚC

Chúng ta sẽ thêm một microservice mới có tên product-api hoạt động như một external API (hay còn gọi là Resource Server trong thuật ngữ của OAuth) và được expose thông qua edge server đóng vai trò token relay, tức là chuyển tiếp OAuth access tokens từ client đến resource server. Chúng ta cũng sẽ thêm OAuth Authorization Server và OAuth client ở phía Service consumer.

Phần được thêm mới được đóng khung màu đỏ trong diagram dưới đây:

1

Ghi chú:

  • Trong phần này chúng ta sẽ xây dựng một máy chủ OAuth authorization đơn giản để minh họa toàn bộ quá trình. Trong thực tế, bạn nên sử dụng các cơ chế SSO có sẵn từ Google, Facebook, Twitter.
  • Bài viết cũng chỉ sử dụng HTTP thay vì TSL như HTTPS

3. CHECKOUT SOURCE CODE

Yêu cầu Java SE 8, Git và Gradle.

git clone https://github.com/callistaenterprise/blog-microservices.git
cd blog-microservices
git checkout -b B3 M3.1
./build-all.sh

Nếu đang sử dụng Windows, bạn có thể thực thi tập tin bat tương ứng build-all.bat!

Hai phần mới được thêm vào là OAuth Authorization Server (auth-server) và OAuth Resource Server (product-api-service):

2

4. DIỄN GIẢI SOURCE CODE

4.1 GRADLE DEPENDENCIES

Để có thể sử dụng OAuth 2.0, chúng ta sẽ cần spring-cloud-security và spring-security-oauth2:

Đối với auth-server:

compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.security.oauth:spring-security-oauth2:2.0.6.RELEASE")

Đối với product-api-service:

compile("org.springframework.cloud:spring-cloud-starter-security:1.0.0.RELEASE")
compile("org.springframework.security.oauth:spring-security-oauth2:2.0.6.RELEASE")

4.2 AUTH-SERVER

Trước tiên là thêm @EnableAuthorizationServer annotation. Sau đó, sử dụng một configuration class để đăng ký các ứng dụng khách được approved (lưu trên memory thôi), chỉ định client-id, client-secret, allowed grant flows và scopes:

@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials")
.scopes("webshop");
}
}

Lưu ý: đây chỉ là cách chúng ta phát triển một OAuth server đơn giản để mô phỏng các máy chủ ủy quyền OAuth trong thế giới thực như Google, LinkedIn hoặc GitHub.

Để xem mã nguồn đầy đủ, hãy xem AuthserverApplication.java.

Đăng ký người dùng (Resource Owner trong thuật ngữ của OAuth) và mô phỏng Identity Provider (IdP) được thực hiện bằng cách thêm một dòng config cho mỗi user trong tệp application.properties, ví dụ:

security.user.password=password

Để xem mã nguồn đầy đủ, hãy xem application.properties.

Ngoài ra còn có hai giao diện người dùng web đơn giản dùng để user đăng nhập và cấp quyền, xem mã nguồn để biết chi tiết.

4.3 PRODUC-API-SERVICE

Để Produc-API-Service hoạt động như một máy chủ OAuth, chúng ta cần thêm @EnableOAuth2Resource annotation:

@EnableOAuth2Resource
public class ProductApiServiceApplication {

Để xem mã nguồn đầy đủ, hãy xem ProductApiServiceApplication.java.

Produc-API-Service rất giống với Composite service trong phần 2. Để có thể xác minh rằng OAuth hoạt động, chúng ta sẽ xuất ra log: id người dùng và access token, lưu ý việc output access token ra log chỉ nên được dùng trong môi trường test thôi nhé:

@RequestMapping("/{productId}")
@HystrixCommand(fallbackMethod = "defaultProductComposite")
public ResponseEntity getProductComposite(
@PathVariable int productId,
@RequestHeader(value="Authorization") String authorizationHeader,
Principal currentUser) {

LOG.info("ProductApi: User={}, Auth={}, called with productId={}",
currentUser.getName(), authorizationHeader, productId);
...

4.4 SỬA LẠI EDGE SERVER

Cuối cùng, chúng ta cần làm cho Edge Server chuyển tiếp OAuth access tokens đến API-service.

zuul:
ignoredServices: "*"
prefix: /api
routes:
productapi: /product/**

URL sample:
http://localhost:8765/api/product/123

Chúng ta cũng thay thế các route đến composite-service sang api-service.

5. KHỞI ĐỘNG SYSTEM

Việc khởi động cũng giống như trong phần 2.

Đầu tiên start RabbitMQ:

$ ~/Applications/rabbitmq_server-3.4.3/sbin/rabbitmq-server

Sau đó start các infrastructure services:

$ cd support/auth-server; ./gradlew bootRun
$ cd support/discovery-server; ./gradlew bootRun
$ cd support/edge-server; ./gradlew bootRun
$ cd support/monitor-dashboard; ./gradlew bootRun
$ cd support/turbine; ./gradlew bootRun

Và các business services:

$ cd core/product-service; ./gradlew bootRun
$ cd core/recommendation-service; ./gradlew bootRun
$ cd core/review-service; ./gradlew bootRun
$ cd composite/product-composite-service; ./gradlew bootRun
$ cd api/product-api-service; ./gradlew bootRun

Nếu đang sử dụng Windows, bạn có thể thực thi tập tin bat tương ứng start-all.bat!

Khi các microservices được khởi động và được đăng ký với máy chủ khám phá dịch vụ, chúng sẽ ghi các thông tin sau vào log:

DiscoveryClient … – registration status: 204

6. OAUTH AUTHORIZATION GRANT FLOWS

Đặc tả OAuth 2.0 giới thiệu bốn luồng cấp quyền để nhận access token:

3

LƯU Ý : Trong đó có Code và Implicit flow là được sử dụng rộng rãi nhất và trong bài viết này chỉ xét 2 trường hợp đó.

6.1 AUTHORIZATION CODE GRANT

Trước tiên, chúng ta cần lấy code grant (giống như one time password) bằng trình duyệt web:

http://localhost:9999/uaa/oauth/authorize? response_type=code& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=97536

Đăng nhập ( user/ password) và đưa ra sự đồng ý của bạn trong các trang web được hiển thị. Trình duyệt web sẽ chuyển hướng đến URL dưới đây:

http://example.com/?
code=IyJh4Y&
state=97536

Lấy tham số code từ URL và lưu trữ nó trong một biến môi trường:

CODE=IyJh4Y

Bây giờ sẽ dùng code grant để lấy về access token:

curl acme:acmesecret@localhost:9999/uaa/oauth/token \
-d grant_type=authorization_code \
-d client_id=acme \
-d redirect_uri=http://example.com \
-d code=$CODE -s | jq .
{
"access_token": "eba6a974-3c33-48fb-9c2e-5978217ae727",
"token_type": "bearer",
"refresh_token": "0eebc878-145d-4df5-a1bc-69a7ef5a0bc3",
"expires_in": 43105,
"scope": "webshop"
}

Lưu access token trong biến môi trường để sử dụng sau này khi chúng ta truy cập tới API:

TOKEN=eba6a974-3c33-48fb-9c2e-5978217ae727

Nếu bạn thực hiện lấy thêm mã token lần nữa bằng code grant trước đó, sẽ có thông báo lỗi vì code grant đó chỉ có giá trị one time.

curl acme:acmesecret@localhost:9999/uaa/oauth/token \
-d grant_type=authorization_code \
-d client_id=acme \
-d redirect_uri=http://example.com \
-d code=$CODE -s | jq .
{
"error": "invalid_grant",
"error_description": "Invalid authorization code: IyJh4Y"
}

6.2 IMPLICIT GRANT

Với implicit grant, chúng ta bỏ qua code grant, thay vào đó sẽ yêu cầu access token trực tiếp từ trình duyệt web (bảo mật thấp hơn). Sử dụng URL sau trong trình duyệt web:

http://localhost:9999/uaa/oauth/authorize? response_type=token& client_id=acme& redirect_uri=http://example.com& scope=webshop& state=48532

Đăng nhập ( user/ password) và đưa ra sự đồng ý cấp quyền. Trình duyệt web sẽ chuyển hướng đến URL dưới đây:

http://example.com/#
access_token=00d182dc-9f41-41cd-b37e-59de8f882703&
token_type=bearer&
state=48532&
expires_in=42704

Lưu access token trong biến môi trường để sử dụng sau này khi truy cập API:

TOKEN=00d182dc-9f41-41cd-b37e-59de8f882703

7. ACCESS ĐẾN API

Bây giờ, khi đã có access token, chúng ta có thể bắt đầu truy cập API.

Trước tiên, hãy thử truy cập vào API mà không có access token, nó sẽ không thành công:

curl 'http://localhost:8765/api/product/123' -s | jq .
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

Tiếp theo, hãy thử với một access token không hợp lệ, nó cũng sẽ thất bại:

curl 'http://localhost:8765/api/product/123' \
-H "Authorization: Bearer invalid-access-token" -s | jq .
{
"error": "access_denied",
"error_description": "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."
}

Bây giờ, chúng ta thực hiện một request đúng cung cấp một trong các access token có được ở bước phía trên:

curl 'http://localhost:8765/api/product/123' \
-H "Authorization: Bearer $TOKEN" -s | jq .
{
"productId": 123,
"name": "name",
"weight": 123,
"recommendations": [...],
"reviews": [... ]
}

Ngon!

Check log events trong product-api-service:

2015-04-23 18:39:59.014 INFO 79321 --- [ XNIO-2 task-20] o.s.c.s.o.r.UserInfoTokenServices : Getting user info from: http://localhost:9999/uaa/user
2015-04-23 18:39:59.030 INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService : ProductApi: User=user, Auth=Bearer a0f91d9e-00a6-4b61-a59f-9a084936e474, called with productId=123
2015-04-23 18:39:59.381 INFO 79321 --- [ctApiService-10] s.c.m.a.p.service.ProductApiService : GetProductComposite http-status: 200

Chúng ta có thể thấy rằng API liên hệ với máy chủ ủy quyền để nhận thông tin về người dùng và sau đó in ra tên người dùng và access token trong log!

Cuối cùng, hãy thử làm mất hiệu lực của access token, bằng cách ví dụ như làm cho nó hết hạn. Một cách để làm điều đó là khởi động lại auth-server (nó chỉ lưu trữ thông tin trong bộ nhớ…) và sau đó thử lại:

curl 'http://localhost:8765/api/product/123' \
-H "Authorization: Bearer $TOKEN" -s | jq .
{
"error": "access_denied",
"error_description": "Unable to obtain a new access token for resource 'null'. The provider manager is not configured to support it."
}

Access token được chấp nhận trước đây hiện bị từ chối.

8. TÓM TẮT

Các hệ thống phân tán (decentralized system) ngày càng trở lên phổ biến và xác thực là một khía cạnh quan trọng của tất cả chúng. SSO giải quyết một vấn đề lớn: làm thế nào để quản được số lượng người dùng đang tăng lên trên toàn bộ hệ thống gồm nhiều ứng dụng và dịch vụ. Các framework chẳng hạn như OpenID Connect và các dịch vụ chẳng hạn như Auth0 làm cho việc tích hợp Single Sign On vào các ứng dụng mới hoặc đã có của bạn trở nên dễ dàng hơn nhiều. Nếu bạn đang triển khai xác thực trên một ứng dụng hay dịch vụ mới hãy xem xét tích hợp SSO.

Bài viết đã trình bày, mô phỏng cách xác thực với OAuth 2.0. Lưu ý khi triển khai thực tế, các bạn nên dùng luôn các cơ chế xác thực của Google, Facebook.

9. TIẾP THEO

Trong bài viết tiếp theo, chúng ta sẽ đến với việc quản lý log tập trung sử dụng ELK stack bao gồm: Elasticsearch, LogStash and Kibana.

Series này là phần tiếp theo series Microservices: Từ Thiết Kế Đến Triển Khai, tập trung vào việc hiện thực hóa các khái niệm đã mô tả ở phần lý thuyết. Hi vọng sẽ giúp ích được cho các bạn khi làm quen với microservies. Các bạn có thể tham khảo thêm về phần Architecture căn bản ở đây nhé.

Tổng hợp và dịch by edwardthienhoang

Bài viết gốc được đăng tải tại edwardthienhoang.wordpress.com

Có thể bạn quan tâm:

Xem thêm Việc làm Developer hấp dẫn trên TopDev