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 由 Header
、Payload
和 Signature
,三部分組成,像這樣的形式:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJBdXRobGliIiwic3ViIjoiMTIzIiwibmFtZSI6ImJvYiJ9.
cBo6e7Uss5__16mlqZECjHJSKJDdyisevDP5cUGvJms
換行符只是為了展示用,實際 token 中不包括換行符
Header
用於指定采用的加密算法,以及 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
)
即先將 header
和 payload
分別做 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,可見,被
.
分隔為三部分,前兩部分是header
和payload
的 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
方法,利用secret
對token
進行解碼,如果簽名正確,就會的到解碼內容,解碼對象是authlib.jose.JWTClaims
類的實例 - 打印出解碼內容,可以看到和生成 token 時的
payload
內容一致 - 打印出
header
,可以看到typ
為JWT
,即使用默認值 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 做介紹,敬請期待。
參考
- https://tools.ietf.org/html/rfc7515
- https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
- https://docs.authlib.org/en/stable/jose/jwt.html
- https://en.wikipedia.org/wiki/JSON_Web_Token
- https://swagger.io/docs/specification/authentication/bearer-authentication/
關注公眾號:python技術,回復"python"一起學習交流