当前位置:网站首页>drf JWT認證模組與自定製
drf JWT認證模組與自定製
2020-11-06 01:29:00 【itread01】
JWT模組
在djangorestframework
中,有一款擴充套件模組可用於做JWT
認證,使用如下命令進行安裝:
pip install djangorestframework-jwt
現在,就讓我們開始使用它吧。
JWT配置
該模組的所有配置都會從settings.py
中進行讀取,與drf
一樣,它會先去讀取專案全域性資料夾下的settings.py
,再去讀取自身的settings.py
,所以如果我們要對JWT
進行配置,則在專案全域性資料夾下的settings.py
中進行配置即可:
import datetime
JWT_AUTH = {
# 配置過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 配置請求頭中攜帶token的字首
'JWT_AUTH_HEADER_PREFIX': 'JWT',
}
如果你想了解更多配置,則可檢視該模組讀取的預設配置檔案。
from rest_framework_jwt import settings
在預設配置檔案中,你可以看到如下程式碼,它會先去全域性中找配置,再到區域性中找配置:
USER_SETTINGS = getattr(settings, 'JWT_AUTH', None)
auth元件
下面將介紹如何使用auth
元件與JWT
配套使用,這當然非常方便,auth
元件可以說是Django
的核心。
我打算這樣做,對內建的user
表做擴充套件,新增頭像欄位,只有使用者登入後才能修改頭像,否則將會採用預設頭像。
準備工作
首先我們需要對內建的auth_user
表做擴充套件,如下所示:
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
avatar = models.FileField(upload_to="avatar",default="avatar/default.png")
其次配置上傳檔案的路徑,宣告media
所在的位置,以及宣告我們對內建的auth_user
表做了擴充套件:
MEDIA_ROOT = BASE_DIR / "media"
AUTH_USER_MODEL = "app01.User"
# python manage.py makemigrations
# python manage.py migrate
最後開啟資源暴露介面:
from django.contrib import admin
from django.urls import path,re_path
from django.views.static import serve
from django.conf import settings
urlpatterns = [
path('admin/', admin.site.urls),
re_path(r"^media/(?P<path>.*)", serve, {"document_root": settings.MEDIA_ROOT}),
]
註冊API
現在,我們需要來做一個註冊的API
介面,如下所示:
class Register(ViewSet):
def register(self,request,*args,**kwargs):
serializer = UserModelSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(data=serializer.data,status=status.HTTP_201_CREATED)
return Response(data=serializer.errors,status=status.HTTP_401_UNAUTHORIZED)
我們可以規定register
這個方法必須是POST
請求才能訪問,在url
中進行配置(ViewSet
是ViewSetMixin
的子類,所以有actions
引數):
path('register/', views.Register.as_view(actions={"post":"register"})),
由於auth_user
的密碼需要密文,所以我們重寫了模型序列化器的create
方法。
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from app01 import models
class UserModelSerializer(serializers.ModelSerializer):
re_password = serializers.CharField(required=True, write_only=True)
# 資料表中不存在該欄位,我們自己寫一個
class Meta:
model = models.User
fields = ("username","password","re_password","email")
extra_kwargs = {
"password":{"write_only":True}
}
def create(self, validated_data):
password = validated_data.get("password")
re_password = validated_data.get("re_password")
email = validated_data.get("email")
if re_password != password:
raise ValidationError("兩次密碼輸入不一致")
if models.User.objects.filter(email=email):
raise ValidationError("郵箱已被註冊")
validated_data.pop("re_password") # 刪除即可,然後寫入
user_obj = models.User.objects.create_user(**validated_data) # 加密建立
return user_obj
簽發token
下面將實現登入介面,如果你使用了auth
元件作為擴充套件那麼登入介面將十分的簡單。
JWT
模組已經全部幫你完成了,你只需要向下面這麼做:
from rest_framework_jwt.views import obtain_jwt_token # 匯入檢視,它都寫好了的,並且會做驗證
from rest_framework_jwt.views import ObtainJSONWebToken # 上面是一個變數,內部實際上是 obtain_jwt_token=ObtainJSONWebToken.as_view()
urlpatterns = [
path('login/', obtain_jwt_token),
# path('login/', ObtainJSONWebToken.as_view()),
]
現在,當我們向該介面傳送POST
請求時,如果校驗全部通過,則會發送給我們一個JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTYyMzcyLCJlbWFpbCI6IjIzMjNAcXEuY29tIn0._SmZ0e0mj5QVOKUftAwI3xBX4_BOw1ZNjAi94_U3mXg
JWT認證
下面我們來實現修改頭像,頭像必須先登入才能修改,所以要新增JWT
認證。
from rest_framework.permissions import IsAuthenticated # 匯入許可權
from rest_framework_jwt.authentication import JSONWebTokenAuthentication # 匯入認證
class SetAvatar(ViewSet):
authentication_classes = [JSONWebTokenAuthentication] # 儲存到request.user,如果只配置這個,則不登陸也能訪問
permission_classes = [IsAuthenticated] # 必須已經登陸,即request.user不能是匿名使用者
def set_avatar(self,request,*args,**kwargs):
serializer = UserSetAvatar(instance=request.user,data=request.FILES)
if serializer.is_valid():
serializer.save()
return Response(data="修改成功",status=status.HTTP_205_RESET_CONTENT)
return Response(data="修改失敗",status=status.HTTP_401_UNAUTHORIZED)
序列類如下:
class UserSetAvatar(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ("avatar",)
extra_kwargs = {
"avatar":{"write_only":True},
}
url
配置:
path('setavatar/', views.SetAvatar.as_view(actions={"post":"set_avatar"})),
現在,我們使用POSTMAN
來發送請求,首先要先登入,獲得JWT
:
然後需要在請求頭中新增JWT
認證,並且在body
體中新增新頭像:
需要注意的是在新增JWT
認證時,需要在VALUE
處新增字首JWT 隨機字串
,以這樣的格式提交,這是因為settings.py
中設定了字首。
最後點選send
,將會提示我們修改成功。
全域性使用
要在全域性使用JWT
認證,方式如下,它將作用於所有檢視:
REST_FRAMEWORK = {
# 認證模組
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES':{
'rest_framework.permissions.IsAuthenticated',
}
}
如果你想取消某一個檢視的認證功能,則新增空列表即可:
authentication_classes = []
permission_classes = []
區域性使用參見JWT
認證中的書寫。
JWT認證通過返回資訊定製
在上面的示例中,我們可以看見在使用者登入之後,返回資訊只有一個JWT
字串,那麼我們可不可以將已登入的使用者名稱字返回呢?也是可以的。
jwt_response_payload_handler()
這個函式就是控制返回格式的,我們可以覆寫它然後在settings.py
中進行配置。
如下所示:
def jwt_response_payload_handler(token, user=None, request=None):
return {
'status': 0,
'msg': 'ok',
'data': {
'token': token,
'user': UserModelSerializers(user).data
}
}
在settings.py
中進行配置,該項配置是配置在REST_FRAMEWORK
中,而不是JWT_AUTH
中,一定要注意:
REST_FRAMEWORK = {
# 配置自定義登入成功後的返回資訊
'JWT_RESPONSE_PAYLOAD_HANDLER':"utils.jwt_response_payload_handler",
}
最後登入完成的結果如下:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6Inl1bnlhIiwiZXhwIjoxNjA0NTY0Njk3LCJlbWFpbCI6IjIzMjNAcXEuY29tIn0.cvmM6LvoVkSQETybss3fVVGZNXT099o8U21tzDvdFe4",
"username": "yunya"
}
JWT認證流程原始碼閱讀
又開始愉快的讀原始碼環節了,那麼JWT
的原始碼還是比較簡單的,下面一起看一看。
簽發流程
首先,我們來分析一下為什麼只寫了下面一個登入介面,甚至都沒寫檢視,就可以完成簽發。
from rest_framework_jwt.views import obtain_jwt_token # 匯入檢視,它都寫好了的,並且會做驗證
from rest_framework_jwt.views import ObtainJSONWebToken # 上面是一個變數,內部實際上是 obtain_jwt_token=ObtainJSONWebToken.as_view()
urlpatterns = [
path('login/', obtain_jwt_token),
]
先看obtain_jwt_token
,可以發現這個程式碼:
obtain_jwt_token = ObtainJSONWebToken.as_view()
我們發現了一個ObtainJSONWebToken
這個類,它會執行as_view()
方法,先不管,看看它繼承了誰:
它繼承了JSONWebTokenAPIView
,並且該類又繼承了APIView
,那麼這個APIView
的檢視的原始碼已經閱讀過不下五次了,可以檢視之前的文章。我們直接來看關於登入的認證,JSONWebTOkenAPIView
中實現了一個post
方法:
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) # 可以發現它有一個自帶的序列化器
if serializer.is_valid(): # 直接進行驗證
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
現在看到序列化器對吧,序列化器其實在這裡:
class ObtainJSONWebToken(JSONWebTokenAPIView):
serializer_class = JSONWebTokenSerializer
這個是序列化器的原始碼,在__init__
方法中實現了欄位:
class JSONWebTokenSerializer(Serializer):
def __init__(self, *args, **kwargs):
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = serializers.CharField() # username欄位
self.fields['password'] = PasswordField(write_only=True) # password欄位
@property
def username_field(self):
return get_username_field()
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
user = authenticate(**credentials) # 這裡是執行認證。
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user) # 拿到荷載資訊
return {
'token': jwt_encode_handler(payload), # 荷載資訊放進去,來生成JWT的token字串
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
也就是說,在執行JSONWebTOkenAPIView
的post()
方法時,會走一次資料庫查詢,根據提交的使用者名稱和密碼來拿到使用者物件。並且在序列化器中,會生成token()
資訊。他們都是配置好的一些函式:
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
(我們可不可以自己寫一個驗證類來覆蓋它,然後用於多端登入呢?手機號,郵箱等都可以登入)
我們繼續來看post()
方法:
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) # 可以發現它有一個自帶的序列化器
if serializer.is_valid(): # 直接進行驗證
user = serializer.object.get('user') or request.user # request.user等於已經登入的使用者
token = serializer.object.get('token') # token等於生成的jwt隨機字串
response_data = jwt_response_payload_handler(token, user, request) # 設定返回資訊
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE: # 未設定,不走
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response # 直接返回
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # 沒驗證通過
OK
,自動簽發的流程已經走完了。
大概的縷一縷,它自己有序列化器,並且在序列化器中完成了JWT
字串的拼接,最後進行返回。
驗證流程
現在我們再來看一下,當用戶登入後,再次訪問的驗證流程,最開始肯定走認證:
authentication_classes = [JSONWebTokenAuthentication]
我們都知道,在APIView
中的dispatch()
方法中的initial()
方法中,會有下面這三條程式碼:
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
# 詳細的這三個程式碼的執行步驟,尤其是認證,可以檢視之前的文章
也就是先走認證,走認證時會統一執行一個叫做authenticators()
的方法。我們直接找JSONWebTokenAuthentication
中的authenticators()
方法即可。
這個類沒有authenticators()
這個方法,所以它繼承類誰呢?
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
所以我們要找的其實是BaseJSONWebTokenAuthentication
中的authenticators()
方法。
終於,找到了:
def authenticate(self, request):
jwt_value = self.get_jwt_value(request) # 獲取jwt字串,進行解析。也就是[ JWT 字串 ],這個字串的內容,排除字首,感興趣可以看看
if jwt_value is None:
return None # 如果沒有JWT驗證字串,則返回None
try: # 一系列的異常捕獲,均來自於內建的jwt模組
payload = jwt_decode_handler(jwt_value) # 解碼荷載部分
except jwt.ExpiredSignature:
msg = _('Signature has expired.') # 簽證已過期
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.') # 解碼簽證時出錯
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed() # 無效令牌
user = self.authenticate_credentials(payload) # 如果都沒出錯,則執行這裡
return (user, jwt_value) # 返回user物件,這個會賦值給reque.user,這個會jwt字串會賦值給request.auth
=================Request.user中關於許可權認證的地方
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self) # 這裡會返回一個user
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple # 進行賦值 self.user=user ,self.auth = jwt_value
return
我們來看看user
是怎麼弄出來的。它是在BaseJSONWebTokenAuthentication
類中定義的:
def authenticate_credentials(self, payload):
User = get_user_model() # 返回模型!!不是記錄物件 ,通過settings.py中的AUTH_USER_MODEL進行獲取
username = jwt_get_username_from_payload(payload) # 獲取payload中的使用者名稱
if not username: # 如果沒有使用者名稱就丟擲異常
msg = _('Invalid payload.')
raise exceptions.AuthenticationFailed(msg)
try: # 根據荷載中的使用者名稱,在模型User中試圖獲取出使用者的記錄物件,也就是說這裡會走資料庫查詢。
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist: # 如果User這個模型不存在則丟擲異常,說明
msg = _('Invalid signature.')
raise exceptions.AuthenticationFailed(msg)
if not user.is_active: # 判斷賬戶是否被禁用
msg = _('User account is disabled.')
raise exceptions.AuthenticationFailed(msg)
return user # 返回user
不用auth元件
看完了上面的原始碼分析後,我們再來想一想,如果不用auth
模組該怎麼辦?
其實也很簡單,我們自己造一個jwt
然後將token
這個隨機字串返回就好了。
認證的時候我們重寫一下認證類就好了,反正都是那一套邏輯。
手動簽發
我們的表中要有username
以及password
欄位,使用JWT
模組中生成token
的幾個函式,自己造就一個token
。
# views.py
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
from users.models import User
class LoginView(APIView): # 登入的時候簽發token
authentication_classes = []
def post(self,request):
username=request.data.get('username')
password=request.data.get('password')
user=User.objects.filter(username=username,password=password).first()
if user: # 能查到,登陸成功,手動簽發
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return CommonResponse('100','登陸成功',data={'token':token})
else:
return CommonResponse('101', '登陸失敗')
如果你想實現多端登入,手機、使用者名稱、郵箱等都能登入,這裡也有一份程式碼,只不過配合了序列化器使用,比較複雜:
# 使用使用者名稱,手機號,郵箱,都可以登入#
# 前端需要傳的資料格式
{
"username":"lqz/1332323223/[email protected]", # 使用者名稱、或者手機號、或者郵箱
"password":"lqz12345"
}
# 檢視
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin, ViewSet
from app02 import ser
class Login2View(ViewSet): # 跟上面完全一樣
def login(self, request, *args, **kwargs):
# 1 需要 有個序列化的類
login_ser = ser.LoginModelSerializer(data=request.data,context={'request':request}) # context引數類似與管道,與序列化器進行連線
# 2 生成序列化類物件
# 3 呼叫序列號物件的is_validad
login_ser.is_valid(raise_exception=True)
token=login_ser.context.get('token') # 從管道中取出token並返回
# 4 return
return Response({'status':100,'msg':'登入成功','token':token,'username':login_ser.context.get('username')})
# 序列化類
from rest_framework import serializers
from api import models
import re
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler,jwt_payload_handler
class LoginModelSerializer(serializers.ModelSerializer):
username=serializers.CharField() # 重新覆蓋username欄位,資料中它是unique,post,認為你儲存資料,自己有校驗沒過
class Meta:
model=models.User
fields=['username','password']
def validate(self, attrs):
print(self.context)
# 在這寫邏輯
username=attrs.get('username') # 使用者名稱有三種方式
password=attrs.get('password')
# 通過判斷,username資料不同,查詢欄位不一樣
# 正則匹配,如果是手機號
if re.match('^1[3-9][0-9]{9}$',username):
user=models.User.objects.filter(mobile=username).first()
elif re.match('^[email protected]+$',username):# 郵箱
user=models.User.objects.filter(email=username).first()
else:
user=models.User.objects.filter(username=username).first()
if user: # 存在使用者
# 校驗密碼,因為是密文,要用check_password
if user.check_password(password):
# 簽發token
payload = jwt_payload_handler(user) # 把user傳入,得到payload
token = jwt_encode_handler(payload) # 把payload傳入,得到token
self.context['token']=token
self.context['username']=user.username
return attrs
else:
raise ValidationError('密碼錯誤')
else:
raise ValidationError('使用者不存在')
JWT驗證
驗證的時候,繼承BaseAuthentication
類,重寫一下驗證方法:
# app_auth.py
from users.models import User
class MyJSONWebTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
jwt_value = get_authorization_header(request)
if not jwt_value:
raise AuthenticationFailed('Authorization 欄位是必須的')
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('簽名過期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法使用者')
username = jwt_get_username_from_payload(payload)
print(username)
user = User.objects.filter(username=username).first()
print(user)
return user, jwt_value
然後你的某一個檢視必須登入後才可以訪問,把他新增到認證中就OK
了。
from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
class OrderView(APIView):
# authentication_classes = [JSONWebTokenAuthentication] # 不用預設的了,用我們自己的
authentication_classes = [MyJSONWebTokenAuthentication] # 由於不是auth_user表,所以不需要判斷是否為匿名使用者。
def get(self,request):
print(request.user)
return CommonResponse('100', '成功',{'資料':'測試'})
最後我想說
1.注意字首,JWT
開頭,一個空格,後面是JWT
的token
字串
2.如果你不用auth
元件,則需要手動生成JWT
的token
字串與手動進行校驗。這很麻煩,所幸JWT
這個模組給我們很多方便之處。
3.多閱讀原始碼,有好
版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1604584335.html
边栏推荐
- 6.8 multipartresolver file upload parser (in-depth analysis of SSM and project practice)
- API 测试利器 WireMock
- 阻塞队列之LinkedBlockingQueue分析
- 字符串的常见算法总结
- TensorFlow2.0 问世,Pytorch还能否撼动老大哥地位?
- 8.2.2 inject bean (interceptor and filter) into filter through delegatingfilterproxy
- 普通算法面试已经Out啦!机器学习算法面试出炉 - kdnuggets
- python 下载模块加速实现记录
- C语言中字符字符串以及内存操作函数
- 面经手册 · 第15篇《码农会锁,synchronized 解毒,剖析源码深度分析!》
猜你喜欢
随机推荐
ES6精华:Proxy & Reflect
面经手册 · 第14篇《volatile 怎么实现的内存可见?没有 volatile 一定不可见吗?》
python 下载模块加速实现记录
python 保存list数据
mac 下常用快捷键,mac启动ftp
mac 安装hanlp,以及win下安装与使用
6.9.2 session flashmapmanager redirection management
DeepWalk模型的简介与优缺点
安装Anaconda3 后,怎样使用 Python 2.7?
业务策略、业务规则、业务流程和业务主数据之间关系 - modernanalyst
非常规聚合问题举例
C语言中字符字符串以及内存操作函数
自然语言处理-错字识别(基于Python)kenlm、pycorrector
词嵌入教程
10款好用的自动化测试工具
滴滴 Elasticsearch 集群跨版本升级与平台重构之路
PPT画成这样,述职答辩还能过吗?
API 测试利器 WireMock
二叉树的常见算法总结
架构文章搜集