• DRF JWT认证(二)


    DRF JWT认证(二)

    img

    上篇中对JWT有了基本的认知,这篇来略谈JWT的使用

    签发:一般我们登录成功后签发一个token串,token串分为三段,头部,载荷,签名

    1)用基本信息公司信息存储json字典,采用base64算法得到 头字符串
    2)用关键信息存储json字典,采用base64算法得到 荷载字符串,过期时间,用户id,用户名
    3)用头、体加密字符串通过加密算法+秘钥加密得到 签名字符串
    拼接成token返回给前台
    

    认证:根据客户端带token的请求 反解出 user 对象

    1)将token按 . 拆分为三段字符串,第一段 头部加密字符串 一般不需要做任何处理
    2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间是安全信息,确保token没过期
    3)再用 第一段 + 第二段 + 加密方式和秘钥得到一个加密串,与第三段 签名字符串 进行比较,通过后才能代表第二段校验得到的user对象就是合法的登录用户
    

    JWT可以使用如下两种:

    djangorestframework-jwtdjangorestframework-simplejwt

    djangorestframework-jwthttps://github.com/jpadilla/django-rest-framework-jwt

    djangorestframework-simplejwthttps://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

    如何签发?#

    步骤

    1. 路由中配置

      from rest_framework_jwt.views import obtain_jwt_token
      urlpatterns = [
          path('login/', obtain_jwt_token),
      ]
      
    2. 使用接口测试工具发送post请求到后端,就能基于auth的user表签发token

      {
          "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTUyNDY2MiwiZW1haWwiOiIifQ.P1Y8Z3WhdndHoWE0PjW-ygd53Ng0T46U04oY8_0StwI"
      }
      

    image

    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,]
        ···
    

    image

    定制签发token返回格式#

    JWT默认的配置是,我们登录成功后只返回一个token串,这也是默认的配置,我们如果想签发token后返回更多数据需要我们自定制

    步骤

    1. 写一个函数,返回什么格式,前端就能看见什么格式
    2. 在配置文件中配置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',
      }
    

    image


    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)
    

    image


    verify_jwt_token用法

    path('verify/', verify_jwt_token),
    

    image


    自定义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')
    

    image


    序列化类中写逻辑#

    源码中签发校验都在序列化类中完成,这种写法确实比较常用,我们来使用这种方式自定义,将上面视图的校验逻辑写到序列化类中,这个序列化类只用来做反序列化,这样我们就可以利用 反序列化 的字段校验功能来帮助我们校验(模型中的条件),但是我们不做保存操作

    视图

    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
    

    image

    总结

    需要我们注意的是,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')
    

    正常的情况

    image

    不携带token的情况

    image


    总结

    • 从请求头中获取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 键。
    

    *** 有错请指正,感谢~
  • 相关阅读:
    在Winform分页控件中集成保存用户列表显示字段及宽度调整设置
    深度学习CNN--眼睛姿态识别联练习
    基于微信公众号的图书借阅平台设计与实现
    Ubuntu 22.04 (WSL2) 安装 libssl1.1
    22年PMP考试内容大改,敏捷项目管理全套资料,不看过不了!
    Android 12 DreamCamera2 默认关闭HDR
    Confluence 内容管理
    JavaScript中if语句优化和部分语法糖小技巧推荐
    javascript的动态作用域和静态作用域
    IDEA Maven项目上传jar包到nexus仓库图文教程
  • 原文地址:https://www.cnblogs.com/48xz/p/16128119.html