使用 Django 編寫的 B/S 應用通常會使用 Cookie + Session 的方式來做身份驗證,用戶登錄信息存儲在後臺資料庫中,前端 Cookie 也會存儲少量用於身份核驗的數據,由後臺直接寫入。但是在開發調試階段,使用 Postman 等請求工具請求登錄時,可能會缺失前端本應存儲的數據,... ...
使用 Django 編寫的 B/S 應用通常會使用 Cookie + Session 的方式來做身份驗證,用戶登錄信息存儲在後臺資料庫中,前端 Cookie 也會存儲少量用於身份核驗的數據,由後臺直接寫入。但是在開發調試階段,使用 Postman 等請求工具請求登錄時,可能會缺失前端本應存儲的數據,而導致登錄信息核驗一直不成功。在本地聯調前後端時可能也會有問題。
本篇介紹基於 Token 的身份驗證機制,並使用 Vue 和 Django 實現。
基於 Token 的驗證流程
與 Session 不同的是,Token 機制不會將用戶登錄信息存儲在後臺資料庫中,而是生成含有身份信息的 Token 字元串存儲在前端中。在前端請求需要驗證的後臺 API 時,後端將優先攔截並核驗身份信息。
基於 Token 的驗證流程如下:
- 客戶端使用用戶名和密碼請求登錄
- 伺服器收到請求後,驗證用戶名和密碼
- 驗證成功後,服務端根據用戶信息簽發一個 Token,返回給客戶端
- 客戶端存儲 Token
- 客戶端每次向伺服器發送其它請求時,都要攜帶 Token
- 伺服器收到請求,若請求的 API 需要驗證身份,則先驗證 Token,成功後再返回數據
Token 的組成
構造 Token 的方法較多,只要客戶端和服務端約定好了生成和驗證的格式,則有很多自定義的方法。當然,也有一些標準的寫法,例如 JWT,讀作 /jot/,表示 JSON Web Tokens。
JWT 標準的 Token 有三個部分:
- header
- payload
- signature
三個部分使用 .
分隔開,且使用 Base64 編碼,示例如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
Header 主要蘊含兩部分內容的信息,分別是 Token 的類型和加密使用的方法。
初始數據對象示例如下:
{
"typ": "JWT",
"alg": "HS256"
}
上述數據對象在經過演算法加密、Base64 編碼後,變為 Token 的第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
Payload 為 Token 的具體內容,下麵是可選的標準欄位,也可以自定義添加需要的內容。
- iss: Issuer, 發行者
- sub: Subject, 主題
- aud: Audience, 觀眾
- exp: Expiration time, 過期時間, 可為時間戳格式
- nbf: Not before
- iat: Issued at, 發行時間, 可為時間戳格式
- jti: JWT ID
同樣的,該部分初始數據對象經過演算法加密、Base64 編碼後,變為 Token 的第二部分。
Signature
Signature 為 Token 的簽名部分,相當於是前兩部分的簽名,用於防止其他人篡改 Token 中的信息。在處理時,可以將生成的 Token 前兩段內容,使用 MD5 等簽名演算法進行處理,將結果作為本部分內容。
加密演算法
從上面對 Token 組成部分的介紹中,可以瞭解到,在規定 Token 需蘊含的數據信息後,需要經過一定的演算法加密、Base64 編碼後成為 Token 的第一、二部分。因此,在生成 Token 時,要解決使用什麼加密演算法。
此處的加密演算法一定要是可逆的、可解密的,因為我們不僅要生成 Token,還要能從 Token 中解析出我們生成時存儲的數據,以驗證用戶信息和 Token 的有效期。因此,這裡不能採用 MD5、SHA1 這樣的哈希演算法,因為它們無法解密,只能用於生成簽名。
在 Django 中內置了加密模塊 django.core.signing
,我們調用其中的 dumps
和 loads
函數實現加密和解密。
示例:
from django.core import signing
data = {
"username": "Zewan"
}
value = signing.dumps(data) # encrypt
raw = signing.loads(value) # decrypt
print(value, src)
Django 生成和驗證 Token
上面我們已經瞭解了 Token 機制的流程和採取的加密演算法,接下來介紹 Django 中如何編寫代碼以實現 Token 機制。
我規定 Token 的 Header 部分為 {"typ": "JWP", "alg": "default"}
,Payload 部分含有用戶名 username
和過期時間 exp
,Signature 我使用 MD5 演算法生成簽名。在登錄成功後,後端返回給前端 username 和 Token,由前端存儲起來;當前端發送需要驗證身份信息的請求時,將 username 和 Token 加入請求頭中,後端從請求頭獲取這兩部分,從 Token 中解析得到用戶名和過期時間,核驗請求頭中的 username 是否正確及 Token 是否有效。
處理 Token
我在 utils/token.py
文件中實現 Token 的生成和解析數據的功能:
import time
from django.core import signing
import hashlib
HEADER = {'typ': 'JWP', 'alg': 'default'}
KEY = "Zewan"
SALT = "blog.zewan.cc"
def encrypt(obj):
"""加密:signing 加密 and Base64 編碼"""
value = signing.dumps(obj, key=KEY, salt=SALT)
value = signing.b64_encode(value.encode()).decode()
return value
def decrypt(src):
"""解密:Base64 解碼 and signing 解密"""
src = signing.b64_decode(src.encode()).decode()
raw = signing.loads(src, key=KEY, salt=SALT)
return raw
def create_token(username):
"""生成token信息"""
# 1. 加密頭信息
header = encrypt(HEADER)
# 2. 構造Payload(有效期14天)
payload = {"username": username, "iat": time.time(),
"exp": time.time()+1209600.0}
payload = encrypt(payload)
# 3. MD5 生成簽名
md5 = hashlib.md5()
md5.update(("%s.%s" % (header, payload)).encode())
signature = md5.hexdigest()
token = "%s.%s.%s" % (header, payload, signature)
return token
def get_payload(token):
"""解析 token 獲取 payload 數據"""
payload = str(token).split('.')[1]
payload = decrypt(payload)
return payload
def get_username(token):
"""解析 token 獲取 username"""
payload = get_payload(token)
return payload['username']
def get_exp_time(token):
"""解析 token 獲取過期時間"""
payload = get_payload(token)
return payload['exp']
def check_token(username, token):
"""驗證 token:檢查 username 和 token 是否一致且未過期"""
return get_username(token) == username and get_exp_time(token) > time.time()
登錄成功批發 Token
在登錄請求處理函數中,驗證用戶名和密碼成功後,調用 utils/token.py
文件中的 create_token
函數生成 token,並將 username 和 token 返回前端。代碼較為簡單,此處不多展示。
中間件攔截驗證 (Middleware)
創建一個中間件(Middleware),在前端請求需要身份核驗的後端路由時,由該中間件核驗其 username 和 token,驗證成功後再放行,進入業務處理的 API 中。
我的項目名稱為 backend_demo
,在自動生成的 backend_demo
包中,我創建 middleware.py
文件,構建中間件。該文件內容如下:
提示:前端向請求頭添加 xxx 信息,一般會自動轉變為 HTTP_XXX (全部大寫)
from utils.token import check_token
from django.http import JsonResponse
try:
from django.utils.deprecation import MiddlewareMixin # Django 1.10.x
except ImportError:
MiddlewareMixin = object
# 白名單,表示請求裡面的路由時不驗證登錄信息
API_WHITELIST = ["/api/user/login", "/api/user/register"]
class AuthorizeMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.path not in API_WHITELIST:
# 從請求頭中獲取 username 和 token
username = request.META.get('HTTP_USERNAME')
token = request.META.get('HTTP_AUTHORIZATION')
if username is None or token is None:
return JsonResponse({'errno': 100001, 'msg': "未查詢到登錄信息"})
else:
# 調用 check_token 函數驗證
if check_token(username, token):
pass
else:
return JsonResponse({'errno': 100002,
'msg': "登錄信息錯誤或已過期"})
實現中間件後,將其添加進項目中,在 settings.py
文件的 MIDDLEWARE
中添加建立的中間件:
MIDDLEWARE = [
'backend_demo.middleware.AuthorizeMiddleware',
# ...
]
Vue 存儲和攜帶 Token
登錄成功存儲 Token
登錄成功後,前端獲取並存儲後端返回的 username 和 token。前端實現存儲的方式有很多,我這裡使用簡單的方法,將其存儲在 localStorage 中。
// 此處用 username 和 authorization 表示,放到項目中要依據情況修改該變數標識
localStorage.setItem("username", username);
localStorage.setItem("authorization", authorization);
請求頭攜帶用戶名和 Token
接下來實現請求頭攜帶用戶名和 Token 信息。
在 Vue.js 實現的項目中,我一般是用 Axios 向後端發送請求。在 Axios 中,不需要在每一處請求的代碼中添加請求頭代碼,只需要在 main.js
中配置 Axios 的預設請求器,即可使所有的請求中 headers 都攜帶用戶名和 token。
main.js
中核心代碼如下:
提示:這裡填入 headers 中雖然是
username
和authorization
,但會被自動轉化為HTTP_USERNAME
和HTTP_AUTHORIZATION
import axios from 'axios';
// add username and token into headers
axios.interceptors.request.use(
config => {
var username = localStorage.getItem('username');
var authorization = localStorage.getItem('authorization');
// 若 localStorage 中含有這兩個欄位,則添加入請求頭
if (username & authorization) {
config.headers.authorization = authorization;
config.headers.username = username;
}
return config;
},
error => {
return Promise.reject(error);
}
);
這樣,就在 Vue.js 和 Django 編寫的前後端項目中,實現了基於 Token 的身份驗證機制。其他前後端框架的 Token 實現原理與本文一致,但是代碼需要根據所用框架進行合理修改。