第十篇:DRF之实现JWT认证

第十篇:DRF之实现JWT认证

一、JWT的构成

在用户注册或登录之后,我们想要记录用户的登录状态,或者为用户创建身份认证的凭证,我们不在使用Session认证机制,而是使用Json Web Token(本质就是token)认证机制。

JWT(Json Web Token)就是一段字符串,由三段信息构成的,将这三段信息文本用.连接在一起就构成了JWT字符串。类似于如下。

"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjI3Mjk2ODc4LCJlbWFpbCI6IiJ9.dR5BTVVTGKmMxYCktBqLOqNZrl2sgf_htm_sgsrlPqA"

第一部分我们成为头部(header),第二部分称其为载荷(payload),第三部分是签发(signature)。

二、JWT认证图

三、JWT工作原理

  1. jwt分三段式:头.体.签名 (header.payload.signature)。

  2. 头和体使用base64进行可逆转码(不是加密,以为可以反解),让服务器可以反解出user对象,而签名是不可逆加密,从而保证整个token的安全性。

  3. 头体签名三部分,都是采用json格式的字符串,进行加密,可逆转码使用base64算法,不可逆加密使用md5算法。

  4. 头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息。

    {
        "company": "公司信息",
        ...
    }
    
  5. 体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间【每次登录请求发来的token都不一样,因为过期时间不同】.

    {
    	"user_id": 1,
    	...
    }
    
  6. 签名中的内容是安全信息:头的转码结果 + 体的转码结果 + 服务器不对外公开的安全密钥,将其全部进行md5(或其他方式)加密。

    {
    	"head": "头的转码字符串",
    	"payload": "体的转码字符串",
    	"secret_key": "安全密钥"
    }
    

四、JWT校验流程

我们需要将客户端发来的token数据进行校验,来验证用户的身份。具体步骤如下。

  1. 将token按照.拆分成三段字符串,第一段是头加密字符串,我们一般不需要做任何处理。
  2. 第二段是体加密字符串,我们需要反接出用户主键,通过主键从User表中就能得到登录用户。而过期时间和设备信息都属于安全信息,是为了确保token没过期,和来自同一设备的。
  3. 再将 第一段 + 第二段 + 服务器安全密钥,使用md5进行不可逆加密,与得到的第三段签名字符串进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户。

五、DRF项目中实现jwt认证

"""步骤"""
1、用账号密码访问登录接口,登录接口逻辑中调用签发token的算法,得到token
   返回给客户端,客户端自己存到cookies中。
2、校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,
   所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,
   在视图类中用request.user就能访问登录的用户。

"""补充"""
登录接口不能做 认证和权限校验,必须进行局部禁用。

1、安装djangorestframework-jwt第三方应用。

pip3 install djangorestframework-jwt

2、创建一个项目,模型表中的用户表,继承AbstractUser,重写auth_user表。

"""settings.py"""
# 配置AUTH_USER_MODEL
AUTH_USER_MODEL = 'app.userinfo'

3、创建超级用户。

username: admin
password: admin123

4、简单使用。

"""urls.py"""
# jwt相关
from rest_framework_jwt.views import ObtainJSONWebToken, VerifyJSONWebToken, RefreshJSONWebToken, obtain_jwt_token

urlpatterns = [
    # jwt相关
    url(r'^login/', obtain_jwt_token),
    # 登录之后测试其他视图类
    url(r'^test/', views.Test.as_view())
]
  • 全局配置
"""settings.py"""
# 全局配置jwt认证
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        # 'rest_framework_jwt.authentication.JSONWebTokenAuthentication'
    ]
}
  • 局部配置
# 测试视图类
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class Test(APIView):
    authentication_classes = [JSONWebTokenAuthentication]
    # 必须进行权限校验才可以生效
    permission_classes = [IsAuthenticated]
    ...

自定义实现jwt认证

"""auth.py"""
from rest_framework_jwt.authentication import BaseAuthentication, BaseJSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework import exceptions

class MyToken(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # 拿到get请求在请求头中的AUTHORIZATION键对应的token值
        jwt_value = str(request.META.get('HTTP_AUTHORIZATION'))
        print(jwt_value)  # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjI3Mjk5MzUzLCJlbWFpbCI6IiJ9.hBnpxweihHhaNWSrwjCvpbJx2YDpiAS2pzUhX0teD9o
        # 认证
        try:
            # 将token值的第二段转化成一个用户信息【没有密码】 还认证是否篡改,是否过期
            payload = jwt_decode_handler(jwt_value)
            print(payload)  # {'user_id': 1, 'username': 'admin', 'exp': 1627299353, 'email': ''}

        except Exception:
            raise exceptions.AuthenticationFailed("认证失败")
        # 根据payload得到用户对象
        user = self.authenticate_credentials(payload)
        print(user)  # admin 用户对象
        # 将用户对象返回
        return user, None
"""views.py"""
from app.auth import MyToken

# 测试视图类
class Test(APIView):
    authentication_classes = [MyToken]
    # 如何进行token认证
    def get(self, request):
        print(request.user)  # admin 拿到用户对象
        print(type(request.user))  # <class 'app.models.UserInfo'>
        return Response('经过token校验,才能显示。')

最终效果如下所示。
我们访问登录路由。

然后我们访问测试路由,用来验证jwt认证是否生效。

如果输入错误token,则验证失败,效果如下。

六、控制用户登录才能访问和不登陆就能访问

  • 路由配置
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    # 登录接口测试
    url(r'^login/', obtain_jwt_token),
    # 订单视图测试
    url(r'^order1/', views.OrderView1.as_view()),
    url(r'^order2/', views.OrderView2.as_view())
]
  • views.py
"""用户登录才能访问"""
class OrderView1(APIView):
    # 导入系统默认的认证类
    authentication_classes = [JSONWebTokenAuthentication]
    # 判断是否通过认证校验
    permission_classes = [IsAuthenticated]
    # 视图
    def get(self, request, *args, **kwargs):
        return Response('这是测试的响应1。')


"""用户不登陆就能访问"""
class OrderView2(APIView):
    # 如此不生效,因为权限没有验证认证是否通过
    authentication_classes = [JSONWebTokenAuthentication]
    # permission_classes = [IsAuthenticated]
    # 视图
    def get(self, request, *args, **kwargs):
        return Response('这是测试的响应2。')

用户登录才能访问,效果如下所示。

访问登录才能访问的视图。

访问未登录就能访问的视图。

七、控制登录接口返回的数据

1、第一种:自己写登录接口

2、第二种:使用内置提供的登录接口返回需要的数据格式

我们分析源码,发现jwt的配置中存在这个属性,用来控制登录返回的数据格式。

from rest_framework_jwt import settings
from rest_framework_jwt.utils import jwt_response_payload_handler

我在新建的utils.py文件中书写如下代码。

def my_jwt_response_payload_handler(token, user=None, request=None): # 返回什么,前端就能看到什么样子
    return {
        'token': token,
        'msg': '登录成功',
        'status': 100,
        'username': user.username,
        'password': user.password
    }

然后再全局进行JWT配置。

# JWT的配置
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.utils.my_jwt_response_payload_handler'
}

即可实现如下效果。

八、多方式登录手动签发token

在很多情况下,我们发现有的网站可以使用多种方式进行登录,比如用户名、手机号或者邮箱,那么是如何实现的呢?

前端一般需要传递的数据格式是

{
    "username":"admin/1345679845/11@qq.com",
    "password":"admin123"
}

具体代码如下所示。

  • urls.py
urlpatterns = [
    # 多方式登陆接口测试
    url(r'^login2/', views.LoginView2.as_view(actions={'post': 'login'}))
]
  • ser.py
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler
from api import models
import re


# 多用户登录的序列化类
class LoginModelSerializer(serializers.ModelSerializer):
    """重新覆盖username字段,数据中username是unique,登录是post提交,默认认为是保存数据,如果有则校验不通过"""
    username = serializers.CharField()

    class Meta:
        model = models.UserInfo
        fields = ['username', 'password']

    # 使用全局钩子进行校验
    def validate(self, attrs):
        """从视图函数传过来的数据,可以放在context字典中,用来实现视图函数和序列化组件之间的交互"""
        print(self.context)  # {'request': <rest_framework.request.Request object at 0x000001A63185CCC0>}
        """在序列化中书写逻辑,当然,也可以在视图函数中书写逻辑"""
        # username中有三种类型
        username = attrs.get('username')
        print(username, type(username))  # 1345679845
        password = attrs.get('password')
        print(password)  # admin123
        """使用正则进行用户名、手机号、邮箱之间的校验"""
        # 如果是手机号
        if re.match('^1[3-9][0-9]{9}$', username):
            # 拿到用户对象
            user_obj = models.UserInfo.objects.filter(phone=username).first()
        # 如果是邮箱[正则随便写的]
        elif re.match('^.+@.+$', username):
            user_obj = models.UserInfo.objects.filter(email=username).first()
        else:
            user_obj = models.UserInfo.objects.filter(username=username).first()
        # 如果用户存在,判断密码是否正确
        if user_obj:
            # 校验密码,因为是密文,要用check_password
            if user_obj.check_password(password):
                """签发token"""
                # 将用户对象传入,生成payload
                payload = jwt_payload_handler(user_obj)
                # 将payload传入,生成token
                token = jwt_encode_handler(payload)
                # 将token数据传入context中,实现与视图函数的交互
                self.context['token'] = token
                self.context['username'] = user_obj.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('该用户不存在')
  • views.py
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
# ViewSet = ViewSetMixin + APIView
from rest_framework.viewsets import ViewSet
from api import ser

class LoginView2(ViewSet):
    def login(self, request, *args, **kwargs):
        # 对传过来的数据进行序列化,生成序列化对象
        login_ser = ser.LoginModelSerializer(data=request.data, context={'request': request})
        # 调用序列化对象的is_valid
        login_ser.is_valid(raise_exception=True)
        # 从序列化类中的context中拿到数据
        token = login_ser.context.get('token')
        # 返回相应的数据
        return Response(data={
            'status': 100,
            'msg': '登陆成功',
            'token': token,
            'username': login_ser.context.get('username')
        })

九、配置Token过期时间

import datetime
# JWT的配置
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.utils.my_jwt_response_payload_handler',
    # 将token的过期时间配置为7天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7)
}

十、base64使用

"""介绍"""
base64: 可变长,可反解
md5: 固定长度,不可反解

"""base64的编码和解码"""
import base64
import json

dic = {'name': 'yangyi', 'age': 18}
"""编码"""
# 先将数据类型转化成json格式的字符串
res = json.dumps(dic)
# TypeError: a bytes-like object is required, not 'dict'
res1 = base64.b64encode(res.encode('utf-8'))
print(res1)  # b'eyJuYW1lIjogInlhbmd5aSIsICJhZ2UiOiAxOH0='

"""解码"""
res2 = base64.b64decode(res1)
print(res2)  # b'{"name": "yangyi", "age": 18}'
posted @ 2021-07-26 19:54  YangYi215  阅读(1265)  评论(0编辑  收藏  举报