04 Python Flask 教學10 所有文章

【Flask 教學系列】淺談 JWT 與 Flask JWT 實作

Flask教學系列_JWT_Max行銷誌

一. 前言

在 www 的世界中,Client 與 Server 間的溝通需透過 HTTP 協定發送 request 和接收 response,但因為 HTTP 協議 stateless 無狀態的設計,代表著 Client 與 Server 兩端不會記得先前的狀態,Client 每次發送請求 request 都會被視為是獨立的,也就是說 Server 無法知道 Client 是否已經發送過認證請求。

而為了讓 HTTP 請求保有狀態,上次在 【Flask教學】Flask Session 使用方法和介紹 中介紹了 Cookie 與 Session 的結合(Server Side Session 和 Client Side Session)。將 Session Information 存放在 Cookie 中,讓 Server 在接收到 request 請求時,可以從 Cookie 中取得先前儲存的狀態,例如使用者是否已經登入、或加入過的購物車資訊。

Server Side Session:
如果是在 Cookie 僅存一個 Session_id,而當使用者發送請求時,Server 會根據這個 Session_id 再去資料庫撈取使用者相關資料,來判斷之前儲存的狀態資訊 。

Client Side Session:
而將 Session 資料加密後,儲存於 Cookie,並沒有再由資料庫再去做撈取的方式,這種專業術語叫做 Client Side Session。Flask 內建的 Session 就是採用的就是這種方式,但是也可以使用擴充套件 Flask-Session 來取代儲存於瀏覽器。

[Flask教學] Flask 實作 Session 操作和淺談

上篇文章中【Flask教學系列】實作 Flask Session-base login 登入驗證 介紹了 Session-based Authentication,接下來就是進入今天的正題 token-based Authentication。

二. 什麼是 JWT (JSON Web Token)?

JWT (JSON Web Token) 簡單來說是:使用者在登入或是驗證過身份過後,後端會在返回請求中附上 JWT Token,未來使用者發 Request 時有攜帶此 Token,就表示通過驗證,而沒有攜帶 JWT Token 的使用者就會拒絕請求,需要再重新登入或重新驗證身份。

嚴謹一點的說法是 JWT 將 JSON 結構的資料進行 Base64Url 編碼並加上數位簽章 Signature 後組成 Token 傳遞給 Client 端,然後此 Token 可用於:

  1. 伺服器端進行驗證身分
  2. 訊息交換使用

三. JWT 實作流程

▍流程 1 – 產生 JWT Token:

後端收到 Login / Signup 請求時,會產生 JWT Token,並附在 response 內返回; 前端收到 JWT Token 後會將 Token 保存。

▍流程 Part 2 – 驗證 JWT Token:

未來當前端發送請求時,都需要將剛剛 Login / Signup 請求時收到的 JWT Token,放在 HTTP Header、Request Body 或 URL 上的 Query Parameter,三者擇一使用。

後端會利用前端請求所附上的 JWT Token 來驗證是否有權利請求和 JWT Token 資料內是否正確。

關於前端如何保存 JWT Token 最佳實踐方法可以參考此篇: The Ultimate Guide to handling JWTs on frontend clients

四. JWT 組成結構

由上圖可以看到 JWT 其實是由三段亂碼組成,分別是

  1. Header
  2. Payload
  3. Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  # header
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.  # payload
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ  # signature

▍Header

Header 存放的是一種聲明,alg 中說明著數位簽章使用的加密演算法是 HS256,而 type 說明這個 token 是 JWT。

{
"alg": "HS256",
"typ": "JWT"
}

然後經過 Base64-Url 編碼過後,就會得到 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 (這段是可以被解密的)

▍Payload

將使用者的狀態存放於此,官方有建議註冊參數,像是 iss (issuer), exp (expiration time), sub (subject), aud (audience), and others.

{
"iss": "Max",
"exp": "2021/01/01",
"iat": "2020/05/16",
"aud": "id0001",
"name": "John Doe",
"admin": true
}

然後經過 Base64-Url 編碼過後,就會得到 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 (這段是可以被解密的)

▍Signature

數位簽章的部分,首先會在 Server 建立一組 secret_key,然後再將 Header、Playload 和這組 secret_key,使用 Hash 256 加密。

未來拿要做驗證時,只需用相同的方法 (收到的 Header + 收到的 Payload + Server 的 key )加密後比對與收到的 JWT 是不是一樣的亂碼,如果有不同就可以知道資料被竄改過了。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret_key)

再加上 Base64-Url 編碼過後,就會得到 TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ (此段是被解密也沒辦法拿到 secret key)

五. Flask 如何實作 JWT

目前 Flask 常見的 JWT 套件大概以下三種:

  1. Flask-JWT-Extended
  2. Flask-JWT
  3. PyJWT

推薦選擇 Flask-JWT-Extended,因為此套件除了提供實現 JSON Web Tokens 功能外,還提供許多其他功能,像是 Refresh Tokens 功能,或是 Partially protecting routes 功能,這些功能待會都會再詳細介紹。

▍實作簡單範例

Part 0 – 安裝 Flask-JWT-Extended

pip install flask-jwt-extended

Part 1 – 載入 & 實例化 JWT

from flask_jwt_extended import JWTManager

jwt = JWTManager()

# 設定 JWT 密鑰
app.config['JWT_SECRET_KEY'] = 'this-should-be-change'
jwt.init_app(app)

Part 2 – 建立 JWT Token

當使用者在登入或註冊成功時,使用 create_access_token 來建立 JWT,並將 JWT 返回給前端。

@app.route('/login', methods=['POST'])
def login(): 
    username = request.json.get('username', None) 
    password = request.json.get('password', None) 

    if username != 'test' or password != 'test': 
        return jsonify({"msg": "Bad username or password"}), 401

    access_token = create_access_token(identity=username)
    return jsonify(access_token=access_token)

使用者登入後,前端會將 access_token 存在 cookie、localstorage、memory 或其他地方,未來呼叫後端 API 時,需要帶上 JWT,可以放在以下三種位置:

1.放在 HTTP Header 裡面

Authorization: Bearer JWT

2.POST方法:放在 Request Body 裡面

access_token=JWT

3.GET方法:放在 URI 裡面的一個 Query Parameter

?access_token=JWT

Part 3 – 後端驗證 JWT Token

只需要在 route 下方加上裝飾詞 @jwt_required,就會自動判斷是否有帶入正確的 JWT 了。

@app.route('/protected', methods=['GET', 'POST'])
@jwt_required
def protected(): 
    return jsonify(msg='ok')

基本上到這邊就完成 JWT 的驗證囉!

▍ JWT Refresh Token 的使用

1. 為什麼需要 Refresh Token

一但 JWT Token 被簽發出來,在期限到期之前始終有效,即使使用者的帳號已經被註銷,只要使用者持有 Token 在期效內都會通過驗證;是可以建立黑名單來過濾被註銷的 Token (可參考此篇:redis + blacklist · flask-jwt-extended · GitHub),但就會需要再額外建立 redis 資料庫,就喪失了 JWT 無狀態 Stateless 的特性。

所以為了讓 JWT Token 的過期時間越短越好,又要能避免使用者需要重複登入驗證身份,所以要介紹 Refresh Tokens — flask-jwt-extended 的功用。

當使用者 JWT Token 過期時,Client 設定在背景呼叫 refresh token API, 當收到 refresh token 請求時,對資料庫進行驗證,如果驗證失敗則不產生新的 JWT Token。

這樣 Short-lived JWT + Long-lived refresh token 的方式,可以保有 JWT 無狀態 Stateless 的特性,也因為 Short-lived JWT 可以減緩 JWT Token 無法被註銷的問題。

2. 如何實作 Refresh Token

Refresh Token API 範例:

from flask import Flask, jsonify, request
from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    jwt_refresh_token_required, create_refresh_token,
    get_jwt_identity
)

app = Flask(__name__)

app.config['JWT_SECRET_KEY'] = 'super-secret'  # Change this!
jwt = JWTManager(app)


@app.route('/refresh', methods=['POST'])
@jwt_refresh_token_required
def refresh():
    current_user = get_jwt_identity()
    ret = {
        'access_token': create_access_token(identity=current_user)
    }
    return jsonify(ret), 200

JWT Token 過期時間預設為 15 分鐘,Refresh Token 過期時間預設為 30 天,可以依照自己喜好修改:

JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(minutes=15)

JWT_REFRESH_TOKEN_EXPIRES = datetime.timedelta(days=30)

完整官方文件:
https://flask-jwt-extended.readthedocs.io/en/stable/refresh_tokens/#refresh-tokens

最後~

▍回顧本篇我們介紹了的內容:

  • 前言
  • 什麼是 JWT (JSON Web Token)?
  • JWT 實作流程
  • JWT 組成結構
  • Flask 如何實作 JWT
    • 為什麼推薦 Flask-JWT-Extended ?
      • Part 0 – 安裝 Flask-JWT-Extended
      • Part 1 – 載入 & 實例化 JWT
      • Part 2 – 建立 JWT Token
      • Part 3 – 後端驗證 JWT Token
    • Refresh Token

關於 Flask 教學的延伸閱讀:

▍關於 Flask 教學系列目錄:

▍其他 Flask 相關教學:

那麼有關於【Flask 教學系列】Flask-JWT-Extended 實作 的介紹就到這邊告一個段落囉!有任何問題可以在以下留言~

有關 Max行銷誌的最新文章,都會發佈在 Max 的 Facebook 粉絲專頁,如果想看最新更新,還請您按讚或是追蹤唷!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *