Django之DRF框架(五)JWT认证
文章目录
- 一、JWT简介
- 二、JWT的构成原理
- 2.1 JWT的生成
- 2.2 手动验证token
- 三、本质原理
- 四、drf-jwt的使用
- 4.1 基本使用
- 4.2 token校验
- 五、自定义响应格式及全局认证
- 5.1 jwt 配置
- 5.2 自定义认证返回结果
- 5.4 自定义基于drf-jwt的全局认证
- 六、手动签发JWT
- 6.1 编写登录视图类(支持多方式登录)
- 6.2 序列化器编写
一、JWT简介
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。
Json web token (JWT):是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

token是一种不需要在服务端的一种认证机制。比如:当客户端第一次登陆时,服务端会响应一个token值给客户端,下次访问时携带这个token值,服务端会解析出这个token值里面的数据,以此来确保是一个合法的用户才能通过认证。
二、JWT的构成原理
2.1 JWT的生成
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.连接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。
1、header头部
JWT头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。
比如:
- 声明类型:这里是jwt
- 声明加密的算法,通常直接使用 HMAC、HS256、MD5都可以
这也可以被表示成一个JSON对象
import base64
import jsonhead = {'typ': 'JWT','alg': 'MD5'x'f'f
}head_json = json.dumps(head).encode('utf-8') # base64只能加密bytes类型内容head_base64 = base64.b64encode(head_json) # 对头部head数据进行base64编码加密
然后将头部进行base64编码(该编码是可以对称解码的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2、payload载荷
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分存入客户端后,不法用户可自行解码.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解码的,意味着该部分信息可以归类为明文信息。
定义一个payload:
import base64
import jsonpayload = {"sub": "1234567890","name": "Tom","admin": True
}payload_json = json.dumps(payload).encode('utf-8')payload_base64 = base64.b64encode(payload_json)
然后将其进行base64加密,得到JWT的第二部分
eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIlRvbSIsICJhZG1pbiI6IHRydWV9
3、signature签名
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64编码加密后的)
- payload (base64编码加密后的)
- secret(秘钥、也可以称为加盐)
import hashlibmd5 = hashlib.md5()md5.update(head_base64)
md5.update(payload_base64)
md5.update(b'wmwm') # secretsignature = md5.hexdigest()token = f'{head_base64.decode("utf-8")}.{payload_base64.decode("utf-8")}.{signature}'print(token)
第三段signature签名值:
04748f59f146cdcd27e1452c4cd819e5
最终返回给我们的就是一个完整的token值
eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJNRDUifQ==.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIlRvbSIsICJhZG1pbiI6IHRydWV9.04748f59f146cdcd27e1452c4cd819e5
我们只需要将此token返回给前端,下次访问服务端时携带token,服务端会检查前前两段经过md5算法,以及指定秘钥后是否和签名值相同。
2.2 手动验证token
比如:当客户端将token携带过来以后,我们需要对其token值是否有效进行校验
import hashlib# 获取到jwt的3段值
head,payload,signature = 'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJNRDUifQ==.eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogIlRvbSIsICJhZG1pbiI6IHRydWV9.04748f59f146cdcd27e1452c4cd819e5'.split('.')# 将前两段进行md5加密,得到签证
md5 = hashlib.md5()
md5.update(head.encode('utf-8'))
md5.update(payload.encode('utf-8'))
md5.update(b'wmwm') # 使用和生成签证使用的加盐秘钥相同(尤为重要)# 校验前两段值经过md5加密以及加盐后,是否和jwt第三段值相同,如果是的话,则说明客户端携带的token值合法(与我们当时响应的相同)
if md5.hexdigest() == signature:print('认证成功')
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
三、本质原理
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": "安全码"
}
"""
签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 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就能访问登录的用户注:登录接口需要做 认证 + 权限 两个局部禁用
"""
四、drf-jwt的使用
这里我们通过jwt提供给我们的登录视图,进行自动签发token,这个视图会在我们调用api时,提供账号密码,然后去User表里面查询判断账户是否正确,然后对账号信息进行提取到dict内、以及一些其它信息,然后进行编码成一串字符,组成jwt格式。
4.1 基本使用
安装:建议使用pycharm可以安装到指定解释器
pip install djangorestframework-jwt
注意是否与Django版本兼容,不兼容的话,会出现模块导入、request.data属性相关错误
通过jwt提供的视图进行登录操作,并返回token
# 1 创建超级用户
python3 manage.py createsuperuser# 2 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [# 感兴趣的可以阅读一下obtain_jwt_token源码,里面具有签发Token的过程path('login/', obtain_jwt_token),
]
使用postman向login接口进行账号登录:可以发现生成的token。

4.2 token校验
我们定义了一个book视图类,它只允许访问时在请求头里面携带了合法的token值才能通过认证。
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthenticationfrom app01 import models
from app01 import serializerclass BookView(ModelViewSet):queryset = models.Book.objects.all()serializer_class = serializer.BookSerializer# 验证请求头里面的token是否合格,如果合格的话,返回user对象,携带的token不合格的话,则返回错误。# 注意:如果没有携带token,则该认证不会生效,也不会报错。那么我们在权限组件内做出限制。authentication_classes = [JSONWebTokenAuthentication]# 检测user对象是否为登录状态,避免了用户没有携带token就访问了此视图且可以访问成功的问题。permission_classes = [IsAuthenticated]
此时我们访问这个book视图,首先演示错误实例:

携带登录后,服务端响应给我们的token值来访问,token值的开头必须是jwt空格,因为在源码内部获取校验token值前,会先通过空格进行分隔一下,第一个值是否为jwt,如果是的话才会获取空格后面的token值来进行校验,所以我们后续会重写一些方法,不需要遵守一些不必要的规则。

五、自定义响应格式及全局认证
5.1 jwt 配置
在项目的settings.py里面对jwt进行配置
import datetime
JWT_AUTH = {# 过期时间1天'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),# 自定义认证结果:见下方序列化user和自定义response# 如果不自定义,返回的格式是固定的,只有token字段# 下面指定是我们自己定义响应方法的路径'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.jwt_test.jwt_response_payload_handler',
}
5.2 自定义认证返回结果
# 这个方法会在jwt的登录视图里面调用
def jwt_response_payload_handler(token, user=None, request=None):return {'status': 100,'msg': 'ok','data': {'token': token,'user': user.username}}
此时登录后的响应结果:

5.4 自定义基于drf-jwt的全局认证
这里我们将会对请求里面携带的token进行提取,然后进行解码,然后提取解码后的内容,也就是一些用户基本信息。
import jwtfrom rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework_jwt.authentication import get_authorization_header,jwt_decode_handler
from rest_framework.exceptions import AuthenticationFailedclass CustomJsonToken(BaseJSONWebTokenAuthentication):def authenticate(self, request):# 获取请求头里面的AUTHORIZATION参数的token值,或者我们自身规定客户端将自携带在哪里,再对应取即可jwt_value = get_authorization_header(request)if not jwt_value:raise AuthenticationFailed('Authorization 字段是必须的')try:# 通过解码token获取其在存储的用户信息,例如:ID、用户名、Email等等payload = jwt_decode_handler(jwt_value) except jwt.ExpiredSignature:raise AuthenticationFailed('签名过期')except jwt.InvalidTokenError:raise AuthenticationFailed('非法用户')# 根据解码后得到的用户信息,再通过去对User表进行查询,得到user对象(在以下方法的源码内)user = self.authenticate_credentials(payload)return user,jwt_value
当token值不正确时,将提供合理的提示信息,并且token值的参数开头不需要携带jwt空格
此时,如果需要所有视图(全局)都使用这个认证组件的话,那么我们只需要在settings.py进行DRF配置即可
REST_FRAMEWORK = {# 全局认证类"DEFAULT_AUTHENTICATION_CLASSES": ["utils.assembly.CustomJsonToken"],
}
视图配置认证组件
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticatedfrom app01 import models
from app01 import serializer
from app01.jwt_test import CustomJsonTokenfrom django.contrib.auth.models import Userclass BookView(ModelViewSet):queryset = models.Book.objects.all()serializer_class = serializer.BookSerializer'''如果这里不配置认证、权限等组件的话,则使用settings里面默认的如果我们这个视图不想进行认证(也就是不登录也可以访问)那么可以将参数配置空列表authentication_classes=[]'''authentication_classes = [CustomJsonToken]permission_classes = [IsAuthenticated]
使用效果:

认证组件返回user对象,最后都会赋值到self.user这个属性上,这也就能解释,为什么我们要返回user对象,这是为了我们后续在视图里面能够更方便知道是哪个用户访问的。至于为什么会赋值到self.user属性上,可以去了解一下笔者的DRF第一篇文章,熟悉一下APIView源码
六、手动签发JWT
这里我们自己定义登录类,当用户账号、密码输入成功后,我们手动给客户端token值。实现效果与上面差不多,但是我们能够更加熟悉jwt的token签发过程
这里的话,关于上面的一些配置就需要调整了,例如:登录视图改为我们自己的,自定义响应格式也是我们自己的视图类来控制的。
手动签发jwt需要用到的内容
# 可以拥有原生登录基于Model类user对象签发JWT
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER# 将用户对象传递进去,获取到该对象的属性值
payload = jwt_payload_handler(user)# 将属性值编码成jwt格式的字符串
token = jwt_encode_handler(payload)
6.1 编写登录视图类(支持多方式登录)
这里使用了模型里面的一个User类存储的用户信息,我们将用户输入的信息传递进入序列化器进行校验是否正确,并且在序列化器里面就生成token。
视图:将数据传递进序列化器进行校验
from rest_framework.response import Response
from rest_framework.generics import CreateAPIViewfrom app01 import modelsclass LoginView(CreateAPIView):queryset = models.User.objects.all()serializer_class = serializer.LoginSerializerauthentication_classes = []def create(self,request,*args,**kwargs):ser = self.get_serializer(data=request.data)if ser.is_valid():# 获取经过序列化器校验后的数据token = ser.validated_data.get('token')username = ser.validated_data.get('username')return Response({'status':100,'msg':"登录成功",'data':{'username':username,'token':token}})else:return Response({'status':101,'msg':ser.errors})
6.2 序列化器编写
序列化器内,对用户的登录信息进行校验,校验成功后进行Token签发
import refrom rest_framework.exceptions import ValidationErrorfrom rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLERclass LoginSerializer(serializers.Serializer):username = serializers.CharField(max_length=16)password = serializers.CharField(max_length=16)def validate(self, attrs):user = self.get_user(attrs)payload = jwt_payload_handler(user) # 获取该对象的一些属性值存储dict内返回token = jwt_encode_handler(payload) # 根据返回的dict类型用户信息进行编码成jwt格式attrs['token'] = tokenattrs['username'] = user.usernamereturn attrsdef get_user(self,attrs): # 获取用户对象,判断用户输入的是否正确,以及数据格式的校验username = attrs.get('username')password = attrs.get('password')if re.findall("^1[3-9][0-9]{9}", username):# 手机号登录user = models.User.objects.filter(phone=username, password=password).first()elif re.findall('^\d{5,10}@[a-z0-9]{1,3}\.(?:cn|com)$', username):# 邮箱登录user = models.User.objects.filter(email=username, password=password).first()else:# 用户名登录user = models.User.objects.filter(username=username, password=password).first()if user:return userelse:raise ValidationError('账号或者密码错误')
路由配置
from django.contrib import admin
from django.urls import pathfrom rest_framework.routers import SimpleRouterfrom app01 import viewsrouter = SimpleRouter()
router.register('book',views.BookView)urlpatterns = [path('admin/', admin.site.urls),path('login/', views.LoginView.as_view())
]urlpatterns += router.urls
实例效果:根据用户传递过来的账号、密码手动签发token值返回到客户端

当然,我们也可以应用于单点登录。只要定义好Token的encode、decode形式一致即可。
我们也可以自己定义token的生成,就使用我们在JWT构成原理里面的内容就可以实现。上面的模块只是给我们一种更为便捷的形式来实现Token的签发与解析。
感谢各位读者阅读完本篇文章,希望我们能在一起成长的路上追随光、成为光,照亮每一个靠近你的人!
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
