Django 基礎實踐(四):JWT實現Token認證-上篇
基於Token的鑑權機制越來越多的用在了專案中,尤其是對於純後端只對外提供API沒有web頁面的專案,例如我們通常所講的前後端分離架構中的純後端服務,只提供API給前端,前端通過API提供的資料對頁面進行渲染展示或增加修改等,我們知道HTTP是一種無狀態的協議,也就是說後端服務並不知道是誰發來的請求,那麼如何校驗請求的合法性呢?這就需要通過一些方式對請求進行鑑權了
先來看看傳統的登入鑑權跟基於Token的鑑權有什麼區別
以Django的賬號密碼登入為例來說明傳統的驗證鑑權方式是怎麼工作的,當我們登入頁面輸入賬號密碼提交表單後,會發送請求給伺服器,伺服器對傳送過來的賬號密碼進行驗證鑑權,驗證鑑權通過後,把使用者資訊記錄在伺服器端(django_session表中),同時返回給瀏覽器一個sessionid用來唯一標識這個使用者,瀏覽器將sessionid儲存在cookie中,之後瀏覽器的每次請求都一併將sessionid傳送給伺服器,伺服器根據sessionid與記錄的資訊做對比以驗證身份
Token的鑑權方式就清晰很多了,客戶端用自己的賬號密碼進行登入,服務端驗證鑑權,驗證鑑權通過生成Token返回給客戶端,之後客戶端每次請求都將Token放在header裡一併傳送,服務端收到請求時校驗Token以確定訪問者身份
session的主要目的是給無狀態的HTTP協議新增狀態保持,通常在瀏覽器作為客戶端的情況下比較通用。而Token的主要目的是為了鑑權,同時又不需要考慮CSRF防護以及跨域的問題,所以更多的用在專門給第三方提供API的情況下,客戶端請求無論是瀏覽器發起還是其他的程式發起都能很好的支援。所以目前基於Token的鑑權機制幾乎已經成了前後端分離架構或者對外提供API訪問的鑑權標準,得到廣泛使用
JSON Web Token(JWT)是目前Token鑑權機制下最流行的方案,網上關於JWT的介紹有很多,這裡不細說,只講下Django如何利用JWT實現對API的認證鑑權,搜了幾乎所有的文章都是說JWT如何結合DRF使用的,如果你的專案沒有用到DRF框架,也不想僅僅為了鑑權API就引入龐大複雜的DRF框架,那麼可以接著往下看
我的需求如下:
同一個view函式既給前端頁面提供資料,又對外提供API服務,要同時滿足基於賬號密碼的驗證和JWT驗證
專案用了Django預設的許可權系統,既能對賬號密碼登入的進行許可權校驗,又能對基於JWT的請求進行許可權校驗
PyJWT介紹
要實現上邊的需求1,我們首先得引入JWT模組,python下有現成的PyJWT模組可以直接用,先看下JWT的簡單用法
安裝PyJWT
$ pip install pyjwt
複製程式碼利用PyJWT生成Token
import jwt
encoded_jwt = jwt.encode({'username':'運維咖啡吧','site':'https://ops-coffee.cn'},'secret_key',algorithm='HS256')
複製程式碼這裡傳了三部分內容給JWT,
第一部分是一個Json物件,稱為Payload,主要用來存放有效的資訊,例如使用者名稱,過期時間等等所有你想要傳遞的資訊
第二部分是一個祕鑰字串,這個祕鑰主要用在下文Signature簽名中,服務端用來校驗Token合法性,這個祕鑰只有服務端知道,不能洩露
第三部分指定了Signature簽名的演算法
檢視生成的Tokenprint(encoded_jwt)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ.fIpSXy476r9F9i7GhdYFNkd-2Ndz8uKLgJPcd84BkJ4'
複製程式碼JWT生成的Token是一個用兩個點(.)分割的長字串
點分割成的三部分分別是Header頭部,Payload負載,Signature簽名:Header.Payload.Signature
JWT是不加密的,任何人都可以讀的到其中的資訊,其中第一部分Header和第二部分Payload只是對原始輸入的資訊轉成了base64編碼,第三部分Signature是用header+payload+secret_key進行加密的結果
可以直接用base64對Header和Payload進行解碼得到相應的資訊import base64
base64.b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'
base64.b64decode('eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ==')
這裡最後加=的原因是base64解碼對傳入的引數長度不是2的物件,需要再引數最後加上一個或兩個等號=
複製程式碼因為JWT不會對結果進行加密,所以不要儲存敏感資訊在Header或者Payload中,服務端也主要依靠最後的Signature來驗證Token是否有效以及有無被篡改
解密Token
jwt.decode(encoded_jwt,'secret_key',algorithms=['HS256'])
{'username': '運維咖啡吧', 'site': 'https://ops-coffee.cn'}
複製程式碼服務端在有祕鑰的情況下可以直接對JWT生成的Token進行解密,解密成功說明Token正確,且資料沒有被篡改
當然我們前文說了JWT並沒有對資料進行加密,如果沒有secret_key也可以直接獲取到Payload裡邊的資料,只是缺少了簽名演算法無法驗證資料是否準確,pyjwt也提供了直接獲取Payload資料的方法,如下jwt.decode(encoded_jwt, verify=False)
{'username': '運維咖啡吧', 'site': 'https://ops-coffee.cn'}
複製程式碼Django案例
Django要相容session認證的方式,還需要同時支援JWT,並且兩種驗證需要共用同一套許可權系統,該如何處理呢?我們可以參考Django的解決方案:裝飾器,例如用來檢查使用者是否登入的login_required和用來檢查使用者是否有許可權的permission_required兩個裝飾器,我們可以自己實現一個裝飾器,檢查使用者的認證模式,同時認證完成後驗證使用者是否有許可權操作
於是一個auth_permission_required的裝飾器產生了:
from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
UserModel = get_user_model()
def auth_permission_required(perm):
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
# 格式化許可權
perms = (perm,) if isinstance(perm, str) else perm
if request.user.is_authenticated:
# 正常登入使用者判斷是否有許可權
if not request.user.has_perms(perms):
raise PermissionDenied
else:
try:
auth = request.META.get('HTTP_AUTHORIZATION').split()
except AttributeError:
return JsonResponse({"code": 401, "message": "No authenticate header"})
# 使用者通過API獲取資料驗證流程
if auth[0].lower() == 'token':
try:
dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=['HS256'])
username = dict.get('data').get('username')
except jwt.ExpiredSignatureError:
return JsonResponse({"status_code": 401, "message": "Token expired"})
except jwt.InvalidTokenError:
return JsonResponse({"status_code": 401, "message": "Invalid token"})
except Exception as e:
return JsonResponse({"status_code": 401, "message": "Can not get user object"})
try:
user = UserModel.objects.get(username=username)
except UserModel.DoesNotExist:
return JsonResponse({"status_code": 401, "message": "User Does not exist"})
if not user.is_active:
return JsonResponse({"status_code": 401, "message": "User inactive or deleted"})
# Token登入的使用者判斷是否有許可權
if not user.has_perms(perms):
return JsonResponse({"status_code": 403, "message": "PermissionDenied"})
else:
return JsonResponse({"status_code": 401, "message": "Not support auth type"})
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator
複製程式碼在view使用時就可以用這個裝飾器來代替原本的login_required和permission_required裝飾器了
@auth_permission_required('account.select_user')
def user(request):
if request.method == 'GET':
_jsondata = {
"user": "ops-coffee",
"site": "https://ops-coffee.cn"
}
return JsonResponse({"state": 1, "message": _jsondata})
else:
return JsonResponse({"state": 0, "message": "Request method 'POST' not supported"})
複製程式碼我們還需要一個生成使用者Token的方法,通過給User model新增一個token的靜態方法來處理
class User(AbstractBaseUser, PermissionsMixin):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新時間')
username = models.EmailField(max_length=255, unique=True, verbose_name='使用者名稱')
fullname = models.CharField(max_length=64, null=True, verbose_name='中文名')
phonenumber = models.CharField(max_length=16, null=True, unique=True, verbose_name='電話')
is_active = models.BooleanField(default=True, verbose_name='啟用狀態')
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
def __str__(self):
return self.username
@property
def token(self):
return self._generate_jwt_token()
def _generate_jwt_token(self):
token = jwt.encode({
'exp': datetime.utcnow() + timedelta(days=1),
'iat': datetime.utcnow(),
'data': {
'username': self.username
}
}, settings.SECRET_KEY, algorithm='HS256')
return token.decode('utf-8')
class Meta:
default_permissions = ()
permissions = (
("select_user", "檢視使用者"),
("change_user", "修改使用者"),
("delete_user", "刪除使用者"),
)
複製程式碼可以直接通過使用者物件來生成Token:
from accounts.models import User
u = User.objects.get(username='[email protected]')
u.token
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg3NzksImlhdCI6MTU0ODE0MjM3OSwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQDE2My5jb20ifX0.akZNU7t_z2kwPxDJjmc-QxtNdICK0yhnwWmKxqqXKLw'
複製程式碼生成的Token給到客戶端,客戶端就可以拿這個Token進行鑑權了import requests
token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg4MzgsImlhdCI6MTU0ODE0MjQzOCwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQDE2My5jb20ifX0.oKc0SafgksMT9ZIhTACupUlz49Q5kI4oJA-B8-GHqLA'r = requests.get('http://localhost/api/user', headers={'Authorization': 'Token '+token})
r.json()
{'username': '[email protected]', 'fullname': '運維咖啡吧', 'is_active': True}
複製程式碼這樣一個auth_permission_required方法就可以搞定上邊的全部需求了,簡單好用。
轉載:
作者:運維咖啡吧
連結:https://juejin.im/post/6844903765598797837