python-JWT(Json Web Token)-pyjwt
JWT的引入
傳統登入認證流程:
1. 使用者第一次登入時, 生成一個token並返回給前臺, 同時將其與使用者主鍵一同存在後臺伺服器上(資料庫或快取中)
2. 下一次訪問需要登入的頁面時, 將token一起傳入
3. 後臺拿著token去資料庫或快取中查詢是否存在該token, 存在則認證通過, 否則認證不通過
傳統認證的缺點:
1. token存在後臺, 增加了儲存和讀取的開銷
2. 當存在多個後臺伺服器時, 需同步共享token, 比較麻煩
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)