第82天: JWT 簡介

by 太陽雪

在之前的課程中,介紹過 Flask-Login 框架,它是基於 Session 和 Cookie 技術來實現用戶授權和驗證的,不過 Session 有很多的侷限性,這一節介紹一種基於 token 的驗證方式 —— JWT (JSON Web Token),除了對 JWT 的概念講解之外,還有在 Flask 中簡單實踐

session 的侷限性

基於 Session 的驗證過程大體是:服務器端有一個 Session 詞典,當用戶驗證登錄後,在詞典中爲該用戶創建一個 Session 對象,在響應( response )中返回一個 Session id,當用戶下次請求時,攜帶 Session id,服務器從 Session 詞典中可以恢復出 Session 對象,以完成用戶的驗證,在用 Session id 從恢復出認證實體。

從 Session 驗證過程可以看出一些侷限性:

  • 服務器橫向擴展很困難:因爲 Session 只能存活在一個服務實例中,將用戶請求引導到其他服務器,將丟掉用戶的登錄狀態
  • 攜帶信息量少,恢復會話信息比較耗時:Session 認證後,客戶端得到 Session ID, 服務器無法從 Session ID 中得到更多信息,需要從數據庫、文件系統或緩存中取得用戶信息,比較耗時
  • 沒有統一標準:Session 由各個服務器框架自己實現,沒有統一標準,存在應用擴展困難的問題,特別加密方式,五花八門,有很大的安全隱患

token 簡介

爲了解決 Session 的問題,有了 token 的驗證方式。

token 可以理解成票據,或者憑證,當用戶得到服務器的認證後,由服務器頒發,在之後的請求時攜帶,免去頻繁登錄。

token 不同於 Session 的地方:

  • 可以獨立於具體的服務器框架生成和校驗
  • 可以攜帶更多的信息,避免對持久層的查詢操作
  • 基於標準的算法可以由不同的節點完成驗證

爲了利用好 token 的驗證機制,IEIT (互聯網工程任務組),制定了基於 JSON 數據結構的網絡認證方式 JWA(JSON Web Algorithms),還針對不同應用場景提出了具體協議,如 JWS、JWE、JWK 等,他們可以統稱爲 JWT,即 Javascript Web Token。

理解 JWA

JWA 的全稱是 JSON Web Algorithms

JSON 是 Javascript 的語言的文本對象表示法,是一種獨立語言環境的數據結構表示,可以用網絡數據傳輸,在前面 RESTful 章節中,對 API 調用的返回數據格式就是 JSON。

Algorithms 本義是算法的意思,這裏特指加密算法,也就是用 JSON 表示的數據,經過加密後在在服務器端和客戶段之間傳輸。

有了數據結構和加密算法的基礎,根據不同的應用場景,定義出了具體實現:

  • JWS(JSON Web Signature)對數據進行簽名的,用於防止數據被篡改,傳輸不敏感數據的情況
  • JWE(JSON Web Encryption)對數據做了加密的,用於傳輸敏感數據,具有更好的安全性
  • JWK(JSON Web Key)是通過密鑰對數據進行加密的方法,規定了相應的加密算法

JWT(JSON Web Token)上面 JWS、JWE 和 JWK 的總稱。

JWT 簡介

JWT Wiki 上的定義是:

JSON Web Token is an Internet standard for creating JSON-based access tokens that assert some number of claims.

大致意思是,JWT 是用基於 JSON 數據結構的生成包含了一些權限聲明的網絡訪問憑證的網絡標準

數據結構

JWT 由 HeaderPayloadSignature,三部分組成,像這樣的形式:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJBdXRobGliIiwic3ViIjoiMTIzIiwibmFtZSI6ImJvYiJ9.
cBo6e7Uss5__16mlqZECjHJSKJDdyisevDP5cUGvJms

換行符只是爲了展示用,實際 token 中不包括換行符

用於指定採用的加密算法,以及 JWT 採用的形式類型,例如:

{
    "alg" : "HS256",
    "typ" : "JWT"
}
  • alg 指定前面所用的算法,默認爲 HmacSHA256 簡寫爲 HS256,還有 HS384、RS256 等
  • typ 是指令牌的類型,JWT 令牌的類型爲 JWT

Payload

用於攜帶一些信息,例如用戶名,過期時間 等等,例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

JWT 標準定義了 7 個字段:

字段 說明
iss (issuer):簽發人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受衆
nbf (Not Before):生效時間
iat (Issued At):簽發時間
jti (JWT ID):編號

這些字段有實現這自由選取,也可以加入其他自定義字段

Signature

首先,需要指定一個密鑰(secret)。密鑰很重要,需要嚴格保密

然後,使用 Header 裏面指定的簽名算法(默認是 HMAC SHA256),按照下面的公式產生簽名:

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

即先將 headerpayload 分別做 base64url 編碼,
然後用 . 將他們連接成一個字符串,用加密算法,使用密鑰 secret, 得到的加密結果就算簽名

Base64URL 編碼字符集是 Base64 字符集的子集
= 被省略、+ 替換成 -/ 替換成 _
因爲 token 可能通過 URL 進行傳輸,而 =+/ 在 URL 中有特殊含義

驗證

當客戶端發送請求時將 token 送到服務器端,可以用和簽名同樣的方式,重新計算一次簽名,如果和客戶端送過來的簽名一致,說明 token 沒有被篡改,如果不一致,說明 token 已被篡改,不安全了。

由此可見,用於做簽名的密鑰 secret 很重要,一旦泄漏,將無法鑑別 token 的真僞

JWT 應用

關於 Python 的 JWT 實現不止一個,不同的庫,不同的實現方式層出不窮,今天要講解的是 Python 的 Authlib 庫,它是一個大而全的 Python Web 驗證庫支持多種 Python 框架

Authlib 的 JWT

Authlib 是構建 OAuth 和 OpenID 安全連接服務器的終極 Python 庫,包括了 JWS, JWE, JWK, JWA, JWT

Authlib 功能強大而豐富,今天我們只瞭解他的 JWT 部分,之後在介紹基於第三方認證的 OAuth 技術時還會進一步講解

安裝

使用 pip 安裝

pip install Authlib

如果一切正常,可以導入 Authlib 模板,例如,引入 jwt :

>>> from authlib.jose import jwt
>>>

小試牛刀

JWT 是服務器端的機制,所以可以在命令行中做測試

生成 token

>>> from authlib.jose import jwt
>>> header = {'alg': 'HS256'}
>>> payload = {'iss': 'Authlib', 'sub': '123', 'name': 'bob'}
>>> secret = '123abc.'
>>> token = jwt.encode(header, payload, secret)
>>> print(token)
b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJBdXRobGliIiwic3ViIjoiMTIzIiwibmFtZSI6ImJvYiJ9.
cBo6e7Uss5__16mlqZECjHJSKJDdyisevDP5cUGvJms'
  • 導入 jwt 模塊
  • 定義 header,並且設置簽名算法爲 HS256
  • 定義 payload,作爲傳輸信息
  • 定義 secret,注意這裏只是方便演示,實際項目中最好是隨機生成,並妥善保存
  • 使用 jwt 的 encode 方法,生成 token,encode 方法一次性實現了所有關於 JWT 協議的定義
  • 打印出 token,可見,被 . 分隔爲三部分,前兩部分是 headerpayload 的 Base64Url 編碼,最後一部分是 簽名

解碼 token

接上面的環境:

>>> claims = jwt.decode(token, secret)
>>> print(claims)
{'iss': 'Authlib', 'sub': '123', 'name': 'bob'}
>>> print(claims.header)
{'alg': 'HS256', 'typ': 'JWT'}
>>> claims.validate()
>>>
  • 用 jwt 模塊的 decode 方法,利用 secrettoken 進行解碼,如果簽名正確,就會的到解碼內容,解碼對象是 authlib.jose.JWTClaims 類的實例
  • 打印出解碼內容,可以看到和生成 token 時的 payload 內容一致
  • 打印出 header,可以看到 typJWT,即使用默認值
  • validate 方法用於檢驗 token 的有效性,比如:是否過期、主題是否一致,是否每到生效時間等等,也可以爭對每種情況單獨做驗證,例如 validate_exp 可用檢驗是否過期

雖然 JWT 理論很繁瑣,但 Authlib 庫提供了簡潔的方法,讓開發應用變得更高效

與客戶端交互

JWT 之所有流行,有個重要原因時可以支持多種客戶端,例如 瀏覽器和 app,JWT 標準規定,一般情況下,客戶端需要將 token 放在 Http 請求的 Header 中的 Authorization 字段中,據個例子:

GET /resource HTTP/1.1
     Host: server.example.com
     Authorization: Bearer mF_9.B5f-4.1JqM
  • 用 GET 方式請求 /resource ,在 Header 中添加了 Authorization 字段

  • 不能直接將 token 作爲 Authorization 的值,必須有類型聲明,這裏是 Bearer

Bearer 表示這個 token 是有認證服務器生成的,用來做身份識別的,除此之外,IEIT 還定義了其他 認證類型,如 Bisic, Digest,可以簡單理解成 Bearer 就是 JWT 的認證類型

除了通過 Http Header 類攜帶 token 之外,還可以通過 POST 請求主體,以及 URL 中的 querystring 來向服務器發送 token,這兩種情況下,需要使用 access_token 字段來表示 token

JWT 標準建議使用 Header 方式,除非 Header 無法使用時才考慮其他方式

Flask JWT

Authlib 主要的用途在打造一個 OAuth 應用,對於單獨做 JWT 的實踐有些麻煩,因此我們用 flask-jwt 框架,做 JWT 的實踐。

flask-jwt 和之前講述的 flask-login 用法很像,是基於 JWT 的認證的框架,提供和很多方便實踐的特性

安裝 flask-jwt

pip install Flask-JWT

創建應用

爲了簡單,將所有代碼放在 app.py 中:

from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp

# User 類,用於模擬用戶實體
class User(object):
    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = password

    def __str__(self):
        return "User(id='%s')" % self.id

# User 實體集合,用於模擬用戶對象的緩存
users = [
    User(1, 'user1', 'abcxyz'),
    User(2, 'user2', 'abcxyz'),
]

username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}

# 獲取認證的回調函數,從 request 中得到登錄憑證,返回憑證所代表的 用戶實體
def authenticate(username, password):
    user = username_table.get(username, None)
    if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')):
        return user

# 通過 token 獲得認證主體的回調函數
def identity(payload):
    user_id = payload['identity']
    return userid_table.get(user_id, None)

app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'super-secret'

jwt = JWT(app, authenticate, identity)  # 用 JWT 初始化應用

@app.route('/protected', methods= ["GET", "POST"])  # 定義一個 endpoint
@jwt_required()  # 聲明需要 token 才能訪問
def protected():
    return '%s' % current_identity  # 驗證通過返回 認證主體

if __name__ == '__main__':
    app.run()

運行:

$ python app.py
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 566-326-511

獲取 access_token

flask-jwt 默認的獲取 token 的路由是 /auth,請求方式是 POST,用 JSON 傳送用戶名密碼給服務器,例如:

$ curl -X POST -H "Content-Type: application/json" localhost:5000/auth -d '{"username":"user1","password":"abcxyz"}'
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
  eyJleHAiOjE...<省略>...VudGl0eSI6MX0.
  M-shnDPAVdu...<省略>...LaH1EMIbrWjPto"
}

如果登錄憑證正確,則返回 access_token,可以看到被 . 分隔成三部分,即 JWT 的結構

使用 access_token

flask-jwt 默認通過 Header 傳送 token,爲了和 OAuth 生成的 JWT 做區分,默認使用 JWT 作爲 token 的類型,例如,用上面生成的 JWT 請求 /protected

curl -H "Authorization: jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE...<省略>...VudGl0eSI6MX0.M-shnDPAVdu...<省略>...LaH1EMIbrWjPto" localhost:5000/protected
User(id='1')

如果 token 有效,則返回 token 對應的認證實體,這個例子中打印出了 user 實體

總結

本節課程講解了基於 token 驗證的 JWT,使用 Authlib 庫對 JWT 做了實踐練習,期望能幫助您更好的理解 JWT,最後通過 flask-jwt 模塊,實踐了 JWT 的驗證方式,和使用方式。在後續的課程中還會對目前流行的第三方認證框架 OAuth 做介紹,敬請期待。

示例代碼:Python-100-days-day093

參考

關注公衆號:python技術,回覆"python"一起學習交流

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章