drf的JWT認證


JWT認證(5星)

token發展史

在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。

image

image

image

image

構成和工作原理

JWT的構成

JWT就是一段字符串,由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我們稱它為頭部(header),第二部分我們稱其為荷載(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

header(頭部)

jwt的頭部承載兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法 通常直接使用 HMAC SHA256

完整的頭部就像下面這樣的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload(荷載)

荷載就是存放類似用戶信息,過期時間,簽發時間...

{
    "userid": "1",
    "name": "John Doe",
    "exp": 1214356
}

然后將其進行base64加密,得到JWT的第二部分。

eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9

signature(簽證)

JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64解密后加密算法加密后的)
  • payload (base64解密后加密算法加密后的)
  • secret(密鑰=加鹽)

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后用base64進行加密,最后就構成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用.連接成一個完整的字符串,構成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

關於簽發和核驗JWT,我們可以使用Django REST framework JWT擴展來完成。

文檔網站:http://getblimp.github.io/django-rest-framework-jwt/

補充base64編碼解碼

import base64
import json

payload = {
    "userid": "1",
    "name": "John Doe",
    "exp": 1214356
}
json_payload = json.dumps(payload)
# 編碼
res = base64.b64encode(json_payload.encode('utf8'))

print(res)
# 解碼
res2 = json.loads(base64.b64decode(res))
print(res2)

# b'eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9'
# {'userid': '1', 'name': 'John Doe', 'exp': 1214356}

本質原理

jwt認證算法:簽發與校驗

1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓服務器可以反解出user對象;簽名是不可逆加密,保證整個token的安全性的(base64反解出的是hash加密后的密文)
3)頭體簽名三部分,都是采用json格式的字符串,進行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)頭中的內容是基本信息:公司信息、項目組信息、token采用的加密方式信息
{
	"company": "公司信息",
	...
}
5)體中的內容是關鍵信息:用戶主鍵、用戶名、簽發時客戶端信息(設備號、地址)、過期時間
{
	"user_id": 1,
	...
}
6)簽名中的內容是安全信息:頭的加密結果 + 體的加密結果 + 服務器不對外公開的安全碼(對整個字典進行md5加密)
{
	"head": "頭的加密字符串",
	"payload": "體的加密字符串",
	"secret": "安全碼"
}

簽發:根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 token

1)用基本信息存儲json字典,采用base64算法加密得到 頭字符串
2)用關鍵信息存儲json字典,采用base64算法加密得到 體字符串
3)用頭、體加密字符串再加安全碼信息存儲json字典,采用hash md5算法加密得到 簽名字符串

賬號密碼就能根據User表得到user對象,形成的三段字符串用 . 拼接成token返回給前台

校驗:根據客戶端帶token的請求 反解出 user 對象

1)將token按 . 拆分為三段字符串,第一段 頭加密字符串 一般不需要做任何處理
2)第二段 體加密字符串,要反解出用戶主鍵,通過主鍵從User表中就能得到登錄用戶,過期時間和設備信息都是安全信息,確保token沒過期,且時同一設備來的
3)再用 第一段 + 第二段 + 服務器安全碼 不可逆md5加密,與第三段 簽名字符串 進行碰撞校驗,通過后才能代表第二段校驗得到的user對象就是合法的登錄用戶

drf項目的jwt認證開發流程(重點)

1)用賬號密碼訪問登錄接口,登錄接口邏輯中調用 簽發token 算法,得到token,返回給客戶端,客戶端自己存到cookies中

2)校驗token的算法應該寫在認證類中(在認證類中調用),全局配置給認證組件,所有視圖類請求,都會進行認證校驗,所以請求帶了token,就會反解出user對象,在視圖類中用request.user就能訪問登錄的用戶

注:登錄接口需要做 認證 + 權限 兩個局部禁用

drf-jwt安裝和簡單使用(2星)

安裝

pip3 install djangorestframework-jwt

簡單使用

簽發

# 1 創建超級用戶
python3 manage.py createsuperuser
# 解釋下為什么要創建超級用戶:因為djangorestframework-jwt認證是基於django的auth里的user表作關聯的,所以驗證的數據也必須源自於這張表
# 2 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/', obtain_jwt_token),
]
# 3 postman測試
向后端接口發送post請求,攜帶用戶名密碼,即可看到生成的token

image

認證

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated

class BookAPIView(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookModelSerializer
    # 必須用這個認證類
    authentication_classes = [JSONWebTokenAuthentication, ]
    # 還要配合這個權限
    permission_classes = [IsAuthenticated, ]

在postman里

image

image

JWT使用auth表簽發token,自定制返回格式(3星)

配置setting.py

JWT_AUTH ={
    # token的過期時間
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
    # 如果不自定義,返回的格式是固定的,只有token字段
    # 這里把下面自定制的函數注冊進來
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}

自定制的py文件內

def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'code': 1000,
        'msg': '登陸成功',
        'username': user.username,
        'token': token
    }

這時登陸時返回的格式就變成了:

image

djangorestframework-jwt模塊源碼分析(2星)

簽發token

ObtainJSONWebToken.as_view()--->ObtainJSONWebToken---->post方法
 def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():  # 驗證用戶登錄和簽發token,都在序列化類的validate方法中完成的
            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)
            # 返回了咱們自定指的格式 
            '''
               {
                'code':100,
                'msg':'登錄成功',
                'username':user.username,
                'token': token,
            }
            
            '''
            return response
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    # 全局鈎子函數
     def validate(self, attrs):
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            # 根據用戶名密碼去auth的user表校驗,是否存在
            user = authenticate(**credentials)

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
				# 生成payload
                payload = jwt_payload_handler(user)

                return {
                    'token': jwt_encode_handler(payload), # 通過payload生成token
                    'user': user
                }
            else:
               # 不在拋異常,前端就看到信息了
                raise serializers.ValidationError(msg)
        else:
            raise serializers.ValidationError(msg)

image

image

image

image

image

image

image

認證

image

image

image

image

image

JWT使用自定義User表,手動簽發token,自定義認證類(5星)

簽發token

重點在於

1.通過用戶輸入的用戶名和密碼去數據庫中查出該用戶

2.獲取到的用戶信息生成荷載(payload),jwt模塊提供了

3.通過荷載來生成toekn,jwt模塊提供了

4.把含有token串的字典返回給前端

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication

jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER

class UserAPIView(ViewSet):
    @action(methods=['POST', ], detail=False)
    def login(self, request):
        back_dic = {'code': 100, 'msg': '登陸成功'}
        username = request.data.get('username')
        password = request.data.get('password')
        user = UserInfo.objects.filter(username=username, password=password).first()
        if user:
            # 獲取荷載  直接用jwt模塊提供的,缺什么導什么
            payload = jwt_payload_handler(user)
            # 獲取token串  直接用jwt模塊提供的,缺什么導什么
            token = jwt_encode_handler(payload)
            back_dic['token'] = token
            back_dic['username'] = username
        else:
            back_dic['code'] = 101
            back_dic['msg'] = '用戶名或密碼錯誤'
        return Response(back_dic)

image

自定義認證類

因為認證類要重寫authenticate方法,所以重點就是在authenticate方法中寫下面邏輯:

1.取出客戶端傳入的token(后端自己規定),看是攜帶在請求頭中,還是在請求地址中

2.驗證token中的簽名(jwt模塊提供了)

3.通過payload得到當前登陸的用戶對象(jwt模塊提供了)

4.返回user對象和token(或者是其他參數)

import jwt
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from .models import UserInfo

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class MyAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 獲取前端傳的token串
        jwt_value = request.META.get('HTTP_TOKEN')
        if not jwt_value:
            raise AuthenticationFailed('未攜帶token')
        try:
            # 獲取荷載  直接用jwt模塊提供的,缺什么導什么
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = 'token已過期'
            raise AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = 'token被篡改'
            raise AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('未知錯誤')
        # 獲取用戶對象  用自定義的User表獲取對象
        user = UserInfo.objects.filter(pk=payload['user_id']).first()
        # 上面的方法每次認證都要查數據庫,下面有兩種方法做優化,減少數據庫壓力
        # 這是實例化得到user對象,沒有去數據庫查表,提高了性能,但是只能取出你傳的字段數據
        user=User(id=payload.get('user_id'),username=payload.get('username'))
        # 直接組織成字典,因為我們后續主要用的是用戶id,視圖類中按字典取值就行了
        user={'id':payload.get('user_id'),'username':payload.get('username')}
        # 把對象和token返回
        return user, jwt_value

image

登陸邏輯寫在序列化類里(以后這種常寫)

在views.py中

class UserAPIView(ViewSet):
    @action(methods=['POST', ], detail=False)
    def login(self, request):
        back_dic = {'code': 100, 'msg': '登陸成功'}
        # 調用序列化類傳入參數獲得序列化類的對象(所有的邏輯都在全局鈎子函數里實現的)
        # 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
        ser = UserInfoSerializer(data=request.data)
        # 如果驗證通過說明已經走完字段校驗及鈎子函數
        if ser.is_valid():
            # 通過在鈎子函數中對context字典中放的數據獲取用戶名和token並返回
            username = ser.context['username']
            token = ser.context['token']
            back_dic['username'] = username
            back_dic['token'] = token
        else:
            # 返回錯誤信息
            back_dic['code'] = 101
            back_dic['msg'] = ser.errors
        return Response(back_dic)

在序列化類.py中

from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class UserInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserInfo
        fields = ['id', 'username', 'password']
	# 字段本身的校驗
    username = serializers.CharField(max_length=10, min_length=3)
    password = serializers.CharField(max_length=10, min_length=3)
	# 全局鈎子函數(核心)
    def validate(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        user = UserInfo.objects.filter(username=username, password=password).first()
        if not user:
            raise ValidationError('用戶名或密碼錯誤')
        # 獲取荷載  直接用jwt模塊提供的,缺什么導什么
        payload = jwt_payload_handler(user)
        # 獲取token  直接用jwt模塊提供的,缺什么導什么
        token = jwt_encode_handler(payload)
        # context字典是與視圖函數溝通的橋梁,這里放,那里取,那里放,這里取
        self.context['username'] = username
        self.context['token'] = token
        return attrs

補充: context字典是視圖類與序列化類溝通的橋梁

在views.py中

class UserAPIView(ViewSet):
    @action(methods=['POST', ], detail=False)
    def login(self, request):
    # 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
    ser = UserInfoSerializer(data=request.data, context={'request':request})
        if ser.is_valid():
			...

在序列化類.py中

# 如果視圖函數中傳了reqeust,也可以取出
    def validate(self, attrs):
        request = self.context['request']
        print(request.method)
		...

多功能登陸

邏輯:

1.獲取用戶提交的用戶名和密碼

2.因為用戶名可能是手機、郵箱、用戶名,所以用正則進行判斷

3.校驗成功后簽發token

普通版

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re

class UserAPIView(ViewSet):
    @action(methods=['post', ], detail=False)
    def login(self, request):
        back_dic = {'code': 100, 'msg': '登陸成功'}
        username = request.data.get('username')
        password = request.data.get('password')
        # 用正則判斷到底是哪種登陸方式
        re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
        re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
        if re_phone.search(username):
            user = UserInfo.objects.filter(phone=username, password=password).first()
        elif re_email.search(username):
            user = UserInfo.objects.filter(email=username, password=password).first()
        else:
            user = UserInfo.objects.filter(username=username, password=password).first()
        if not user:
            back_dic['code'] = 101
            back_dic['msg'] = '用戶名或密碼錯誤'
        else:
            # 獲取荷載  直接用jwt模塊提供的,缺什么導什么
            payload = jwt_payload_handler(user)
            # 獲取token  直接用jwt模塊提供的,缺什么導什么
            token = jwt_encode_handler(payload)
            back_dic['username'] = user.username
            back_dic['token'] = token
        return Response(back_dic)

進階版(邏輯寫在序列化類)

在views.py中

from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re

class UserAPIView(ViewSet):
    @action(methods=['post', ], detail=False)
    def login(self, request):
        back_dic = {'code': 100, 'msg': '登陸成功'}
        # 調用序列化類傳入參數獲得序列化類的對象(所有的邏輯都在全局鈎子函數里實現的)
        # 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
        ser = UserInfoSerializer(data=request.data)
        # 如果驗證通過說明已經走完字段校驗及鈎子函數
        if ser.is_valid():
            # 通過在鈎子函數中對context字典中放的數據獲取用戶名和token並返回
            username = ser.context['username']
            token = ser.context['token']
            back_dic['username'] = username
            back_dic['token'] = token
        else:
            back_dic['code'] = 101
            back_dic['msg'] = ser.errors
        return Response(back_dic)

在序列化類.py中

from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings
import re

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class UserInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserInfo
        fields = ['id', 'username', 'phone', 'email', 'password']
	
	# 不重寫的話會因為User表的username字段本身是unique的,所以用戶傳的username肯定在User表內存在,所以肯定會報錯
    username = serializers.CharField()
    password = serializers.CharField(max_length=10, min_length=3)
	
    # 獲取user對象的函數(拆分有助於擴展)
    def _get_user(self, attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 用正則判斷到底是哪種登陸方式
        re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
        re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
        if re_phone.search(username):
            user = UserInfo.objects.filter(phone=username, password=password).first()
        elif re_email.search(username):
            user = UserInfo.objects.filter(email=username, password=password).first()
        else:
            user = UserInfo.objects.filter(username=username, password=password).first()
        if not user:
            raise ValidationError('用戶名或密碼錯誤')
        return user

    def validate(self, attrs):
        user = self._get_user(attrs)
        # 獲取荷載  直接用jwt模塊提供的,缺什么導什么
        payload = jwt_payload_handler(user)
        # 獲取token  直接用jwt模塊提供的,缺什么導什么
        token = jwt_encode_handler(payload)
        # context字典是與視圖函數溝通的橋梁,這里放,那里取,那里放,這里取
        self.context['username'] = user.username
        self.context['token'] = token
        return attrs

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM