68、jwt原理、开发流程、drf-jwt快速使用 、定制返回格式 、自定义用户表签发 、自定义认证类 、签发源码分析 、认证源码

472 阅读12分钟

今日内容概要

  • jwt原理
  • jwt开发流程
  • drf-jwt快速使用
  • drf-jwt定制返回格式
  • drf-jwt自定义用户表签发
  • drf-jwt自定义认证类
  • drf-jwt的签发源码分析
  • 认证源码

今日内容详细

jwt原理

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

使用jwt认证和使用session认证的区别

基于session认证

基于session认证.jpeg

session集群.jpeg

基于jwt认证

基于jwt认证.jpeg

jwt集群.jpeg

jwt构成-三段式

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

1.header :jwt的头部承载两部分信息:
	1.声明类型:这里是jwt
	2.声明加密的算法:通常直接使用 HMAC SHA256
    1.完整的头部:
    {
      'typ': 'JWT',
      'alg': 'HS256'
    }
		2.然后将头部进行base64编码,构成了第一部分.
			eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
      
2.payload:载荷就是存放有效信息的地方。存储的信息:
		1.当前用户信息,用户名,用户id
		2.exp: jwt的过期时间,这个过期时间必须要大于签发时间

      1.定义一个payload:
        {
          "exp": "1234567890",
          "name": "John Doe",
          "user_id":99
        }
			2.然后将其进行base64编码,得到JWT的第二部分。eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
      
3.signature :签证信息,这个签证信息由三部分组成:
    1.header (base64后的)
    2.payload (base64后的)
    3.secret
    这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    """
    var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
    var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    """
		ps:将这三部分用.连接成一个完整的字符串,构成了最终的jwt:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

jwt本质原理

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

7.签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token

    1.用基本信息存储json字典,采用base64算法加密得到 头字符串
    2.用关键信息存储json字典,采用base64算法加密得到 体字符串
    3.用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串
		账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台

8.校验:根据客户端带token的请求,反解出user对象
    1.将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
    2.第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
    3.再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞

drf开发流程

1.使用jwt认证的开发流程,就是两部分
	1.第一部分:签发token的过程,登录做的
			用户携带用户名和密码,访问我,我们校验通过,生成token串,返回给前端
  2.第二部分:token认证过程,登录认证时使用,其实就是咱们之前讲的认证类,在认证类中完成对token的认证操作
    	1.用户访问我们需要登陆后才能访问的接口,必须携带我们签发的token串(请求头)
      2.我们取出token,验证该token是否过期,是否被篡改,是否是伪造的
      3.如果正常,说明荷载中的数据,就是安全的,可以根据荷载中的用户id,查询出当前登录用户,放到request中即可

drf-jwt快速使用

需求:djagno+drf框架中,使用jwt来做登录认证

1.使用第三方:
	1.django-rest-framework-jwt:https://github.com/jpadilla/django-rest-framework-jwt
	2.djangorestframework-simplejwt:https://github.com/jazzband/djangorestframework-simplejwt
  
2.我们可以自己封装 :https://gitee.com/liuqingzheng/rbac_manager/tree/master/libs/lqz_jwt

3.安装: pip3.10 install djangorestframework-jwt

4.补充:
	1.密码明文一样,每次加密后都不一样,如何做?
  	动态加盐,动态的盐要保存,跟密码保存在一起
  2.有代码,有数据库,没有登录不进去的系统--->超级用户,自动添加用户
	3.token被获取,模拟发送请求:不能篡改、不能伪造
     
5.HMACSHA256算法具有以下特点:
    1.安全性:HMACSHA256基于SHA-256哈希函数,该哈希函数在密码学中被广泛认可为安全且具有强大的抗碰撞能力。
    2.抗篡改性:HMACSHA256可以检测到消息是否被篡改,因为一旦消息被更改,生成的消息认证码也会不同。
    3.可靠性:HMACSHA256提供了一种可靠的方式来进行消息完整性验证和身份认证,而且计算速度相对较快。
    4.灵活性:HMACSHA256可以接受任意长度的密钥,并且可以处理任意长度的消息。
    总之,HMACSHA256是一种强大的消息认证码算法,通过结合哈希函数和密钥,可以实现消息完整性验证和身份认证的安全机制。它在许多应用中被广泛使用,包括网络通信、数据传输和数字签名等领域。

快速使用

签发

只需要在路由中配置

1.签发过程(快速签发),必须是auth的user表(人家帮你写好了)
	登录接口:基于auth的user表签发的
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('login/', obtain_jwt_token),  # # 登录接口就有了,并且可以签发token,如果使用auth的user表做登录,不需要写额外的代码了
]

ps:
  1.提前创建了超级用户,通过postman发送post请求,json格式,用户名密码正确,返回了token字符串
	2.可在配置文件注册INSTALLED_APPS = [ ...,'rest_framework_jwt'],并修改国际化,返回的错误提示信息为中文

认证

在视图类上加

from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated # 是否认证通过
class BookView(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # 认证类,drf-jwt提供的
    permission_classes = [IsAuthenticated]  # 权限类,drf提供的
    def get(self,request):

        return Response('你看见我了')
     

urls.py

from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from app01.views import BookView
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', obtain_jwt_token),  # # 登录接口就有了,并且可以签发token,如果使用auth的user表做登录,不需要写额外的代码了
path('books/', BookView.as_view())
]

注意:
 1.在postman的get请求中,headers请求头中key为Authorization,value值为:jwt token串--->空格
  

总结

1.签发:只需要在路由中配置
    from rest_framework_jwt.views import obtain_jwt_token
    urlpatterns = [
      path('login/', obtain_jwt_token), 
    ]
2.认证:视图类上加
    class BookView(APIView):
      authentication_classes = [JSONWebTokenAuthentication] # 认证类,drf-jwt提供的
      permission_classes = [IsAuthenticated] # 权限类,drf提供的
   ps:访问的时候,要在请求头中携带,必须叫:Authorization:jwt token串

drf-jwt定制返回格式

需求:登录签发token的接口,要返回code,msg,username,token等信息

分析

1.入口:from rest_framework_jwt.views import obtain_jwt_token-->obtain_jwt_token
2.obtain_jwt_token = ObtainJSONWebToken.as_view()-->点进ObtainJSONWebToken
3.class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer
4.点进JSONWebTokenAPIView:-->找post方法
5.class JSONWebTokenAPIView(APIView):
    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():  # 校验
            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)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
6.点进去jwt_response_payload_handler(token, user, request) -->jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER

7.根据:from .settings import api_settings 找到:'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
  def jwt_response_payload_handler(token, user=None, request=None):

      return {
          'token': token
      }

对返回格式进行定制

1.写个函数,函数返回字典格式,返回的格式,会被序列化,前端看到
2.写的函数配置一下
		JWT_AUTH = {      'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.jwt_response.common_response', }

def common_response(token,user=None,request=None):
    return {
        'code':100,
        'msg':'登陆成功',
        'username':user.username,
        'token':token,
    }
JWT_AUTH={
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app01.jwt_response.common_response',
}
from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from app01.views import BookView
urlpatterns = [
    path('admin/', admin.site.urls),
    path('login/', obtain_jwt_token),  # # 登录接口就有了,并且可以签发token,如果使用auth的user表做登录,不需要写额外的代码了
    path('books/', BookView.as_view())
]

drf-jwt自定义用户表签发

分析

1.入口:from rest_framework_jwt.views import obtain_jwt_token-->obtain_jwt_token
2.obtain_jwt_token = ObtainJSONWebToken.as_view()-->点进ObtainJSONWebToken
3.class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer
4.点进JSONWebTokenAPIView:
class JSONWebTokenAPIView(APIView):
    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()  # self从根上找,ObtainJSONWebToken类中的属性调方法
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)  # 返回
      
5.点进去ObtainJSONWebToken类中的属性调方法
class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer

class JSONWebTokenSerializer(Serializer):
    def __init__(self, *args, **kwargs):
					pass
    @property
    def username_field(self):
        return get_username_field()

    def validate(self, attrs):  # 全局钩子
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            user = authenticate(**credentials)

            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user)   # 获取payload

                return {
                    'token': jwt_encode_handler(payload),  # 获取token
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

自定义用户表签发

创建一个用户表

from django.db import models
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    age = models.IntegerField()

urls.py

from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from app01.views import BookView,UserView
urlpatterns = [
    path('admin/', admin.site.urls),
    path('books/', BookView.as_view()),
    path('login/', UserView.as_view({'post':'login'}))
]

views.py

from rest_framework.viewsets import ViewSet
from app01.models import User
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 UserView(ViewSet):
    def login(self,request):
        name = request.data.get('username')
        pwd = request.data.get('password')
        user = User.objects.filter(username=name,password=pwd).first()
        if user:
            # 1.登录成功,签发token
            payload = jwt_payload_handler(user)
            token= jwt_encode_handler(payload)
            return Response({'code': 100, 'msg': '登录成功', 'username': user.username, 'token': token})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})

drf-jwt自定义认证类

分析

drf的认证类定义方式,之前学过--->在认证类中,自己写逻辑

1.入口:
class BookView(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # 认证类,drf-jwt提供的
    permission_classes = [IsAuthenticated]  # 权限类,drf提供的

2.点进去JSONWebTokenAuthentication:from rest_framework_jwt.authentication import JSONWebTokenAuthentication
	class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
  	...
3.发现:JSONWebTokenAuthentication类中没有authenticate方法,在BaseJSONWebTokenAuthentication中找-->点进去

4.BaseJSONWebTokenAuthentication有authenticate方法
class BaseJSONWebTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        jwt_value = self.get_jwt_value(request)  # 获取token
        if jwt_value is None:
            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)

自定义认证类

urls.py

from django.contrib import admin
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from app01.views import BookView,UserView
urlpatterns = [
    path('admin/', admin.site.urls),
    # path('login/', obtain_jwt_token),  # # 登录接口就有了,并且可以签发token,如果使用auth的user表做登录,不需要写额外的代码了
    path('books/', BookView.as_view()),
    path('login/', UserView.as_view({'post':'login'}))
]

views.py

from .auth import JWTAuthentication
class BookView(APIView):
    authentication_classes = [JWTAuthentication]  # 认证类,drf-jwt提供的
    def get(self,request):

        return Response('你看见我了')

auth.py

import jwt
from rest_framework_jwt.utils import jwt_decode_handler
from  rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01.models import User
class JWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.META.get('HTTP_TOKEN')
        # 比对
        if token is None:
            return None
        try:
            payload = jwt_decode_handler(token)  # 反解密之后的payload
            # 如果认证通过,payload就可以认为是安全的,我们就可以使用
            user_id = payload.get('user_id')
            # user= User.objects.get(pk=user_id)  # 每个需要登录后,才能访问的接口,都会走这个认证类,一旦走到这个认证类,机会去数据库查询一次数据,会对数据造成压力
            # 优化:
            user= User(username=payload.get('username'),id = user_id)
        except jwt.ExpiredSignature:
            msg = ('token超时')
            raise AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = ('解码失败')
            raise AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise AuthenticationFailed()
        return (user, token)

drf-jwt的签发源码分析

1.入口:from rest_framework_jwt.views import obtain_jwt_token-->obtain_jwt_token

2.点进obtain_jwt_token

obtain_jwt_token = ObtainJSONWebToken.as_view()

ps:点进ObtainJSONWebToken

3.点进ObtainJSONWebToken

class ObtainJSONWebToken(JSONWebTokenAPIView):
    serializer_class = JSONWebTokenSerializer
    
ps:没有post方法,在父类中找JSONWebTokenAPIView

4.点进JSONWebTokenAPIView

class JSONWebTokenAPIView(APIView):
		# 局部禁用掉权限和认证
    permission_classes = ()
    authentication_classes = ()
 def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
				# serializer=JSONWebTokenSerializer(data=request.data)
        if serializer.is_valid():  # 全局钩子在校验用户,生成token
          	# 从序列化类中取出user
            user = serializer.object.get('user') or request.user
            # 从序列化类中取出token
            """
            object属性是一个字典,其中包含了从JWT中解析出的各种信息,例如token值、用户信息等
            """
            token = serializer.object.get('token')
            response_data = jwt_response_payload_handler(token, user, request)			# 定制返回格式的时候,写的就是这个函数
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
      
ps:点进去:get_serializer   

5.点进去:get_serializer--->JSONWebTokenAPIView中的方法

    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()  # JSONWebTokenSerializer()
        kwargs['context'] = self.get_serializer_context()
        return serializer_class(*args, **kwargs)

7.点进去:get_serializer_class--->JSONWebTokenAPIView中的方法

    def get_serializer_class(self):
        assert self.serializer_class is not None, (
            "'%s' should either include a `serializer_class` attribute, "
            "or override the `get_serializer_class()` method."
            % self.__class__.__name__)
        return self.serializer_class    # 从根上找:ObtainJSONWebToken中的serializer_class ---> serializer_class = JSONWebTokenSerializer

8.点进去:JSONWebTokenSerializer


class JSONWebTokenSerializer(Serializer):
    def validate(self, attrs):  # 全局钩子
      	# 前端传入的
        """
        credentials = {
            'username': attrs.get('usernme'),
            'password': attrs.get('password')
        }
        """
        credentials = {
            self.username_field: attrs.get(self.username_field),
            'password': attrs.get('password')
        }

        if all(credentials.values()):  # 校验credentials中字典的value值是否都不为空
            user = authenticate(**credentials)
						"""
						user=authenticate(username=前端传入的,password=前端传入的)
            auth模块的用户名密码认证函数,可以传入用户名密码,去auth的user表中校验用户是否存在
            等同于:User.object.filter(username=username,password=加密后的密码).first()
						"""
            if user:
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)

                payload = jwt_payload_handler(user) 

                return {
                    'token': jwt_encode_handler(payload), 
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

drf-jwt的认证源码分析

1.入口:from rest_framework_jwt.authentication import JSONWebTokenAuthentication

class BookView(APIView):
    authentication_classes = [JSONWebTokenAuthentication]  # 认证类,drf-jwt提供的
    permission_classes = [IsAuthenticated]  # 权限类,drf提供的

2.点进去JSONWebTokenAuthentication,里面没有authenticate方法,在父类中找

class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    www_authenticate_realm = 'api'

    def get_jwt_value(self, request):
        auth = get_authorization_header(request).split()  # ["JWT","token串"]
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower() # jwt

        if not auth:
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None

        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None

        if len(auth) == 1:
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        return auth[1]  # token串

    def authenticate_header(self, request):
        return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)  # ('JWT_AUTH_HEADER_PREFIX': 'JWT',api)
      
      

3.点进BaseJSONWebTokenAuthentication

class BaseJSONWebTokenAuthentication(BaseAuthentication):
    def authenticate(self, request):

        jwt_value = self.get_jwt_value(request)  # JSONWebTokenAuthentication的对象中有get_jwt_value  -->返回token串
        if jwt_value is None:
            return None
         """
        # 如果前端没传,返回None,request.user中就没有当前登录用户
        # 如果前端没有携带token,也能进入到视图类的方法中执行,控制不住登录用户
        # 所以咱们才加了个权限类,来做控制
         """
        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)


4.如果用户不携带token,也能认证通过,所以我们必须加个权限类来限制

class IsAuthenticated(BasePermission):
    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)