Hướng dẫn lập trình Flask – Phần 23: Xây dựng API

flask_tutorial_1

Đây là phần 23 và cũng là phần cuối của loạt bài này. Trong phần này, chúng ta sẽ tìm hiểu cách xây dựng giao diện lập trình ứng dụng (Application Programming Interface hay gọi tắt là API) cho ứng dụng của chúng ta để các ứng dụng khác có thể làm việc trực tiếp với Myblog mà không cần thông qua trình duyệt.

Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:

Bạn có thể truy cập mã nguồn cho phần này tại GitHub.

Cho đến thời điểm này, tất cả các chức năng mà chúng ta đã tạo ra trong ứng dụng chỉ có thể được sử dụng thông qua trình duyệt. Nhưng trong thực tế, một ứng dụng trên nền tảng Internet có thể được truy cập qua các chương trình khách (client) khác nhau. Ví dụ như nếu chúng ta muốn tạo ra một ứng dụng iOS hoặc Android sử dụng ứng dụng của chúng ta, chúng ta phải làm thế nào? Chúng ta có thể chọn một trong hai cách. Giải pháp dễ dàng nhất là tạo một ứng dụng đơn giản với một thành phần hiển thị Web (Web view) và kết nối với Web site có ứng dụng Myblog. Tuy nhiên, điều này không khác gì truy cập đến Myblog thông qua trình duyệt trên thiết bị. Một giải pháp tốt hơn nhiều (nhưng cũng tốn công hơn) là tạo ra một ứng dụng hoàn chỉnh. Nhưng điều này lại làm nảy sinh một vấn đề mới là làm sao để ứng dụng này có thể tương tác với máy chủ và ứng dụng chỉ trả về các trang HTML?

Để giải quyết vấn đề này, chúng ta có thể nhờ đến sự giúp đỡ của Giao diện lập trình ứng dụng (API). Một API là một tập hợp các định tuyến HTTP được thiết kế như là các đầu mối xuất nhập ở mức thấp đến ứng dụng. Thay vì định nghĩa các định tuyến và các hàm hiển thị để trả về các trang HTML cho trình duyệt, các API cho phép các chương trình khách làm việc trực tiếp với các tài nguyên của ứng dụng và để cho các chương trình khách toàn quyền quyết định cách hiển thị các thông tin với người sử dụng.  Ví dụ như một API trong Myblog có thể cung cấp thông tin về user và bài viết cho chương trình khách hoặc cho phép user soạn thảo một bài viết có sẵn, nhưng ở mức dữ liệu và không pha trộn giữa dữ liệu và các logic về dữ liệu với HTML.  

Nếu bạn xem lại các định tuyến hiện có trong ứng dụng của chúng ta, bạn sẽ thấy một vài định tuyến phù hợp với định nghĩa về API như trên. Ví dụ như là các định tuyến trả về dữ liệu dạng JSON như là /translate trong Phần 14. Đây là một định tuyến với dữ liệu nhập là văn bản, ngôn ngữ nguồn và ngôn ngữ đích dưới dạng JSON và được gởi đến server bằng yêu cầu dạng POST. Yêu cầu này sẽ được server hồi đáp bằng văn bản được dịch cũng dưới dạng JSON. Server chỉ trả về dữ liệu được yêu cầu và giao trách nhiệm hiển thị dữ liệu này cho chương trình khách.

Tuy nhiên, dù các định tuyến loại này trong ứng dụng nằm trong phạm trù của API, chúng được thiết kế để hỗ trợ cho ứng dụng qua trình duyệt. Các ứng dụng trong điện thoại di động vẫn chưa thể sử dụng các định tuyến này vì chúng đòi hỏi user phải đăng nhập, và việc đăng nhập chỉ có thể được thực hiện qua một form HTML. Trong phần này, chúng ta sẽ thấy cách xây dựng các API không phụ thuộc vào trình duyệt và không đặt ra bất kỳ giới hạn nào với các chương trình khách.

Nền tảng của thiết kết API với REST

Cho đến nay, vẫn không có một định nghĩa tuyệt đối về cách xây dựng các API. Có những quan điểm trái ngược nhau trong vấn đề này. Một số người có thể không đồng ý khi chúng ta nói rằng định tuyến /translate được đề cập ở trên là một API. Một số khác có thể đồng ý nhưng cho rằng đây là các API được thiết kết tồi. Như vậy thì các API đưuợc thiết kế tốt có những đặc tính gì? Và tại sao các định tuyến trả về dữ liệu JSON không nằm trong phạm trù này?

Bạn có thể đã nghe về thuật ngữ REST API. REST (xuất phát từ thuật ngữ Representational State Transfer) là một kiến trúc do tiến sĩ Dr. Roy Fielding đề xướng trong luận án tiến sĩ của ông. Trong luận án này, tiến sĩ Fielding đã giới thiệu sáu đặc tính tổng quát định nghĩa về REST.

Ngoài luận án của tiến sĩ Fielding, không có đặc tả nào khác để chuẩn hóa REST, vì vậy, nhiều chi tiết về cách xây dựng REST được diễn dịch theo cách hiểu của đọc giả. Chủ đề thảo luận về việc một API có phải là REST hay không thường là một chủ đề nóng và gây tranh cãi giữa những người theo REST “thuần túy” – là những người cho rằng các REST API phải có đủ sáu đặc tính và tuân theo một quy trình riêng biệt – và những người theo trường phái “thực dụng” – là những người cho rằng các đề xuất trong luận án của tiến sĩ Fielding chỉ mang tính hướng dẫn hoặc đề nghị. Về phần mình, tiến sĩ Fielding đứng về phía những người theo REST thuần túy và cung cấp thêm một số chi tiết về quan điểm của mình trong các bài viết và bình luận trên mạng.

Phần lớn các REST API đã được xây dựng hiện nay đi theo hướng “thực dụng”, bao gồm cả các API từ những nhà phát triển lớn như Facebook, GitHub, Twitter, … Chỉ có một số rất ít các API được công nhận là thuần REST, bởi vì phần lớn các API không có một số chi tiết được xem như bắt buộc cho tiêu chuẩn thuần REST. Và bất kể quan điểm nghiêm ngặt của Tiến sĩ Fielding và cộng đồng REST “thuần túy”, quan điểm về REST của giới phát triển phần mềm nói chung thiên về tính thực dụng.

Để giúp cho bạn có một cái nhìn tổng quát trong luận án về REST của Tiến sĩ Fielding, chúng ta sẽ điểm qua sáu nguyên tắc được đưa ra trong luận án:

Chủ-Khách (Client-Server)

Nguyên tắc client-server tương đối rõ ràng. Nó chỉ ra rằng vai trò của client và server phải được phân biệt rõ ràng trong một REST API. Khi áp dụng, điều này có nghĩa là client và server phải ở trong những tiến trình (process) khác nhau và giao tiếp thông qua một kênh liên lạc – trong phần lớn trường hợp là giao thức HTTP qua mạng TCP.

Hệ thống phân lớp (Layered System )

Nguyên tắc hệ thống phân lớp định nghĩa rằng khi một client cần giao tiếp với một server, nó có thê kết nối với một đối tượng trung gian và giao tiếp với đối tượng này thay vì server thật sự. Trọng tâm ở đây là client không phân biệt giữa việc gởi yêu cầu đến server và một đối tượng trung gian, và thật ra, nó hoàn toàn không cần biết rằng nó đang kết nối với server thật hay không. Và ở chiều ngược lại, nguyên tắc này cũng đồng nghĩa với việc server có thể nhận các yêu cầu từ một đối tượng trung gian mà không phải trực tiếp từ client. Do đó, server không được giả định rằng nó đang được kết nối với client.

Bộ nhớ tạm (Cache)

Nguyên tắc này cho phép server hay các đối tượng trung gian lưu các hồi đáp cho các yêu cầu trùng lặp thường xuyên vào bộ nhớ tạm (cache) để rút ngắn thời gian phản hồi. Trong thực tế, phần lớn các bạn đã gặp qua mô hình cache này trong các trình duyệt. Lớp cache trong trình duyệt thường được dùng để tránh việc gởi lặp lại các yêu cầu đến cùng một file như là các file hình ảnh trên cùng một server.

Với các API, server cần phải chỉ định việc sử dụng chế độ điều khiển cache (cache control) nếu một hồi đáp có thể được cache bởi các đối tượng trung gian trước khi đến được client. Lưu ý rằng vì lý do bảo mật, việc triển khai các API trong môi trường sản phẩm phải được mã hóa. Do đó, việc sử dụng cache thường không được thực hiện ở các đối tượng trung gian, trừ khi các đối tượng này là điểm đầu cuối trong kết nối SSL hoặc thực hiện việc mã hóa và giải mã.

Mã theo yêu cầu (Code On Demand)

Đây là một tùy chọn định nghĩa rằng server có thể cung cấp các mã có thể thực thi được cho client. Bởi vì nguyên tắc này đòi hỏi server và client thỏa thuận về loại mã nào có thể thực thi được bởi client, nó it khi được sử dụng trong các API. Bạn có thể cho rằng server có thể trả về mã JavaScript để client là trình duyệt có thể thi hành, nhưng REST không chỉ nhằm vào các client là trình duyệt. Vì vậy, việc thực thi JavaScript có thể gây ra một số rắc rối nếu client là các thiết bị iOS hoặc Android.

Không có trạng thái (Stateless)

Nguyên tắc không có trạng thái là một trong hai nguyên tắc gây tranh cãi nhiều nhất nhất giữa cộng đồng REST thuần túy và thực dụng. Nó định nghĩa rằng một API REST không nên lưu bất kỳ trạng thái nào của client để có thể được gọi lại mỗi khi client gởi một yêu cầu. Điều này có nghĩa là các kỹ thuật thông dụng trong việc phát triển các ứng dụng Web để “ghi nhớ” user khi họ thăm các trang khác nhau trong một phiên làm việc đều bất hợp lệ theo nguyên tắc này. Trong các API không có trạng thái, mỗi một yêu cầu đều độc lập với nhau và phải kèm theo các thông tin cho phép server định danh, xác thực user và hoàn tất công việc. Diều này cũng có nghĩa là server không thể lưu trữ bất kỳ dữ liệu nào liên quan đến kết nối từ client trong cơ sở dữ liệu hoặc các hình thức lưu trữ khác.

Nếu bạn thắc mắc tại sao REST yêu cầu server phải là không có trạng thái, lý do chính là vì các server không trạng thái có thể được mở rộng để phục vụ nhiều client (scale) dễ dàng hơn. Để thực hiện việc mở rộng các server không trạng thái, bạn chỉ cần thực thi nhiều server cùng loại với một máy chủ cân bẳng tải (load balancer) để phân phối các yêu cầu đến từng server. Nếu các server có lưu trạng thái của client, công việc này sẽ trở nên rất phức tạp vì bạn sẽ phải tìm cách để nhiều server có thể truy xuất và cập nhật các trạng thái này, hoặc phải bảo đảm một client sẽ luôn được xử lý bởi cùng một server – một quá trình thường được biết với thuật ngữ “phiên làm việc dính chặt” (sticky session).

Nếu bạn xem xét lại định tuyến /translate mà chúng ta đã đề cập ở phần trên, bạn sẽ thấy rằng nó không hợp với tiêu chuẩn của REST bởi vì hàm hiển thị của định tuyến này có sử dụng decorator @login_required của thư viện Flask-Login, và decorator này lại sử dụng đến các thông tin được lưu trữ liên quan đến trạng thái đăng nhập của user trong một phiên làm việc của Flask.

Giao diện đồng nhất (Uniform Interface)

Nguyên tắc cuối cùng, cũng là nguyên tắc gây tranh cãi và nhập nhằng nhất trong các tài liệu về REST là nguyên tắc giao diện đồng nhất. Tiến sĩ Fielding liệt kê bốn khía cạnh phân biệt của nguyên tắc này là: định danh tài nguyên duy nhất (unique resource identifiers), đặc tả tài nguyên (resource representations), các thông điệp tự mô tả (self-descriptive) và các siêu phương tiện (hypermedia).

Định danh tài nguyên duy nhất được thực hiện bằng cách gán các URL duy nhất cho mỗi tài nguyên. Ví dụ như một URL liên kết với một user có thể là /api/users/<user-id> với <user-id> là định danh được gán cho user đó trong khóa chính của bản tương ứng trong cơ sở dữ liệu. Điều này được xây dựng trong hầu hết các API.

Việc sử dụng các đặc tả tài nguyên có nghĩa là khi server và client trao đổi thông tin về một tài nguyên, chúng phải sử dụng một định dạng chung được chấp nhận từ cả hai phía. Với hầu hết các API hiện đại, định dạng JSON được dùng cho mục đích này. Một API cũng có thể hỗ trợ nhiều đặc tả tài nguyên khác nhau cùng lúc, và trong những trường hợp như vậy, các tùy chọn để thỏa thuận về nội dung (content negotiation) trong giao thức HTTP sẽ được server và client sử dụng để thống nhất về định dạng của dữ liệu được trao đổi giữa hai bên.

Các thông điệp tự mô tả có nghĩa là các yêu cầu và hồi đáp giữa client và server phải có tất cả các thông tin mà đối tác cần. Một ví dụ điển hình là phương thức yêu cầu HTTP được dùng để chỉ định loại tác vụ mà client muốn server thực thi. Một yêu cầu GET chỉ ra rằng client muốn lấy thông tin về một tài nguyên, một yêu cầu POST chỉ ra rằng client muốn tạo ra một tài nguyên mới, PUT hoặc PATCH định nghĩa các yêu cầu để cập nhật các tài nguyên có sẵn, và DELETE được dùng để yêu cầu xóa bỏ một tài nguyên nào đó. Tài nguyên đích sẽ được chỉ định trong URL của yêu cầu và với các thông tin bổ sung nằm trong đầu mục của yêu cầu HTTP, một phần trong danh sách tham số trong URL (query string) hoặc trong phần thân của yêu cầu HTTP.

Tiêu chuẩn về siêu phương tiện là tiêu chuẩn gây ra nhiều tranh cãi nhất và chỉ được một số ít các API tuân theo, và ngay cả các API tuân theo đặc tả này cũng không được xây dựng theo cách thức làm thỏa lòng cộng đồng “thuần” REST. Bởi vì các tài nguyên trong một ứng dụng đều liên quan với nhau, tiêu chuẩn này yêu cầu các mối liên hệ này phải được đưa vào trong các đặc tả tài nguyên để các client có thể tự tìm ra các tài nguyên mới bằng cách lần theo các mối quan hệ này, giống như cách bạn tìm ra các trang mới trong một ứng dụng web bằng cách bấm vào các liên kết (link) tương ứng. Trọng tâm của tiêu chuẩn này là cho phép một client sử dụng một API mà không biết trước về các tài nguyên được API này sử dụng, nhưng có thể tìm ra các tài nguyên này bằng cách lần theo các siêu liên kết. Tuy nhiên, tiêu chuẩn này lại gặp rắc rối khi xây dựng vì định dạng JSON vốn rất phổ biến để dùng cho mục đích đặc tả tài nguyên trong các API lại không có cách chuẩn để khai báo các liên kết như HTML hoặc XML. Vì vậy, bạn buộc phải dùng một cấu trúc tùy biến hoặc là một trong các chuẩn mở rộng của JSON để lấp đầy khoảng trống này như là JSON-API, HAL, JSON-LD hoặc các chuẩn tương tự.

Xây dựng một Blueprint (bản thiết kế) cho API

Để giúp bạn có một khái niệm về việc phát triển các API, chúng ta sẽ thêm một API vào ứng dụng Myblog. API này sẽ không phải là một API hoàn chỉnh, chúng ta sẽ xây dựng tất cả các chức năng liên quan đến người sử dụng nhưng chừa lại các chức năng liên quan đến các tài nguyên khác như là bài viết để bạn có thể thực hiện như là một bài tập cho phần này.

Để tuân thủ theo cấu trúc của ứng dụng mà chúng ta đã đưa ra trong Phần 15, chúng ta sẽ tạo ra một blueprint có chứa tất cả định tuyến của API. Chúng ta sẽ bắt đầu với việc tạo ra một thư mục cho blueprint mới:

File __init__.py sẽ tạo ra đối tượng tương ứng cho blueprint tương tự như các blueprint khác trong ứng dụng:

app/__init__.py: Constructor cho blueprint của API.

Chắc bạn còn nhớ rằng có những khi chúng ta cần đưa mệnh đề tham chiếu đến cuối của file để tránh lỗi tham chiếu vòng. Đây là lý do tại sao các module app/api/users.py, app/api/errors.pyapp/api/tokens.py được tham chiếu sau khi blueprint được tạo ra.

Phần chính của API sẽ nằm trong module app/api/users.py. Toàn bộ các định tuyến mới mà chúng ta sẽ xây dựng được định nghĩa trong bảng dưới đây:

Phương thức HTTPURLGhi chú
GET/api/users/<id>Trả vể thông tin của một user.
GET/api/usersTrả về danh sách của một nhóm user.
GET/api/users/<id>/followersTrả về danh sách của các follower của một user.
GET/api/users/<id>/followedTrả về danh sách các user được một user theo dõi.
POST/api/usersĐăng ký một tài khoản mới cho user
PUT/api/users/<id>Cập nhật thông tin của một user.

Tạm thời, chúng ta sẽ tạo ra một bộ khung cơ bản cho module với các định tuyến này như sau:

app/api/users.py: Module cho API về user.

Module app/api/errors.py sẽ định nghĩa một vài hàm trợ giúp để xử lý và trả về các thông báo lỗi. Nhưng tạm thời, chúng ta sẽ chỉ tạo ra định nghĩa hàm và hoàn tất phần còn lại sau này.

app/api/errors.py: Xử lý lỗi.

Hệ thống xác thực người dùng sẽ được định nghĩa trong module app/api/tokens.py. Hệ thống này sẽ được dùng để xác thực các client không phải là trình duyệt. Cũng như các module khác, chúng ta cũng chỉ dừng lại ở mức độ định nghĩa cho module này:

app/api/tokens.py: Xử lý các token.

Chúng ta cũng cần đăng ký blueprint cho API với hàm tạo ứng dụng:

app/__init__.py: Đăng ký blueprint API với ứng dụng.

Đặc tả định dạng JSON cho dữ liệu user

Việc đầu tiên chúng ta phải làm khi xây dựng một API là quy định các đặc tả về tài nguyên được hệ thống sử dụng. Vì chúng ta sẽ xây dựng một API cho các dữ liệu về user, chúng ta sẽ cần quyết định đặc tả cho các thông tin về user. Chúng ta sẽ dùng định dạng JSON sau đây cho mục đích này:

Đa số các trường trong đối tượng JSON được định nghĩa ở trên được lấy trực tiếp từ mô hình dữ liệu về user với một số trường hợp đặc biệt cần lưu ý. Trường password tương đối đặc biệt vì nó chỉ được sử dụng khi một user mới vừa đăng ký. Trong Phần 5, cơ sở dữ liệu không lưu trữ mật mã của user mà chỉ lưu mã băm (hash), vì vậy mật mã sẽ không bao giờ được trả về. Trường hợp đặc biệt thứ hai là trường email bởi vì chúng ta không muốn làm lộ địa chỉ email của người sử dụng. Trường này sẽ chỉ được trả về khi user yêu cầu thông tin về chính họ, và sẽ không được trả về nếu họ yêu cầu thông tin về user khác. Các trường post_count, follower_count và followed_count là các trường “ảo” và không tồn tại trong cơ sở dữ liệu nhưng sẽ được cung cấp để tăng thêm sự tiện lợi cho client. Đây là một ví dụ rất hay để minh họa cho các trường hợp đặc tả dữ liệu không cần trùng khớp với các tài nguyên thật được định nghĩa tại server.

Bạn cũng cần lưu ý đến phần định nghĩa _links để đáp ứng tiêu chuẩn về siêu phương tiện. Các liên kết được định nghĩa trong phần này gồm có liên kết đến tài nguyên hiện tại, danh sách các user đang theo dõi user hiện tại, danh sách các user đang được user hiện tại theo dõi, và cuối cùng là liên kết đến ảnh đại diện của user. Trong tương lai, nếu chúng ta quyết định đưa thêm bài viết vào APi này, chúng ta cũng sẽ thêm danh sách các bài viết của user hiện tại vào phần này.

Một lợi thế khi sử dụng định dạng JSON với Python là nó luôn được chuyển đổi thành dữ liệu dạng từ điển hay danh sách trong Python. Một thư viện chuẩn của Python là json sẽ đảm nhiệm việc chuyển đổi giữa dữ liệu dạng JSON và cấu trúc dữ liệu tương ứng trong Python. Vì vậy, để tạo ra cấu trúc dữ liệu trong Python,  chúng ta sẽ thêm một phương thức mới gọi là to_dict() vào mô hình dữ liệu User để trả về một từ điển Python:

app/models.py: Đặc tả cho mô hình dữ liệu User

Phương thức mới trong đoạn mã trên tương đối rõ ràng: chúng ta đưa các giá trị cần thiết vào các trường tương ứng trong một từ điển và trả về từ điển đó. Như đã đề cập ở trên, giá trị của trường email cần được xử lý riêng vì chúng ta chỉ muốn trả về địa chỉ email khi user yêu cầu dữ liệu của chính họ. Vì vậy, chúng ta sử dụng tham số include_email để quyết định xem trường này có được đưa vào dữ liệu trả về hay không.

Bạn cũng nên lưu ý cách tạo ra trường last_seen ở trên. Đối với các trường có giá trị là thời gian, chúng ta sẽ sử dụng hàm isoformat() thuộc đối tượng datetime của Python để định dạng giá trị theo chuẩn ISO 8601. Nhưng bởi vì chúng ta dùng các đối tượng datetime không có kèm theo các thông tin về múi giờ khi lưu lại các giá trị thời gian này, chúng ta cần thêm ký tự Z – cũng là mã tương ứng với múi giờ UTC theo chuẩn ISO 8601 – vào cuối chuỗi giá trị thời gian.

Và cuối cùng, bạn nên quan sát cách thực hiện các liên kết siêu phương tiện. Chúng ta sử dụng hàm url_for() để tạo ra các địa chỉ (URL) cho ba liên kết đến các định tuyến khác trong ứng dụng (ba liên kết này dẫn đến các hàm hiển thị mà chúng ta đã định nghĩa vắn tắt trong moudle app/api/users.py). Liên kết đến ảnh đại diện đặc biệt hơn một chút bởi vì nó là một địa chỉ bên ngoài ứng dụng thuộc về Gravatar. Đối với liên kết này, chúng ta sử dụng cùng một phương thức avatar() mà chúng ta đã dùng trước đây để hiển thị ảnh đại diện trong trang Web.

Phương thức to_dict() sẽ chuyển đổi một đối tượng User thành một cấu trúc dữ liệu của Python và cuối cúng được đưa về định dạng JSON. Chúng ta cũng cần thực hiện theo chiều ngược lại: phân tích dữ liệu nhận được từ client và đổi nó thành một đối tượng User. Sau đây là phương thức from_dict() để chuyển đổi từ một từ điển Python thành một mô hình dữ liệu:

app/models.py: Đặc tả mô hình dữ liệu User.

Trong trường hợp này chúng ta dùng một vòng lặp để nhập giá trị của các trường được client gởi đến là username, email và about_me. Đối với mỗi trường này, chúng ta sẽ kiểm tra xem có giá trị tương ứng trong tham số data hay không. Và nếu có, chúng ta sẽ dùng hàm setattr() của Python để gán giá trị nhận được vào thuộc tính tương ứng của đối tượng.

Một lần nữa, trường password được xử lý đặc biệt bời vì nó không phải là một trường trong đối tượng.  Tham số new_user sẽ cho biết đây có phải là trường hợp đăng ký một user mới hay không, đồng nghĩa với việc dữ liệu nhập sẽ có giá trị cho password hay không. Để thiết lập mật mã cho một mô hình dữ liệu user, chúng ta gọi hàm set_password() để tạo ra mã băm cho mật mã.

Đặc tả một nhóm các User

Ngoài việc đặc tả dữ liệu cho một tài nguyên đơn (trong trường hợp này là dữ liệu của một user), API của chúng ta cũng cần đặc tả dữ liệu cho một nhóm user. Đây là định dạng sẽ được sử dụng khi client yêu cầu thông tin về một danh sách các user hay người theo dõi (follower). Sau đây là đặc tả cho một nhóm các user:

Trong đặc tả này, items là danh sách các user, mỗi một phần tử trong danh sách này chứa dữ liệu của một user theo định dạng đã được thiết lập ở phần trên. Tiếp theo, mục _meta cung cấp các thông tin về dữ liệu (metadata) để client có thể sử dụng khi hiển thị thông tin theo trang nếu cần thiết.  Mục _links định nghĩa các liên kết có liên quan, bao gồm một liên kết đến chính tập hợp và liên kết đến các trang trước và trang sau để hỗ trợ client có thể hiển thị thông tin theo trang.

Logic của việc phân trang làm cho quá trình xây dựng đặc tả cho nhóm user trở nên phức tạp hơn cần thiết. Tuy nhiên, logic này cũng sẽ được sử dụng chung cho các tài nguyên khác mà chúng ta có thể đưa vào API trong tương này. Vì vậy, chúng ta sẽ xây dựng đặc tả theo cách tổng quát để có thể áp dụng cho các mô hình khác. Trong Phần 16, chúng ta cũng đã gặp tình huống tương tự với các chỉ mục tìm kiếm văn bản khi muốn tạo ra một giải pháp tổng quát để dùng cho nhiều mô hình dữ liệu khác nhau. Khi đó, cách giải quyết của chúng ta là tạo ra lớp SearchableMixin để bất kỳ một mô hình dữ liệu nào cần sử dụng chỉ mục tìm kiếm văn bản có thể kế thừa từ đó. Chúng ta cũng sẽ sử dụng kỹ thuật tương tự ở đây và tạo ra một lớp mixin mới gọi là PaginatedAPIMixin:

app/models.py: Lớp mixin để đặc tả dữ liệu phân trang.

Phương thức to_collection_dict() tạo ra một từ điển với dữ liệu về một nhóm các user bao gồm các mục items, _meta và _links. Bạn cần xem kỹ phương thức này để hiểu cách làm việc của nó. Ba tham số đầu tiên bao gồm một đối tượng truy vấn Flask-SQLAlchemy, số thứ tự của trang và kích thước trang. Đây là những tham số quyết định có bao nhiêu dữ liệu sẽ được trả về. Phương thức này sử dụng phương thức paginate() từ đối tượng truy vấn để nhận dữ liệu của các user trong một trang, tương tự như cách chúng ta đã làm với các bài viết trong trang chủ, trang explore và hồ sơ cá nhân trong ứng dụng Web.

Công việc phức tạp nhất ở đây là tạo ra các liên kết bao gồm một liên kết đến chính trang hiện tại và các liên kết đến các trang trước và sau nó. Bởi vì chúng ta muốn tạo ra một hàm mang tính tổng quát, chúng ta không thể sử dụng các hàm cụ thể như url_for(‘api.get_users’, id=id, page=page) để tạo ra một liên kết đến chính nó. Các tham số cho url_for() sẽ phụ thuộc vào từng nhóm tài nguyên riêng biệt, vì vậy, chúng ta sẽ cần dựa vào tham số endpoint do client cung cấp với giá trị là hàm hiển thị cần được hàm url_for() sử dụng. Và bởi vì nhiều định tuyến cần có các tham số kèm theo, chúng ta phải ghi nhận các tham số này từ các tham số từ khóa trong kwargs và truyền chúng cho hàm url_for(). Các tham số page và per_page được truyền trực tiếp bởi vì đây là các tham số chung cho toàn bộ các định tuyến trong API.

Chúng ta cần cập nhật lớp User để kế thừa từ lớp mixin mới này:

app/models.py: Thêm PaginatedAPIMixin vào lớp User.

Đối với nhóm các user, chúng ta không cần làm theo hướng ngược lại vì chúng ta sẽ không có URL nào để client gởi một danh sách các user đến server.

Xử lý lỗi

Các trang thông báo lỗi chúng ta đã tạo ra trong Phần 17 chỉ thích hợp cho các tương tác giữa user và ứng dụng thông qua trình duyệt. Bởi vì đối tượng sử dụng API thường các ứng dụng, các thông báo lỗi do API trả về cho client cần phải được định nghĩa theo cách “thân thiện với máy” hơn là với người sử dụng bình thường. Điều này cho phép các ứng dụng có thể dễ dàng phân tích các thông điệp lỗi. Vì vậy, chúng ta sẽ đặc tả định dạng của các thông báo lỗi cho API cũng tương tự như các đặc tả cho tài nguyên API bằng JSON. Sau đây là cấu trúc cơ bản mà chúng ta sẽ sử dụng:

Ngoài dữ liệu về lỗi, chúng ta cũng sẽ dùng mã trạng thái (status code) từ giao thức HTTP để báo hiệu loại lỗi. Để tạo ra các thông báo lỗi, chúng ta sẽ xây dựng hàm error_response() trong app/api/errors.py:

app/api/errors.py: Hồi đáp khi phát sinh lỗi.

Hàm này sử dụng từ điển HTTP_STATUS_CODE từ thư viện Werkzeug (một thư viện chính Flask) có chứa các tên ngắn cho mỗi mã trạng thái HTTP. Chúng ta sẽ sử dụng các tên này trong trường error trong đặc tả về lỗi của chúng ta, nhờ đó chúng ta chúng ta có thể tập trung vào mã trạng thái bằng số và tùy chọn mô tả đầy đủ về lỗi. Hàm jsonify() trả về một đối tượng Response của Flask với mã trạng thái mặc định là 200. Sau khi hồi đáp được tạo lập, chúng ta sẽ gán mã trạng thái thích hợp với lỗi phát sinh.

Lỗi phổ biến nhất thường xảy ra khi gọi các API là lỗi do các yêu cầu bất hợp lệ (bad request) và có mã trạng thái được trả về là 400. Đây là lỗi phát sinh khi client gởi một yêu cầu với dữ liệu không hợp lệ. Để giúp cho việc sinh ra mã lỗi này dễ dàng hơn, chúng ta sẽ thêm một hàm dành riêng cho nó và chỉ cần một tham số là một thông báo lỗi đầy đủ. Sau đây là phiên bản đầy đủ của hàm bad_request() mà chúng ta đã giới thiệu ở phần trên:

app/api/errors.py: Hồi đáp cho các yêu cầu không hợp lệ.

Các Endpoint (điểm cuối) cho tài nguyên về User

Đến đây, chúng ta đã chuẩn bị mọi thứ cần thiết để làm việc với đặc tả JSON cho tài nguyên về user và có thể bắt tay vào việc tạo ra các endpoint cho API (Endpoint thực chất là các URL để client có thể truy cập các tài nguyên ở server thông qua API. Nếu cần tìm hiểu về endpoint, bạn có thể tham khảo tại các trang tài liệu trực tuyến).

Lấy thông tin của một User

Chúng ta hãy bắt đầu với yêu cầu để lấy thông tin về một user với id của user đó:

app/api/users.py: Trả về các thông tin của một user.

Hàm hiển thị trên sẽ nhận id của user được yêu cầu từ danh sách các tham số trong URL. Phương thức get_or_404() của đối tượng truy vấn là một biến thể rất hữu ích của phương thức get() mà bạn đã gặp trước đây. Nó cũng sẽ trả về một đối tượng với id được chỉ định nếu đối tượng đó tồn tại, nhưng thay vì trả về None khi id không tồn tại, not sẽ hủy bỏ yêu cầu và trả về lỗi 404 cho client. Lợi thế của get_or_404() so với get() là nó giúp chúng ta tránh được việc kiểm tra kết quả truy vấn và nhờ đó đơn giản hóa logic của hàm hiển thị.

Phương thức to_dict() mà chúng ta đã thêm vào lớp User được gọi để tạo ra một từ điển với đặc tả tài nguyên cho user được yêu cầu, sau đó hàm jsonify() của Flask sẽ chuyển đổi từ điển này thành định dạng JSON để trả về cho client.

Nếu muốn thấy API đầu tiên này hoạt động như thế nào, bạn có thể khởi động ứng dụng và nhập vào URL sau trong trình duyệt:

Sau đó bạn sẽ nhận được thông tin về user đầu tiên theo định dạng JSON. Bạn cũng có thể thử nhập một giá trị id lớn để xem cách kích hoạt lỗi 404 từ phương thức get_or_404() của đối tượng truy vấn SQLAlchemy (Trong phần sau, chúng ta sẽ tìm hiểu cách để tùy biến mã xử lý lỗi để trả về các lỗi này cũng trong định dạng JSON).

Để kiểm tra định tuyến mới này, chúng ta sẽ cài đặt một chương trình HTTPie. Đây là một chương trình được viết bằng Python để giúp chúng ta gởi các yêu cầu API dễ dàng hơn.

Từ bây giờ, chúng ta có thể gởi yêu cầu để nhận dữ liệu về user với id là 1 bằng lệnh sau đây:

Lấy thông tin của một nhóm User

Để trả về thông tin của một nhóm user, chúng ta có thể dùng phương thức to_collection_dict() từ PaginatedAPIMixin:

app/api/users.py: Trả về thông tin của một nhóm user.

Trong hàm get_users() ở trên, đầu tiên chúng ta sẽ lấy giá trị của các tham số page và per_page từ danh sách các tham số trong URL (query string), với các giá trị mặc định là 1 và 10 nếu không tìm thấy các tham số này. Tham số per_page có thêm một điều kiện phụ để bảo đảm rằng giá trị tối đa của nó không quá 100. Chúng ta không nên cho phép các yêu cầu với số dữ liệu quá lớn trong mỗi trang vì chúng có thể ảnh hưởng đến tốc độ thực thi của server. Tiếp theo, các tham số page và per_page sẽ được truyền cho phương thức to_collection_query() cùng với truy vấn, và trong trường hợp này chỉ là một truy vấn đơn giản là User.query – một truy vấn tổng quát để trả về tất cả user. Tham số cuối cùng là api.get_users cũng là tên của endpoint chúng ta cần dùng trong ba liên kết trong dữ liệu được trả về.

Để kiểm tra endpoint này với HTTPie, bạn hãy dùng lệnh sau:

Hai endpoint tiếp theo sẽ trả về danh sách người theo dõi và người được theo dõi. Các endpoint này cũng hoàn toàn tương tự như endpoint trên:

app/api/users.py: Trả về danh sách các user theo dõi và được theo dõi.

Bởi vì các định tuyến này là riêng biệt với mỗi user khác nhau, chúng sẽ cần đến tham số động id của user. Giá trị của id được dùng để lấy dữ liệu của user từ cơ sở dữ liệu và sau đó cung cấp các truy vấn quan hệ user.followers và user.followed cho phương thức to_collection_dict(). Đến đây, bạn có thể thấy tại sao chúng ta phải tốn công để thiết kế các phương thức mang tính tổng quát và làm việc được với nhiều loại dữ liệu khác nhau. Hai tham số cuối của to_collection_dict() lần lượt là tên của endpoint và id. Riêng tham số id sẽ được đưa vào danh sách các tham số theo từ khóa và được truy cập thông qua tham số kwargs trong phương thức to_collection_dict(), và sau đó sẽ được đưa vào url_for() trong quá trình tạo ra các liên kết trong mục _links trong đặc tả dữ liệu.

Tương tự như ví dụ trước đây, bạn có thể kiểm tra hai định tuyến mới này với HTTPie như sau:

Và nhờ có siêu phương tiện, chúng ta không cần nhớ các URL này vì chúng đã được đưa vào mục _links trong dữ liệu được server trả về.

Đăng ký user mới

Yêu cầu dạng POST với định tuyến /users sẽ được dùng để đăng ký user mới. Sau đây là mã nguồn cho định tuyến này:

app/api/users.py: Đăng ký một user.

Yêu cầu này được gởi đến server với dữ liệu về user theo định dạng JSON. Flask có sẵn phương thức request.get_json() để lấy dữ liệu dạng JSON có trong yêu cầu và trả về một cấu trúc dữ liệu Python. Phương thức này sẽ trả về None nếu yêu cầu không có dữ liệu JSON kèm theo, vì vậy chúng ta dùng biểu thức request.get_json() or {} để bảo đảm rằng chúng ta luôn có một từ điển dữ liệu.

Trước khi sử dụng dữ liệu, chúng ta cần phải bảo đảm rằng chúng ta có tất cả các thông tin cần thiết. Vì vậy, chúng ta phải kiểm tra cả ba trường bắt buộc là username, email và password đều có trong dữ liệu đã được gởi đến server. Nếu không tìm được một trong ba trường này, chúng ta sẽ dùng hàm trợ giúp bad_request() từ module app/api/errors.py để gởi một thông báo lỗi đến client. Ngoài ra, chúng ta cũng cần phải xác định rằng giá trị của các trường username và email chưa được sử dụng bởi bất kỳ user nào. Để kiểm tra điều này, chúng ta sẽ thử tìm một user trong cơ sở dữ liệu dựa vào username và email được gởi đến. Và nếu tìm được, chúng ta sẽ thông báo lỗi đến client.

Sau khi đã kiểm tra dữ liệu nhập, chúng ta có thể tạo ra một đối tượng user mới và lưu vào cơ sở dữ liệu. Chúng ta sẽ sử dụng phương thức from_dict() trong lớp User để lấy các thông tin về user đã được client gởi đến. Tham số new_user sẽ được thiết lập là True để sử dụng trường password vốn không được sử dụng đối với các đặc tả dữ liệu về user trong các trường hợp thông thường.

Hồi đáp cho yêu cầu này là đặc tả dữ liệu cho user mới, vì vậy chúng ta sử dụng phương thức to_dict() để tạo ra dữ liệu sẽ được trả về cho client. Mã trạng thái cho một yêu cầu POST để tạo ra một tài nguyên mới phải là 201 – mã này được dùng cho các trường hợp khi một thực thể mới được tạo ra thành công. Ngoài ra, giao thức HTTP cũng yêu cầu mã trạng thái 201 phải đi kèm với trường Location trong phần mở đầu (header) với giá trị là URL của tài nguyên mới.

Sau đây là cách tạo ra một user mới từ dòng lệnh với HTTPie:

Cập nhật thông tin về user

Endpoint cuối cùng trong API là endpoint để cập nhật thông tin của các user:

app/api/users.py: Cập nhật một user.

URL cho yêu cầu này sẽ có một thành phần thay đổi là id của user. Id này sẽ được dùng để truy cập các thông tin về người sử dụng từ cơ sở dữ liệu và trả về lỗi 404 nếu không tìm được. Cần lưu ý rằng bởi vì hiện vẫn chưa có phương thức xác thực người dùng, một user có thể cập nhật thông tin của các user khác. Chúng ta sẽ hiệu chỉnh điều này sau khi xây dựng thành phần xác thực người dùng.

Cũng như trường hợp đăng ký user mới, chúng ta cần kiểm tra dữ liệu của các trường username và email do client gởi đến để đảm bảo rằng các dữ liệu này không trùng với dữ liệu của các user khác. Tuy nhiên trường hợp này phức tạp hơn vì các lý do sau: Thứ nhất, các trường này là tùy chọn trong yêu cầu này, vì vậy chúng ta cần kiểm tra xem dữ liệu của một trường có trong yêu cầu đã được gởi đến hay không. Thứ nhì là việc client có thể gởi đến cùng một giá trị email hoặc username đang được sử dụng, vì vậy trước khi chúng ta kiểm tra xem username hoặc email đã được sử dụng bởi các user khác hay chưa,  chúng ta cần xác định rằng chúng không trùng lặp với các giá trị đang được user hiện hành sử dụng. Nếu một trong các kiểm tra này không thành công, chúng ta sẽ trả về lỗi 400 cho client như trước đây.

Sau khi dữ liệu đã được kiểm tra thành công, chúng ta có thể sử dụng phương thức from_dict() trong lớp User để tham chiếu đến dữ liệu được client gởi đến và lưu vào cơ sở dữ liệu. Sau đó, chúng ta sẽ gởi hồi đáp cho client với đặc tả dữ liệu về user và mã trạng thái 200 theo mặc định. Sau đây là ví dụ về cập nhật trường about_me với HTTPie:

Xác thực user trong API

Hiện giờ các endpoint chúng ta vừa tạo ra trong phần trên có thể được sử dụng bởi bất kỳ client nào. Rõ ràng là vấn đề này cần được giải quyết vì chỉ có các user đã đăng ký mới có quyền sử dụng các endpoint này.  Vì vậy chúng ta cần phải thêm cơ chế xác thực (authentication) và xác quyền (authorization) vào ứng dụng. Mục đích của chúng ta là xác nhận user là ai qua các yêu cầu của họ và đồng thời kiểm tra xem các yêu cầu của một user nào đó có được phép hay không.

Cách bảo vệ các endpoint rõ ràng nhất là sử dụng decorator @login_required của Flask-Login, nhưng phương pháp này có một số vấn đề. Khi decorator này không xác thực được một user, nó sẽ đưa user đến một trang HTML để đăng nhập. API không thể sử dụng cơ chế này vì API không sử dụng HTML hoặc trang đăng nhập. Với API, khi một client gởi một yêu cầu mà không có các thông tin hoặc có thông tin bất hợp lệ về tài khoản user, server sẽ từ chối yêu cầu này và trả về mã trạng thái 401. Server không thể giả định client cho API là một trình duyệt, hoặc có thể xử lý các chỉ thị để tái định hướng user (redirect), hoặc có thể hiển thị và xử lý các form đăng nhập dạng HTML. Khi một client sử dụng API nhận được mã trạng thái 401 từ server, nó sẽ biết cần phải yêu cầu user cung cấp thông tin cho quá trình xác thực, nhưng cách thức thực hiện quá trình xác thực không phải là công việc của server.

Token trong mô hình dữ liệu User

Để đáp ứng nhu cầu bảo mật, chúng ta sẽ sử dụng mô hình bảo mật với token. Khi một client muốn tương tác với API, nó cần phải yêu cầu một token tạm thời. Yêu cầu này sẽ được xác thực qua tên người sử dụng và mật mã. Sau khi đã được xác thực, client có thể gởi token đã được server cung cấp kèm theo các yêu cầu API để được xác thực trong khi token còn hiệu lực. khi token hết hạn, client cần yêu cầu một token khác. Để hỗ trợ cho cơ chế bảo mật với token, chúng ta sẽ mở rộng mô hình dữ liệu User như sau:

app/models.py: Hỗ trợ cho token của user.

Với thay đổi này, chúng ta thêm thuộc tính token vào mô hình user, và bởi vì chúng ta cần tìm các giá trị này trong cơ sở dữ liệu, chúng ta sẽ đánh dấu thuộc tính này là duy nhất (unique) và cần được lập chỉ mục (index). Chúng ta cũng thêm vào thuộc tính token_expiration để lưu thời gian token hết hiệu lực. Điều này đảm bảo rằng token sẽ không tồn tại quá lâu và trở thành một nguy cơ bảo mật.

Chúng ta tạo ra ba phương thức để sử dụng các token. Phương thức get_token() trả về một token cho user. Token được tạo ra sẽ mang giá trị là một chuỗi ngẫu nhiên và được mã hóa theo chuẩn base64. Trước khi tạo ra một token, phương thức này sẽ kiểm tra nếu token đang được gán cho user hiện hành còn hạn sử dụng dài hơn một phút, và trả về token hiện hành nếu đúng là trường hợp này.

Khi sử dụng các token, chúng ta nên chuẩn bị sẵn phương án để có thể chấm dứt hiệu lực của các token ngay lập tức khi cần thay vì đợi cho đến khi các token hết hạn. Đây là một phương pháp tốt để bảo đảm tính bảo mật cho các ứng dụng nhưng thường bị bỏ quên. Phương thức revoke_token() sẽ làm cho token hiện hành của user không còn hiệu lực bằng một cách đơn giản là thiết lập thời gian hết hạn của token một giây trước thời điểm hiện tại.

Phương thức check_token() là một phương thức tĩnh có đầu vào là một token và trả về user sở hữu token đó. Nếu token được đưa vào là bất hợp lệ hoặc quá hạn sử dụng, phương thức này sẽ trả về None.

Bởi vì chúng ta đã cập nhật mô hình dữ liệu, chúng ta cần thực hiện các bước cần thiết để tạo ra kịch bản chuyển đổi và sau đó cập nhật cơ sở dữ liệu:

Yêu cầu token

Khi tạo API, bạn cần giả định rằng không phải mọi client đều kết nối với ứng dụng web bằng trình duyệt. Giá trị thật sự của API là ở chỗ các client như ứng dụng trên các thiết bị di động hoặc nggay cả cac ứng dụng web đơn trang (single page application) trên trình duyệt có thể truy cập các dịch vụ ở phía sau (backend). Khi các client chuyên biệt này cần truy cập API, việc đầu tiên chúng sẽ làm là yêu cầu một token, tương đương với việc đăng nhập qua form đăng nhập trong các ứng dụng web truyền thống.

Để đơn giản hóa các tương tác giữa client và server trong quá trình xác thực bằng token, chúng ta sẽ sử dụng một thư viện mở rộng của Flask gọi là Flask-HTTPAuth. Chúng ta có thể cài đặt thư viện này bằng pip:

Flask-HTTPAuth hỗ trợ một số cơ chế đăng nhập khác nhau và đều dễ sử dụng với API. Đầu tiên, chúng ta sẽ sử dụng cơ chế xác thực HTTP cơ bản (HTTP Basic Authentication). Với cơ chế này, client sẽ gởi các thông tin đăng nhập của user trong một thuộc tính tiêu chuẩn cho cơ chế xác thực nằm trong phần đầu của các yêu cầu HTTP (Authorization HTTP Header). Để tích hợp với Flask-HTTPAuth, ứng dụng cần có hai hàm chức năng: một dùng để kiểm tra tên và mật mã của người dùng và một dùng để gởi trả các thông báo lỗi nếu việc xác thực user không thành công. Các hàm chức năng này sẽ được đăng ký với Flask-HTTPAuth nhờ các decorator và sau đó sẽ được tự động thực thi bởi thư viện mở rộng này khi cần thiết trong quá trình xác thực. Sau đây là mã nguồn cho hai hàm này:

app/api/auth.py: Hỗ trợ cho cơ chế xác thực cơ bản.

Lớp HTTPBasicAuth tù Flask-HTTPAuth chứa mã thực thi cho quá trình xác thực cơ bản. Hai hàm chức năng mà chúng ta đã đề cập ở trên được đăng ký qua các decorator tương ứng là verify_password và error_handler.

Hàm xác minh sẽ nhận tên và mật mã của user do client gởi đến và trả về True nếu các thông tin này là hợp lệ và False nếu không. Để kiểm tra mật mã chúng ta sẽ sử dụng phương thức check_password() từ lớp User – cũng là phương thức được Flask-Login sử dụng để xác thực user trong ứng dụng web.  Chúng ta sẽ lưu user cần được xác thực vào g.current_user để có thể truy cập từ các hàm hiển thị cho API.

Hàm xử lý lỗi chỉ thực hiện một công việc đơn giản là trả về lỗi 401 được tạo ra nhờ hàm error_response() trong app/api/errors.py. Theo tiêu chuẩn của HTTP, lỗi 401 được định nghĩa là lỗi “Unauthorized” (truy cập trái phép). Khi nhận được lỗi này, các chương trình client biết rằng chúng cần gởi lại yêu cầu với thông tin đăng nhập hợp lệ.

Sau khi đã có mã hỗ trợ cho cơ chế xác thực cơ bản, chúng ta có thể thêm một định tuyến mới để client yêu cầu một token:

app/api/tokens.py: Tạo ra các token cho user.

Hàm hiển thị này sẽ sử dụng decorator @basic_auth.login_required từ thực thể HTTPBasicAuth để chỉ thị Flask-HTTPAuth thực hiện xác thực user (với các hàm xác thực chúng ta đã định nghĩa ở trên) và chỉ cho phép hàm này thực thi khi các thông tin về user đã nhận được là hợp lệ. Hàm này dựa vào phương thức get_token() từ mô hình dữ liệu user để tạo ra token. Tiếp theo, hàm sẽ lưu token và hạn sử dụng của token vào cơ sở dữ liệu.

Nếu bạn thử gởi một yêu cầu dạng POST đến định tuyến mới này, bạn sẽ thấy các thông tin như sau:

Hồi đáp HTTP trên bao gồm mã trạng thái 401 và thông báo lỗi được định nghĩa trong hàm basic_auth_error(). Và dưới đây là một yêu cầu cùng loại, nhưng có thêm các thông tin để xác thực user:

Bây giờ thì mã trạng thái là 200 đồng nghĩa với một yêu cầu đã được thực hiện thành công và dữ liệu trả về sẽ có một token mới cho user. Lưu ý rằng khi gởi yêu cầu này, bạn cần thay thết <username>:<password> bằng tên và mật mã của bạn. Tên và mật mã cần được ngăn bởi dấu chấm phẩy.

Bảo vệ các định tuyến API bằng Token

Như vậy các chương trình client đã có thể yêu cầu và nhận được token để sử dụng với các endpoint của API. Việc còn lại là thêm mã kiểm tra token vào các endpoint. Và thật tốt là Flask-HTTPAuth cũng có thể giúp chúng ta làm điều này. Chúng ta sẽ cần tạo ra thực thể thứ hai cho quá trình xác thực từ lớp HTTPTokenAuth và cung cấp các hàm callback cho quá trình xác thực:

app/api/auth.py: Hỗ trợ cho xác thực bằng token.

Khi sử dụng xác thực bằng token, Flask-HTTPAuth sẽ dùng hàm được khai báo với decorator verify_token. Ngoài ra, cách hoạt động của xác thực bằng token hoàn toàn giống như xác thực cơ bản. Hàm xác nhận token của chúng ta sẽ sử dụng phương thức User.check_token() để tìm ra user sở hữu token đã nhận được. Nếu không có token, hàm sẽ thiết lập user hiện hành là None. Giá trị trả về True hoặc False của hàm sẽ quyết định Flask-HTTPAuth có cho phép thực thi hàm hiển thị hay không.

Để bảo vệ các định tuyến API với các token, chúng ta cần thêm decorator @token_auth.login_required vào các định tuyến này như sau:

app/api/users.py: Bảo vệ các định tuyến với cơ chế xác thực bằng token.

Lưu ý rằng chúng ta đã thêm decorator trên vào tất cả các hàm hiển thị API ngoại trừ create_user() vì hiển nhiên là user không thể có token trước khi có tài khoản! Ngoài ra, cũng bạn cũng nên lưu ý về điều kiện để ngăn trường hợp một user cập nhật thông tin của một user khác trong yêu cầu PUT để cập nhật thông tin về user. Nếu id của user đang yêu cầu cập nhật khác với id của user được cập nhật, chúng ta sẽ trả về lỗi 403 để chỉ ra rằng client không được phép thi hành tác vụ đã yêu cầu.

Đến đây, nếu bạn gởi một yêu cầu đến một trong các endpoint như trước đây, bạn sẽ nhận được một hồi đáp với lỗi 401. Để có thể truy cập, bạn cần thêm thuộc tính Authorization vào phần mở đầu của dữ liệu HTTP với giá trị là token nhận được từ yêu cầu đến địa chỉ /api/tokens. Flask-HTTPAuth quy ước rằng token được sử dụng sẽ là một “bearer” token, tuy nhiên HTTPie lại không hỗ trợ trực tiếp cho các token loại này dù rằng nó có hỗ trợ cho cơ chế xác thực cơ bản bằng tên và mật mã với tùy chọn –auth. Vì vậy, để sử dụng token với HTTPie, chúng ta phải chỉ định phần mở đầu của yêu cầu HTTP với giá trị của token như sau:

Hủy bỏ các token

Chức năng cuối cùng liên quan đến token là chức năng dùng để hủy bỏ hay chấm dứt hiệu lực của các token như sau:

app/api/tokens.py: Hủy bỏ các token.

Clien có thể gởi một yêu cầu dạng DELETE đến địa chỉ /tokens để hủy bỏ hiệu lực của token. Nhưng ngay cả địa chỉ này cũng sử dụng xác thực với token. Vì vậy, trong phần mở đầu trong yêu cầu này cần phải có chính token được yêu cầu hủy bỏ. Quá trình hủy bỏ sẽ gọi hàm trợ giúp trong lớp User để thiết lập giá trị mới cho thời gian hết hạn của token. Dữ liệu mới của token cũng sẽ được lưu vào cơ sở dữ liệu. Hồi đáp cho yêu cầu này không có phần thân, vì vậy chúng ta có thể trả về một chuỗi trống. Giá trị thứ hai được trả về trong hàm này sẽ thiết lập mã trạng thái của hồi đáp là 204, đại diện cho một yêu cầu được thực hiện thành công nhưng nhận được hồi đáp không có phần thân.

Sau đây là ví dụ về yêu cầu hủy bỏ token được gởi đi bằng HTTPie:

Các thông báo lỗi cho API

Bạn còn nhớ điều gì đã xảy ra khi chúng ta gởi một yêu cầu API từ trình duyệt với một URL bất hợp lệ trong phần đầu của bài này không? Chúng ta đã nhận được một lỗi 404, nhưng thông báo lỗi này được định dạng bằng một trang báo lỗi 404 theo chuẩn HTML. Nhiều thông báo lỗi API có thể được thay thế bằng phiên bản JSON trong blueprint của API, nhưng một số lỗi trong Flask vẫn còn được xử lỳ bởi các hàm xử lý lỗi toàn cục trong ứng dụng và tiếp tục trả về các thông báo lỗi dạng HTML.

Giao thức HTTP hỗ trợ một cơ chế cho phép client và server chấp nhận định dạng tốt nhất cho các hồi đáp được gọi là thương thảo nội dung (content negotiation). Client cần gởi một yêu cầu HTTP với phần mở đầu có thuộc tính Accept để đề nghị một danh sách các định dạng của hồi đáp mà client muốn nhận được. Server sẽ kiểm tra danh sách này và hồi đáp với định dạng tốt nhất mà nó hỗ trợ từ danh sách này.

Chúng ta sẽ cập nhật các hàm xử lý lỗi toàn cục để chúng sử dụng cơ chế thương thảo nội dung và trả về dữ liệu dạng HTML hoặc JSON tùy theo đề nghị từ client. Điều này có thể được thực hiện thông qua đối tượng request.accept_mimetypes từ Flask:

app/errors/handlers.py: Thương thảo nội dung cho các hồi đáp có thông báo lỗi.

Hàm trợ giúp wants_json_response() sẽ so sánh giữa hai định dạng JSON và HTML trong danh sách đề nghị các định dạng cho hồi đáp được client gởi đến. Nếu định dạng JSON có thứ tự ưu tiên cao hơn HTML, server sẽ gởi hồi đáp dạng JSON đến client. Trong trường hợp ngược lại, server sẽ gởi hồi đáp theo dạng HTML từ các template báo lỗi đã được định nghĩa trước đây. Đối với các hồi đáp sử dụng JSON, chúng ta sẽ dùng hàm trợ giúp error_response từ blueprint của API nhưng chúng ta sẽ đặt tên lại là api_error_response() để phân biệt rõ công dụng và nguồn gốc của hàm này.

2 thoughts on “Hướng dẫn lập trình Flask – Phần 23: Xây dựng API

  1. Bạn chổ Blog ví dụ có thêm danh mục blog vậy để hiển thì ở forntend theo danh mục bạn hướng dẫn mình làm đoạn này với. Thanks bạn

  2. Chào anh em đang muốn học backend với python em rất vui khi biết tới blog hướng dẫn này của anh
    Bài viết cảu anh rất hay và bổ ích
    Cảm ơn anh chúc anh nhiều sức khoẻ
    Nếu có thể mong anh có thể làm về django vì em đang muốn tìm hiểu về nó
    Em xin cảm ơn

Leave a Reply

Your email address will not be published. Required fields are marked *