Hướng dẫn cơ bản framework FastAPI từ A – Z (Phần 2)

Tram Ho

Lời mở đầu

Hôm nay tôi sẽ cùng các bạn nghiên cứu tiếp framework FastAPi, cụ thể là về vấn đề bảo mật của framework này.

Security

Hiện nay có rất nhiều cách giải quyết vấn đề về bảo mật, xác thực hay ủy quyền. Đây là 1 chủ đề khó, nhiều lập trình viên tốn kha khá công sức để viết code. Bài viết này sẽ gói gọn trong khuôn khổ của framework, giới thiệu các tools giúp bạn thực hiện chức năng bảo mật nhanh chóng và dễ dàng.

Các chuẩn bảo mật

Phần này đi nhanh thôi, mặc định các bạn biết rồi đi.

OAuth2

Là 1 chuẩn giao thức ủy quyền ra đời vào tháng 10 năm 2012, được sử dụng ở hầu hết mọi ứng dụng (web, mobile), cho phép người dùng cung cấp thông tin cá nhân bởi ứng dụng của bên thứ 3, cũng được dùng để cung cấp cơ chế cho việc xác thực người dùng.

OAuth 1

Đây cũng là 1 chuẩn giao thức nhưng khác với OAuth2, phức tạp hơn vì có thêm cơ chế mã hóa giao tiếp. Tại thời điểm này, chuẩn này không còn được dùng nhiều nữa.

OpenID

Còn cũ hơn cả 2 cái trên, chuẩn này cần người dùng cung cấp username và password. Nói chung là có 1 nhà cung cấp OpenID, chúng ta tài khoản trên đó, tài khoản này có thể truy cập vào các trang web sử dụng cơ chế xác thực của OpenID.

OpenID Connect

Based trên OAuth2, là 1 layer nằm phía trên giao thức OAuth2.

Cơ chế bảo mật của FastAPI

Do based trên OpenAPI nên FastAPI thừa kế security flow của OpenAPI

  • apiKey: chỉ là key mà thôi, có thể đến từ query param, header hoặc cookie.
  • http: hệ thống xác thực của HTTP, bao gồm:
    • bearer: header param với giá trị là một token (thừa kế từ OAuth2)
    • HTTP Basic authentication
    • HTTP Digest authentication
  • oauth2
  • openIdConnect

Practice

Khu vực này mang tính chất minh họa, code chưa hoàn thiện

Giả sử hiện giờ tôi có 1 backend API trên server, 1 frontend trên Android, backend và frontend đã kết nối với nhau nhưng còn thiếu 1 ổ khóa nếu không request nào đều có thể dễ dàng thông qua frontend tới backend. Đơn giản nhất tôi dùng username và password, ở đây tôi dùng OAuth2 để build.

Trong hàm main.py, thêm cơ chế xác thực dựa trên login username/password của OAuth2. Khi chạy app, kiểm tra trong doc ta có 1 url api /items/, góc phải trên cùng có 1 box Authorize là nơi để điền username/password.

Áp dụng trong thực tế khi có frontend:

  • Người dùng sẽ đăng nhập username/password trên frontend
  • Frontend (browser chẳng hạn) gửi username và password tới url api (tokenUrl=”token”)
  • API kiểm tra username và password, trả về kết quả là 1 token (code trên chưa có đoạn này, đoạn dưới sẽ đề cập)
    • Token là 1 chuỗi các ký tự dùng để xác thực người dùng.
    • Token tồn tại có thời hạn, thời hạn này do bên phía backend setup
  • Frontend sau đó lưu token ở một nơi tạm thời nào đó (cookie, ram, …)

Tạo 1 instance của OAuth2PasswordBearer oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token"). Ở đây tôi không tạo endpoint nhưng định nghĩa đường dẫn /token để người dùng lấy token sau đó, khá quan trọng khi làm việc với proxy.

Còn nếu muốn trả token về trong response body. Ok truyền param vào hàm read_items biến token có type là string phụ thuộc instance oauth2_scheme: token: str = Depends(oauth2_scheme). Param này sẽ ngó qua request ở Authorization header, kiểm tra xem có token hay không và trả về, nếu không có thì sẽ báo lỗi 401 UNAUTHORIZED.

OAuth2 với Password và Bearer

Như tiêu đề, tôi sẽ dùng cách thức xác thực đơn giản nhất để tạo ổ khóa, đó là dùng username/password, vì vậy tôi cần validate khi người dùng điền username/password. Giả sử tôi có 1 dict user có username là admin và password là 123456. Khi người dùng đăng nhập tôi phải so sánh xem 2 cái username có giống nhau không hoặc 2 cái password có giống nhau không.

Ok ở đây có 2 trường hợp nhập sai => trả về error code 400. Như hình dưới là do tôi nhập sai tên.

Trường hợp đúng sẽ có hình như thế này:

Nếu muốn bảo mật thêm, tôi phải tùy biến password nhận được, như thêm 1 hàm hash code chẳng hạn, để nếu mà hacker có hack mất pass ý thì chỉ lấy được hashed password thôi

Get current user

Trước khi đi vào phần JWT, như các bạn biết phần Pratice trên tôi có giới thiệu sơ qua cơ chế bảo mật của fastapi là follow theo oauth2 nên phải import OAuth2PasswordBearer, api đòi hỏi đăng nhập nhưng username với password ở đâu ? Để giải quyết vấn đề này tôi kết hợp OAuth2PasswordBearerOAuth2PasswordRequestForm. Cái sau cung cấp cho người dùng form để đăng nhập (url api), kết quả là tạo token, thông báo có token với type bearer này và cái trước phát hiện có token sẽ cho phép trả về thông tin người dùng. Nghe có vẻ hơi khó hiểu nhưng không sao tôi sẽ giải thích code cho bạn.

Code tổng thể là đây, tôi sẽ bóc tách từng phần ra.

Khai báo người dùng có các thông tin như sau

Khai báo User model thừa kế từ BaseModel của thư viện pydantic.

Hàm login này tạo 1 url api token, khi người dùng đăng nhập sẽ gửi 1 request về phía server, thông tin của request chứa trong biến form_data. Dựa trên attribute username của form_data, nếu đúng giá trị thì ta sẽ được 1user dict, ngược lại trả về lỗi 400. Lại xét tiếp attribute password của form_data, nếu sai cũng trả về lỗi 400, ngược lại trả về token là tên của người dùng (bạn có thể trả về token tùy ý, không nhất thiết là tên người dùng).

Ok đã có token, chúng ta cần pass qua ô Authorize ở góc phải trên cùng. Chúng ta tiếp tục validate token bằng hàm get_current_user. Hàm này có param truyền vào là token dạng string, phụ thuộc vào instance oauth2_scheme đã khai báo ở trên. Token sau đó truyền vào hàm fake_decode_token, hàm này có nhiệm vụ decode token và trả về nguyên dạng (nếu bạn encode bằng thuật toán mã hóa nào đó thì lúc decode cũng phải dùng thuật toán đấy), nhưng ở đây tôi chỉ code ví dụ nên không có mã hóa token gì hết. Trong hàm fake_decode_token gọi hàm get_user xem thử token (ở đây là username) có trong db (ở đây tôi chỉ khai báo 1 biến dict, các bạn có thể tùy biến nhé) hay không, nếu có thì trả về user. Đấy validate chỉ đơn giản thế thôi, các bạn muốn phức tạp thì có thể tùy biến mấy hàm này.

Kết quả đúng khi trả về

Kết quả sai khi trả về

Cuối cùng khai báo hàm read_users_me để tạo api /users/me, biến current_user phụ thuộc vào hàm get_current_user.

OAuth2 với Password (có hashing), Bearer với JWT tokens

Nói sơ qua về JWT, viết tắt của JSON Web Tokens, là 1 chuỗi các ký tự, dạng mã hóa của Json. Ví dụ:

Mỗi chuỗi phân cách nhau bằng dấu chấm. Ở trên có 3 chuỗi tương ứng với:

Signature a.k.a chữ ký cần truyền vào là 1 token.

Các thư viện cần cài thêm: jose (để sing JWT token, passlib (hash password)

Phần này tập trung việc “băm” và xác thực mật khẩu nên tôi thêm một số thư viện vào.

Khai báo user dict

Gọi instance cần thiết

Đầu tiên tôi cần các hàm dùng để xác thực người dùng với các biến truyền vào là user, tên người dùng, mật khẩu. Nếu username không khớp với user["email"] hoặc password không khớp với user["password"] đã được hash thì trả về False, ngược lại trả về user (True).

Để mã hóa password, tôi gọi hàm get_password_hash, instance pwd_context dùng thuật toán Bcrypt để mã hóa mật khẩu.

Để xác thực mật khẩu, tôi so sánh biến password được truyền vào và password của user trong db bằng hàm verify_password. Cơ chế so sánh tôi không cần quan tâm bởi instance pwd_context hỗ trợ sẵn rồi.

Tiếp, nếu bạn thông qua validate username/password thì sẽ nhận được token trả về, thấy quen không, bên trên đã code rồi đấy còn hàm dưới đây sẽ thêm các cơ chế bảo mật như: mã hóa, thời gian tồn tại. Mục đích của hàm là encode 1 json data (có thể là thông tin người dùng hay thứ khác tùy bạn).

Tạo 1 biến copy data gọi là to_encode. Khởi tạo biến expire có thời gian tồn tại là 15 phút kể từ thời điểm hiện tại. Biến to_encode sẽ cập nhật expire. Thời điểm để tôi dùng thư viện jose tới rồi , thư viện này encode json dict to_encode thành JWT với Secret Key và thuật toán HS256. Biến ACCESS_TOKEN_EXPIRE_MINUTES là thời gian tồn tại khoảng 30 phút, nếu không truyền vào hàm create_access_token thì mặc định là 15 phút.

Sau tất cả, hàm authenticate_usercreate_access_token sẽ được gọi ở đâu ? Tất nhiên là trong hàm login rồi . Hàm này nhận request từ form, request đi qua hàm authenticate_user để validate. Nếu kết quả false thì trả về lỗi 401, ngược lại tạo token với payload là email. và trả về token đó. Đơn giản thế thôi.

Nếu bạn để ý ở các api thường đòi hỏi api key ở header, cho nên tôi cũng sẽ code thêm 1 url api đòi hỏi access token. Hàm text_to_speech có biến truyền vào là 1 Header parameter, biến này sẽ được decode để lấy payload, phần decode dùng thuật toán và Secret Key y hệt encode. Validate xem 2 cái user có giống nhau không, nếu giống thì “do something” thôi, trả về kết quả còn ngược lại trả về lỗi 400.

Tổng hợp tất cả các block code ở trên:

Page 1

Page 2

Page 3: Copy access token từ page 2 và nhét vào header box

Kết luận

Xong rồi đấy, có lẽ sẽ có phần 3 bởi framework này có nhiều thứ để vọc. Ai đi ngang qua tiện thể cho 1 upvote để mình có động lực viết tiếp. Cám ơn vì đã đọc đến tận đây

Reference

https://fastapi.tiangolo.com/tutorial/security/

https://jwt.io/

https://oauth.net/2/

https://dzone.com/articles/open-id-connect-authentication-with-oauth20-author

https://medium.com/@greekykhs/whats-the-difference-oauth-1-0-and-oauth-2-0-9f1d22e06963

https://en.wikipedia.org/wiki/OAuth

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo