天天看點

Vue 和 Django 實作 Token 身份驗證

使用 Django 編寫的 B/S 應用通常會使用 Cookie + Session 的方式來做身份驗證,使用者登入資訊存儲在背景資料庫中,前端 Cookie 也會存儲少量用于身份核驗的資料,由背景直接寫入。但是在開發調試階段,使用 Postman 等請求工具請求登入時,可能會缺失前端本應存儲的資料,而導緻登入資訊核驗一直不成功。本篇介紹基于 Token 的身份驗證機制,并使用 Vue 和 Django 實作。

使用 Django 編寫的 B/S 應用通常會使用 Cookie + Session 的方式來做身份驗證,使用者登入資訊存儲在背景資料庫中,前端 Cookie 也會存儲少量用于身份核驗的資料,由背景直接寫入。但是在開發調試階段,使用 Postman 等請求工具請求登入時,可能會缺失前端本應存儲的資料,而導緻登入資訊核驗一直不成功。在本地聯調前後端時可能也會有問題。

本篇介紹基于 Token 的身份驗證機制,并使用 Vue 和 Django 實作。

基于 Token 的驗證流程

與 Session 不同的是,Token 機制不會将使用者登入資訊存儲在背景資料庫中,而是生成含有身份資訊的 Token 字元串存儲在前端中。在前端請求需要驗證的背景 API 時,後端将優先攔截并核驗身份資訊。

基于 Token 的驗證流程如下:

  1. 用戶端使用使用者名和密碼請求登入
  2. 伺服器收到請求後,驗證使用者名和密碼
  3. 驗證成功後,服務端根據使用者資訊簽發一個 Token,傳回給用戶端
  4. 用戶端存儲 Token
  5. 用戶端每次向伺服器發送其它請求時,都要攜帶 Token
  6. 伺服器收到請求,若請求的 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 實作原理與本文一緻,但是代碼需要根據所用架構進行合理修改。