百木园-与人分享,
就是让自己快乐。

DRF JWT认证(二)

目录

  • DRF JWT认证(二)
    • django中快速使用JWT
      • 如何签发?
      • 如何认证?
      • 定制签发token返回格式
    • JWT源码分析
      • 签发源码分析
      • 认证源码分析
      • 签发源码内的其他两个类
    • 自定义User表,签发token
      • 普通写法,视图类写
      • 序列化类中写逻辑
    • 自定义认证类
    • 补充:HttpRequest.META

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-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

如何签发?

步骤

  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 键。

***
有错请指正,感谢~


来源:https://www.cnblogs.com/48xz/p/16128119.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » DRF JWT认证(二)

相关推荐

  • 暂无文章