DRF组件之JWT认证模块

JWT认证简介

jwt:json web token,是一种前后端的登录认证方式,

它分token的签发和认证,签发的意思是用户登录成功,生成三段式的token串;

认证指的是用户访问某个带有用户登录权限的接口,需要携带token串过来,完成认证。
三段式分为头、荷载和签名。

认证通过头和荷载,通过加密得到签名,然后比较两个签名是否一样,一样就通过不一样就不通过。

JWT的构成

JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。

JWT三段式:头.体.签名 (head.payload.sign)

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature).

JWT的原理

1)jwt分三段式:头.体.签名 (head.payload.sign)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息 { "company": "公司信息", ... }
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间 { "user_id": 1, ... }
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密 { "head": "头的加密字符串", "payload": "体的加密字符串", "secret_key": "安全码" }

JWT认证算法:签发+校验

签发

根据登录请求提交来的 账号 + 密码 + 设备信息 签发 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就能访问登录的用户 注:登录接口需要做 认证
+ 权限 两个局部禁用

base64编码解码

import base64
import json
dic_info={
  "sub": "1234567890",
  "name": "lqz",
  "admin": True
}
byte_info=json.dumps(dic_info).encode('utf-8')
# base64编码
base64_str=base64.b64encode(byte_info)
print(base64_str)
# base64解码
base64_str='eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'
str_url = base64.b64decode(base64_str).decode("utf-8")
print(str_url)

JWT的安装

pip install djangorestframework-jwt

JWT的简单使用

新建一个项目,在模型类中继承 AbstractUser 表

# models.py

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    phone = models.CharField(max_length=11)
    icon = models.ImageField(upload_to='icon')

在配置文件中配置

# settings.py

AUTH_USER_MODEL = 'api.User'  # '应用名.表名'
# 一定要在执行数据库迁移命令前配置上面这条配置信息,否则会创建django的au_user表 

执行数据库迁移命令并创建超级用户

# Terminal终端

# 分别执行下面两条数据库迁移命令
python manage.py makemigrations
python manage.py migrate

# 创建超级用户
python manage.py createsuperuser

路由配置

# urls.py

from django.urls import path,re_path
from rest_framework_jwt.views import ObtainJSONWebToken,RefreshJSONWebToken,VerifyJSONWebToken
from rest_framework_jwt.views import obtain_jwt_token
from api import views
''' 基类:JSONWebTokenAPIView继承了APIView ObtainJSONWebToken,RefreshJSONWebToken,VerifyJSONWebToken都继承了JSONWebTokenAPIView obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() '''

urlpatterns = [ # path('login/', ObtainJSONWebToken.as_view()), path('login/', obtain_jwt_token), # 本质跟上面的那条路由相同
   path('userinfo/',views.UserInfoView.as_view()),
path('order/',views.OrderView.as_view()), ]

postman测试

1.postman中向http://127.0.0.1:8000/api/login/发送post请求,
  请求体中携带用户名和密码,即可看到生成的token

视图类书写测试接口

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated

class UserInforView(APIView):
    # jwt认证
    authentication_classes = [JSONWebTokenAuthentication,]
    # 权限控制
    permission_classes = [IsAuthenticated,]
    def get(self,request,*args,**kwargs):
        return Response('用户信息,登录才能看')

# 控制用户登录才能访问需要加上jwt认证和权限控制
# 如果用户不登录就能访问 只需要将权限控制去掉即可
class OrderView(APIView):
    # jwt认证
    authentication_classes = [JSONWebTokenAuthentication,]
    # 权限控制 如果不登录就能看 去除权限控制即可
    # permission_classes = [IsAuthenticated, ]
    def get(self,request,*args,**kwargs):
        return Response('订单测试,游客也能看')

在postman中访问http://127.0.0.1:8000/api/user/测试

1. 用get请求访问http://127.0.0.1:8000/api/user/

2.在headers中加参数Authorization,对应的value值:JWT+空格+token值

 

 只要Authorization对应的value值JWT后的token值错误,就会直接校验不通过

 这样,我们就通过JWT完成了一个简单的登录校验功能,但是却存在很大的弊端

如果我们在Authorization对应的value值中不加上JWT,
JWT就会默认不对该功能进行认证校验,所以我们需要自己写一个认证类

 控制登录接口返回的数据格式(两种方案)

方案一,自己写登录接口,我们自己就能控制返回数据的格式


方案二,用内置,控制登录接口返回的数据格式,具体如下:

jwt的settings.py中有这个属性:
'JWT_RESPONSE_PAYLOAD_HANDLER':'rest_framework_jwt.utils.jwt_response_payload_handler',

from rest_framework_jwt.utils import jwt_response_payload_handler
def my_jwt_response_payload_handler(token, user=None, request=None):
  return {
  'token': token,
  }

# 源代码中jwt_response_payload_handler方法只返回token,
# 所以我们可以通过重写该方法,返回我们需要的数据类型

重写jwt_response_payload_handler方法

def my_jwt_response_payload_handler(token, user=None, request=None):

    return {
        'token': token,
        'msg':'登录成功',
        'status':100,
        'user':user.username,
    }

# 还需要在settings.py中配置我们重写后的
# my_jwt_response_payload_handler方法

settings.py中配置

import datetime

JWT_AUTH = {
    # 还可以配置过期时间 1天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),

    # 如果不自定义,返回的格式是固定的,只有token字段
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.my_jwt_response_payload_handler',
}

再次访问登录接口,返回的数据格式就是我们自己定制的数据格式了,如下:

自定义基于jwt的认证类(重点)

自定义认证类原因:

jwt自带的认证类必须严格按照其固定方式传参,否则就不会触发认证功能,使用起来很不方便

#jwt自带认证功能要按照jwt的规则在请求头headers中传入:
#                     key为:Authorization
#                     value为:JWT + 空格 + token 的形式    

因此我们需要自己定义一个认证类,继承jwt的功能,同时又可以不用按照它规定的方式传参

下面,我们就自己来定义一个基于jwt的认证类:

继承 BaseJSONWebTokenAuthentication/BaseAuthentication

继承以上两个类任意其中一个都可以(本质是一样的)

# app_auth.py

# 方案一:基于BaseAuthentication类

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


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 = models.UserInfo.objects.filter(pk=payload['user_id']).first()
        # 上面的方法每次认证都要查数据库,下面有两种方法做优化,减少数据库压力
        # 这是实例化得到user对象,没有去数据库查表,提高了性能,但是只能取出你传的字段数据
        user=models.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



# 方案二:基于BaseJSONWebTokenAuthentication类

from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
from rest_framework_jwt.settings import api_settings
from api import models

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER

class MyJwtAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        jwt_value=request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            try:
            #jwt提供了通过三段token,取出payload的方法,并且有校验功能
                payload=jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('用户非法')
            except Exception as e:
                # 所有异常都会走到这
                raise AuthenticationFailed(str(e))
            user=self.authenticate_credentials(payload)  # 通过这个方法直接在payload取出user信息
            return user,jwt_value
        # 没有值,直接抛异常
        raise AuthenticationFailed('您没有携带认证信息')


# 上面两种方案任选一种均可

全局使用

# setting.py
REST_FRAMEWORK = {
# 认证模块
'DEFAULT_AUTHENTICATION_CLASSES': (
'users.app_auth.MyAuthentication',
),
}

 局部启用禁用

# 局部禁用
authentication_classes = []
# 局部启用
from user.authentications import JSONWebTokenAuthentication
authentication_classes = [JSONWebTokenAuthentication]



# views.py 代码如下:全局已经配置了jwt认证
from user.authentications import JSONWebTokenAuthentication
class UserInforView(APIView):
    # jwt认证
    # authentication_classes = [JSONWebTokenAuthentication,]
def get(self,request,*args,**kwargs): return Response('用户信息,登录才能看') class OrderView(APIView): # jwt认证 authentication_classes = [] # 局部禁用jwt认证 def get(self,request,*args,**kwargs): return Response('订单测试,游客也能看')

手动签发token(多方式登录)

逻辑:

1.获取用户提交的用户名和密码

2.因为用户名可能是手机、邮箱、用户名,所以用正则进行判断

3.校验成功后签发token

方式一:逻辑写在视图类中

views.py

# views.py

from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin,ViewSet
from rest_framework.response import Response
from app02.general_serializer import LoginModelSerializer
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 LoginView(APIView,ViewSetMixin):
class LoginView(ViewSet):  # 本质跟上面的一样
    # 继承ViewSetMixin扩展类可利用as_view(action={'post':'login'})进行反射
    def login(self, request, *args, **kwargs):
        username=request.data.get('username') # 用户名有三种方式
        password=request.data.get('password')
        import re
        from api import models
        from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler
        # 通过判断,username数据不同,查询字段不一样
        # 正则匹配,如果是手机号
        if re.match('^1[3-9][0-9]{9}$',username):
            user=models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$',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
               return Response()

方式二:逻辑写在序列化器中(推荐)

views.py

from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin,ViewSet
from rest_framework.response import Response
from app02.general_serializer import LoginModelSerializer

# class LoginView(APIView,ViewSetMixin):
class LoginView(ViewSet):  # 本质跟上面的一样
    # 继承ViewSetMixin扩展类可利用as_view(action={'post':'login'})进行反射
    def login(self,request,*args,**kwargs):
        back_dict = {'status':'100','msg':'成功'}
        login_ser = LoginModelSerializer(data=request.data)
        # login_ser.is_valid(raise_exception=True)  # 或用if判断也可以
        if login_ser.is_valid():
            # context字典是序列化器与视图函数沟通的桥梁
            # 两方都可从context中取出对方放进去的数据
            token = login_ser.context.get('token')  # 从序列化器内获取token
            username = login_ser.context.get('username')
            back_dict['token'] = token
            back_dict['username'] = username
            return Response(back_dict)
        else:
            back_dict['status'] = 101
            back_dict['msg'] = login_ser.errors
            return Response(back_dict)

general_serializer.py

# general_serializer.py

from api import models
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 LoginModelSerializer(serializers.ModelSerializer):
    # 这里需要重新覆盖username字段,数据库中它是unique唯一
    # post请求,它会认为你新增create数据,已经有了无法新增
    username = serializers.CharField()
    class Meta:
        model = models.User
        fields = ['username','password']

    def validate(self, attrs):
        # username有三种方式:用户名/手机号/邮箱
        username = attrs.get('username')
        password = attrs.get('password')
        # 通过正则匹配来判断,username是哪种类型的数据
        if re.match('^1[0-9][0-9]{9}$',username):
            user_obj = models.User.objects.filter(phone=username).first()
        elif re.match('^.+@.+$',username):  # 邮箱
            user_obj = models.User.objects.filter(email=username).first()
        else:
            user_obj = models.User.objects.filter(username=username).first()
        # 如果用户对象存在 判断密码是否正确
        if user_obj:
            # 密码是密文 所以需要用check_password
            if user_obj.check_password(password):
                payload = jwt_payload_handler(user_obj)  # 传入user_obj得到payload
                token = jwt_encode_handler(payload)  # 传入payload得到token
                self.context['token'] = token  # 在context中传入token 视图类可以从context拿到
                self.context['username'] = user_obj.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')



'''
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user) # 把user传入,得到payload
token = jwt_encode_handler(payload) # 把payload传入,得到token

'''

urls.py

# urls.py

urlpatterns = [
    path('login/',views.LoginView.as_view(actions={'post':'login'})),
]

jwt的配置参数

# settings.py

# jwt的配置
import datetime
JWT_AUTH={
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,手动配置
}

 

posted @ 2022-05-23 13:51  _yessir  阅读(162)  评论(0编辑  收藏  举报