JWT的引入
傳統登錄認證流程:
1. 用戶第一次登錄時, 生成一個session並返回給前台, 同時將其與用戶主鍵一同存在后台服務器上(數據庫或緩存中)
2. 下一次訪問需要登錄的頁面時, 將session一起傳入
3. 后台拿着session去數據庫或緩存中查找是否存在該session, 存在則認證通過, 否則認證不通過
傳統認證的缺點:
1. session存在后台, 增加了存儲和讀取的開銷
2. 當存在多個后台服務器時, 需同步共享session, 比較麻煩
JWT認證流程(解決了傳統認證的問題):
1. 用戶第一次登錄時, 生成一個token, 但后台不存儲該token
2. 下一次訪問需要登錄的頁面時, 將token一起傳入
3. 后台拿着token進行解析和校驗, 若解析成功則認證通過, 否則認證不通過
JWT加密原理:
生成的token分為三個部分: HEADER.PAYLOAD.SIGNATURE, 這三個部分都是可逆算法base64加密后的字符串, 最后用點號(.)拼接.如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
1. HEADER
代表了加密算法和token類型, 若不顯示指定, 默認為:
{
"alg": "HS256",
"typ": "JWT"
}
加密后結果為: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. PAYLOAD
代表了想要傳輸的業務信息和token的過期時間(可選), 例如:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 451154141
}
加密后結果為: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
3. SIGNATURE
JWT的關鍵, 其規則是將前面兩段加密后的密文再加上自定義的鹽值一起拼接后, 再通過不可逆算法HS256(具體使用的是HEADER中的算法)進行加密, 最后再對該密文進行可逆算法base64加密
鹽值(salt): 指的是加密時加入的自定義的字符串, 最好是隨機或者雜亂的字符串, 這樣更能夠確定加密后字符串的唯一性, django中可以使用settings中的SECRET_KEY
加密后結果為: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT解密原理:
拿到請求中傳過來的token后:
1. 按.號拆分token, 拿到三段值
2. 對這三段密文進行base64解密, 從明文中拿到加密算法和業務數據以及過期時間
3. 再次拼接前兩段的密文和自定義的鹽值(該鹽值必須和創建token時的鹽值一樣), 使用HEADER中的算法進行加密
4. 將加密的結果和拿到的token中的第三段解密的明文進行比較, 若完全一致則說明認證成功, 若不一致則說明token被篡改過, 認證失敗
對JWT三段式的思考:
JWT的根本思想就是將業務數據通過不可逆算法加密存儲在token中, 那么為什么要搞成三段式這么復雜呢?
直接把業務數據加上鹽值, 然后用默認不可逆算法生成一段密文字符串進行傳輸不就可以了嗎?
這樣加密時是比較簡單, 但是解密時卻由於不可逆算法而拿不到其中的業務數據, 所以確實需要再加一段式來單獨存儲業務數據
JWT又加了一段用來存儲加密算法, 能夠讓使用者自己確定具體使用什么算法進行加密, 增加了可擴展性
python中使用JWT
pyjwt
這是python使用JWT的基礎包, 在jwt官網中python語言點贊最多的就是pyjwt, 安裝方式為: pip install pyjwt , 這個包已經把加密和解密的邏輯寫好了, 我們只需要傳入加密算法/業務數據/鹽值即可
在rest_framework中使用pyjwt
定義兩個接口, 登錄(login)和查看訂單(order), 只有登錄過的用戶才能成功訪問查看訂單接口, 我們可以在登錄接口中若成功登錄則返回jwt的加密token, 在訂單接口中自定義一個認證類, 在認證類中校驗token
1. 編輯urls.py
from django.urls import path from users import views urlpatterns = [ path('login/', views.LoginView.as_view()), path('order/', views.OrderView.as_view()), ]
2. 編輯登錄和訂單視圖類
1. 登錄成功后, 調用獲取token的方法 create_token() , 傳入參數為用戶信息和token過期時間(單位: 分鍾), 默認1分鍾
2. 在訂單視圖類中設置認證類 JWTAuthentication
3. create_token和JWTAuthentication都定義在utils包的JWTAuth.py中
from rest_framework.views import APIView
from rest_framework.response import Response
from utils.JWTAuth import create_token, JWTAuthentication
class LoginView(APIView): def post(self, request, *args, **kwargs): # 獲取用戶名密碼 name = request.data.get('name') pwd = request.data.get('pwd') # 獲取User對象 try: user = models.User.objects.filter(name=name, pwd=pwd).first() except Exception: return Response({'status': 1, 'errmsg': '用戶名或密碼不正確!'}) # 獲取token token = create_token({'id': user.id, 'name': user.name}, 1) # 返回成功響應 return Response({'status': 0, 'token': token}) class OrderView(APIView): authentication_classes = [JWTAuthentication, ] def get(self, request): return Response({'status': 0, 'msg': 'ok'})
3. 編輯JWTAuth.py
import jwt from jwt import exceptions as JWTException from django.conf import settings import datetime from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed def create_token(payload, timeout=1): # 給傳過來的業務數據增加一個過期時間限制 payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout) # 定義鹽值 salt = settings.SECRET_KEY # 默認不可逆加密算法為HS256 token = jwt.encode(payload=payload, key=salt) return token class JWTAuthentication(BaseAuthentication): def authenticate(self, request): # 從url參數中獲取token token = request.query_params.get('token') # 鹽值 salt = settings.SECRET_KEY # 解碼token try: payload = jwt.decode(jwt=token, key=salt, verify=True) except JWTException.ExpiredSignature: raise AuthenticationFailed('token已失效') except jwt.DecodeError: raise AuthenticationFailed('token認證失敗') except jwt.InvalidTokenError: raise AuthenticationFailed('非法的token') return payload.get('name'), token
注意: 設置過期時間時, 一定是在payload段中設置, 且鍵名固定為'exp', 值為 datetime.datetime.utcnow() + datetime.timedelta(xxxx)