DRF JWT認證(二)
上篇中對JWT有了基本的認知,這篇來略談JWT的使用
簽發:一般我們登入成功後簽發一個token串,token串分為三段,頭部,載荷,簽名
1)用基本資訊公司資訊儲存json字典,採用base64演算法得到 頭字串
2)用關鍵資訊儲存json字典,採用base64演算法得到 荷載字串,過期時間,使用者id,使用者名稱
3)用頭、體加密字串通過加密演算法+祕鑰加密得到 簽名字串
拼接成token返回給前臺
認證:根據客戶端帶token的請求 反解出 user 物件
1)將token按 . 拆分為三段字串,第一段 頭部加密字串 一般不需要做任何處理
2)第二段 體加密字串,要反解出使用者主鍵,通過主鍵從User表中就能得到登入使用者,過期時間是安全資訊,確保token沒過期
3)再用 第一段 + 第二段 + 加密方式和祕鑰得到一個加密串,與第三段 簽名字串 進行比較,通過後才能代表第二段校驗得到的user物件就是合法的登入使用者
JWT可以使用如下兩種:
djangorestframework-jwt
和djangorestframework-simplejwt
djangorestframework-jwt:https://github.com/jpadilla/django-rest-framework-jwt
djangorestframework-simplejwt:https://github.com/jazzband/djangorestframework-simplejwt
區別:https://blog.csdn.net/lady_killer9/article/details/103075076
官網文件:https://jpadilla.github.io/django-rest-framework-jwt/
django中快速使用JWT
匯入:pip3 install djangorestframework-jwt
如何簽發?
步驟
-
路由中配置
from rest_framework_jwt.views import obtain_jwt_token urlpatterns = [ path('login/', obtain_jwt_token), ]
-
使用介面測試工具傳送post請求到後端,就能基於auth的user表簽發token
{ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTUyNDY2MiwiZW1haWwiOiIifQ.P1Y8Z3WhdndHoWE0PjW-ygd53Ng0T46U04oY8_0StwI" }
base64反解
import base64
# 第一段
s1 = b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
print(base64.b64decode(s1))
# b'{"typ":"JWT","alg":"HS256"}'
# 第二段
s2 = b'eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTUyNDY2MiwiZW1haWwiOiIifQ=='
print(base64.b64decode(s2))
# b'{"user_id":1,"username":"Hammer","exp":1649524662,"email":""}'
# 我們發現第二段可以反解密出使用者資訊,是有一定的風險,可以使用,但是不能更改,就好比你的身份證丟了,別人可以在你不掛失的情況下去網咖上網
'''第三段不能不能反解,只能做base64解碼,第三段使用base64編碼只是為了統一格式'''
如何認證?
我們沒有認證的時候,直接訪問介面就可以返回資料,比如訪問/books/
傳送GET請求就可以獲取所有book資訊,那麼現在新增認證,需要訪問通過才能訪問才更合理
步驟:
-
檢視中配置,必須配置認證類和許可權類
-
訪問需要在請求頭中使用,攜帶簽發的token串,格式是:
key是Authorization value是jwt token串 Authorization : jwt token串 '''注意jwt和token串中間有空格'''
檢視
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookView(GenericViewSet,ListModelMixin):
···
# JSONWebTokenAuthentication :rest_framework_jwt模組寫的認證類
authentication_classes = [JSONWebTokenAuthentication,]
# 需要配合一個許可權類
permission_classes = [IsAuthenticated,]
···
定製簽發token返回格式
JWT預設的配置是,我們登入成功後只返回一個token串,這也是預設的配置,我們如果想簽發token後返回更多資料需要我們自定製
步驟
- 寫一個函式,返回什麼格式,前端就能看見什麼格式
- 在配置檔案中配置
JWT_AUTH
utils.py
# 定義簽發token(登陸介面)返回格式
def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 100,
'msg': "登陸成功",
'token': token,
'username': user.username
}
settings.py
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}
JWT原始碼分析
簽發原始碼分析
1.入口:path('login/', obtain_jwt_token)
2.obtain_jwt_token--->obtain_jwt_token = ObtainJSONWebToken.as_view()
ObtainJSONWebToken.as_view(),其實就是一個檢視類.as_view()
3.ObtainJSONWebToken類原始碼
'''
class ObtainJSONWebToken(JSONWebTokenAPIView):
serializer_class = JSONWebTokenSerializer
'''
4.登入簽發token肯定需要一個post方法出來,但是ObtainJSONWebToken類內沒有父類JSONWebTokenAPIView寫了post方法:
def post(self, request, *args, **kwargs):
# 獲取資料:{'username': 'Hammer', 'password': '7410'}
serializer = self.get_serializer(data=request.data)
# 校驗
if serializer.is_valid():
user = serializer.object.get('user') or request.user # 獲取使用者
token = serializer.object.get('token') # 獲取token
response_data = jwt_response_payload_handler(token, user, request)
# {'code': 100, 'msg': '登陸成功', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTU4MTU0NiwiZW1haWwiOiIifQ.2oAjKQ90SV2S9Yxrwppo7BwAOv0xFW4i4AHHBX5Cg2Q', 'username': 'Hammer'}
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
···
return response # 定製什麼返回什麼
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
5.get_serializer(data=request.data)如何獲取到使用者資料?
JSONWebTokenSerializer序列化類中全域性鉤子中獲取當前登入使用者和簽發token
···
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
···
簽發總結
從obtain_jwt_token開始, 通過ObtainJSONWebToken檢視類處理,其實是父類JSONWebTokenAPIView的post方法通過傳入的使用者名稱和密碼處理獲取當前使用者,簽發了token
認證原始碼分析
# 檢視類內認證類搭配許可權類使用
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
我們在前面寫過,如果需要認證肯定需要重寫authenticate方法,這裡從列表內的認證類作為入口分析:
'''認證類原始碼'''
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
www_authenticate_realm = 'api'
def get_jwt_value(self, request):
# 獲取傳入的Authorization:jwt token串,然後切分
auth = get_authorization_header(request).split()
auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
# 獲取不到的情況
if not auth:
if api_settings.JWT_AUTH_COOKIE:
return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
return None # 直接返回None,也不會報錯,所以必須搭配許可權類使用
···
return auth[1] # 一切符合判斷條件,通過split切分的列表索引到token串
'''認證類父類原始碼'''
def authenticate(self, request):
jwt_value = self.get_jwt_value(request) # 獲取真正的token,三段式,上面分析
if jwt_value is None: # 如果沒傳token,就不認證了,直接通過,所以需要配合許可權類一起用
return None
try:
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)
簽發原始碼內的其他兩個類
匯入:from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token,verify_jwt_token
obtain_jwt_token = ObtainJSONWebToken.as_view() # 獲取token
refresh_jwt_token = RefreshJSONWebToken.as_view() # 更新token
verify_jwt_token = VerifyJSONWebToken.as_view() # 認證token
refresh_jwt_token
用法
# 配置檔案
JWT_AUTH = {
'JWT_ALLOW_REFRESH': True
}
# 路由
path('refresh/', refresh_jwt_token)
verify_jwt_token
用法
path('verify/', verify_jwt_token),
自定義User表,簽發token
普通寫法,檢視類寫
上面我們寫道,簽發token是基於Django自帶的auth_user
表簽發,如果我們自定義User表該如何簽發token,如下:
檢視
# 自定義表簽發token
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from app01 import models
class UserView(ViewSetMixin,APIView):
@action(methods=['POST'],detail=False)
def login(self,request):
username = request.data.get('username')
password = request.data.get('password')
user = models.UserInfo.objects.filter(username=username,password=password).first()
response_dict = {'code':None,'msg':None}
# 原始碼copy錯來使用
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
if user:
'''
簽發token去原始碼copy過來使用
'''
# 載荷字典
payload = jwt_payload_handler(user)
print(payload)
# {'user_id': 1, 'username': 'Hammer', 'exp': datetime.datetime(2022, 4, 10, 13, 13, 15, 363206), 'email': '123@qq.com', 'orig_iat': 1649596095}
# 通過荷載得到token串
token = jwt_encode_handler(payload)
response_dict['code'] = 2000
response_dict['msg'] = '登入成功'
response_dict['token'] = token
else:
response_dict['code'] = 4001
response_dict['msg'] = '登入失敗,使用者名稱或密碼錯誤'
return Response(response_dict)
模型
# user表
class UserInfo(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=32)
email = models.EmailField()
路由
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('user',views.UserView,'user')
序列化類中寫邏輯
原始碼中籤發校驗都在序列化類中完成,這種寫法確實比較常用,我們來使用這種方式自定義,將上面檢視的校驗邏輯寫到序列化類中,這個序列化類只用來做反序列化,這樣我們就可以利用 反序列化 的欄位校驗功能來幫助我們校驗(模型中的條件),但是我們不做儲存操作
檢視
from .serializer import UserInfoSerializer
class UserView(ViewSetMixin,APIView):
@action(methods=['POST'],detail=False)
def login(self,request):
# 如果想獲取什麼這裡可以例項化物件寫入,比如request
serializer = UserInfoSerializer(data=request.data, context={'request': request})
response_dict = {'code':None,'msg':None}
# 校驗,區域性鉤子,全域性鉤子都校驗完才算校驗通過,走自己的校驗規則
if serializer.is_valid():
# 從序列化器物件中獲取token和username
token = serializer.context.get('token')
username = serializer.context.get('username')
response_dict['code']=2000
response_dict['msg']='登入成功'
response_dict['token'] = token
response_dict['username'] = username
else:
response_dict['code'] = 4001
response_dict['msg'] = '登入失敗,使用者名稱或密碼錯誤'
return Response(response_dict)
序列化器
from rest_framework.exceptions import ValidationError
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
# 根據模型裡的欄位寫
fields = ['username', 'password']
# 全域性鉤子
def validate(self, attrs):
# attrs是校驗過的欄位,這裡利用
username = attrs.get('username')
password = attrs.get('password')
user = UserInfo.objects.filter(username=username, password=password).first()
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
if user: # 登入成功
payload = jwt_payload_handler(user) # 得到荷載字典
token = jwt_encode_handler(payload) # 通過荷載得到token串
# 將token放入context字典中
self.context['token'] = token
self.context['username'] = username
# context是serializer和檢視類溝通的橋樑
print(self.context.get('request').method)
else: # 登入失敗
raise ValidationError('使用者名稱或密碼錯誤')
return attrs
總結
需要我們注意的是,context
只是我們定義的字典,比如上面寫到的例項化序列化類中指定的context,那麼就可以從序列化類列印出請求的方法,context是序列化類和檢視類溝通的橋樑
自定義認證類
auth.py
import jwt
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from .models import UserInfo
class JWTAuthentication(BaseAuthentication):
def authenticate(self, request):
# 第一步、取出傳入的token,從請求頭中取
# 這裡注意,獲取的時候格式為:HTTP_請求頭的key大寫
jwt_value = request.META.get('HTTP_TOKEN')
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
# 驗證token:驗證是否過期,是否被篡改,是否有其他未知錯誤,從原始碼copy過來使用
if jwt_value:
try:
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:
msg = _('Unknown Error.')
raise exceptions.AuthenticationFailed(msg)
# 第二部、通過payload獲得當前登入使用者,本質是使用者資訊通過base64編碼到token串的第二段載荷中
user = UserInfo.objects.filter(pk=payload['user_id']).first()
# 返回user和token
return (user, jwt_value)
else:
raise AuthenticationFailed('No token was detected')
檢視
from rest_framework.viewsets import ModelViewSet
from .models import Book
from .serializer import BookSerializer
from .auth import JWTAuthentication
class BookView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
authentication_classes = [JWTAuthentication,]
序列化器
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
路由
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('book',views.BookView,'book')
正常的情況
不攜帶token的情況
總結
- 從請求頭中獲取token,格式是
HTTP_KEY
,key要大寫 - 認證token串沒有問題,返回使用者資訊從載荷中獲取,本質是使用者資訊通過base64編碼到token串的第二段載荷中,可以通過base64解碼獲取到使用者資訊
補充:HttpRequest.META
HTTP請求的資料在META中
HttpRequest.META
一個標準的Python 字典,包含所有的HTTP 首部。具體的頭部資訊取決於客戶端和伺服器,下面是一些示例:
取值:
CONTENT_LENGTH —— 請求的正文的長度(是一個字串)。
CONTENT_TYPE —— 請求的正文的MIME 型別。
HTTP_ACCEPT —— 響應可接收的Content-Type。
HTTP_ACCEPT_ENCODING —— 響應可接收的編碼。
HTTP_ACCEPT_LANGUAGE —— 響應可接收的語言。
HTTP_HOST —— 客服端傳送的HTTP Host 頭部。
HTTP_REFERER —— Referring 頁面。
HTTP_USER_AGENT —— 客戶端的user-agent 字串。
QUERY_STRING —— 單個字串形式的查詢字串(未解析過的形式)。
REMOTE_ADDR —— 客戶端的IP 地址。
REMOTE_HOST —— 客戶端的主機名。
REMOTE_USER —— 伺服器認證後的使用者。
REQUEST_METHOD —— 一個字串,例如"GET" 或"POST"。
SERVER_NAME —— 伺服器的主機名。
SERVER_PORT —— 伺服器的埠(是一個字串)。
從上面可以看到,除 CONTENT_LENGTH 和 CONTENT_TYPE 之外,請求中的任何 HTTP 首部轉換為 META 的鍵時,
都會將所有字母大寫並將連線符替換為下劃線最後加上 HTTP_ 字首。
所以,一個叫做 X-Bender 的頭部將轉換成 META 中的 HTTP_X_BENDER 鍵。
*** 有錯請指正,感謝~