JWT
目录
jwt内置类JSONWebTokenAuthentication
JWT组成
jwt(java web token)本质就是一个token,是一个字符串,由三段信息组成:
header(头部) + payload(荷载) + signature(签证)
比如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
jwt定义来源优势种类: JWT详解_baobao555#的博客-CSDN博客
第一部分header
header承载两部分信息:类型(声明是jwt)和加密算法(声明加密算法,一般都是使用HMAC SHA256) 完整头部信息就比如下面的json数据: { ''type'':''JWT'', "alg":"HS256" } 然后将头部进行base64加密(该加密是可以对称解密的),就构成了第一部分header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
第二部分payload
存放有效信息的地方,有效信息包含三部分: 1 标准中注册的声明 iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。 2 公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。 3 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 比如定义一个payload { "sub": "123456789", "name": "weer", "admin": true } 然后将其进行base64加密,得到JWT的第二部分 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
第三部分signature
signature是个签证信息,由三部分构成 header(base64后的)+payload(base64后的)+secret signature将经base64加密后的header和payload连接组成字符串, 然后通过header中声明的加密算法进行加盐secret组合加密,然后就形成了signature。
将header、payload、signature连接成一个字符串就形成了最终的jwt。
注意
secret是保存在服务端的,jwt的签发认证也是在服务端进行。secret就是用来进行jwt的签发和jwt的验证,所以secret就是你服务端的私钥,在任何时候都不得流露出去。一旦客户端搞到了它意味着客户端就可以自己签发jwt了,那服务端的认证就没用了严重威胁数据安全。
JWT认证算法:签发和校验
jwt简言分为三段式:头、体、签名(header,payload,signature)。头和体是可逆加密,服务器可以反解出user对象;签名是不可逆加密,保证整个token安全性。
jwt组成的三部分都是采用json格式字符串进行加密,可逆加密一般用base64算法,不可逆加密一般采用hash(/md5)算法。
头中内容通常为基本信息:公司信息,项目组信息,token采用的加密方式信息等
体中内容是关键信息:用户主键、用户名、签发时客户端信息(ip,地址),过期时间等
签名内容为安全信息:头的加密结果,体的加密结果,服务器保密的安全码/私钥,不可逆加密
签发就是传数据信息给jwt,jwt自动给你返回一个token(jwt token);
# 签发:根据登录请求提交来的账号+密码+设备信息签发token """ (1) 用基本信息存储json字典,采用base64算法加密得到 头字符串 (2) 用关键信息存储json字典,采用base64算法加密得到 体字符串 (3) 用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串 """
校验就是jwt提供的认证方法,可以用它进行用户校验、权限校验等。
# 校验 """ (1) 将token拆分成三段字符串,第一段为头加密字符串,一般不需做任何处理 (2) 第二段 体加密字符串,要反解主键,通过表就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时间同一设备过来的 (3) 再用第一段+第二段+服务器安全码 进行不可逆md5加密,与第三段签名字符串进行碰撞校验,通过后才能代表第二段校验得到的对象,那就是合法登录用户 """
drf使用jwt
关于签发和校验,我们可以使用drf中与jwt有关的第三方包来实现
drf项目的jwt认证开发流程
"""
1、用账号密码访问登录接口,登录接口逻辑中调用,通过签发token的相关算法得到token,返回给客户端,客户端存到自己的cookie中
2、校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求都会进行认证校验,所以请求里带了token就会反解出user用户对象,在视图类中使用request.user就能访问登录的用户
注:登录接口需要做 认证+权限 两个局部禁用
"""
drf-jwt安装和简单使用
安装
pip3 install djangorestframework-jwt
简单使用
创建超级用户weer
在url.py中配置了一个登录路由
from rest_framework_jwt.views import ObtainJSONWebToken,VerifyJSONWebToken,RefreshJSONWebToken
"""
drf-jwt内置了三种jwt视图类,三种视图类都继承了基类JSONWebTokenAPIView---看源码
基类JSONWebTokenAPIView继承了APIView
"""
from rest_framework_jwt.views import obtain_jwt_token """ 源码中自动将三种jwt视图类调用as_view()方法生成对象,所以两种方式等价 obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() """ urlpatterns = [ # jwt简单使用 # path('login/', ObtainJSONWebToken.as_view()), # 与下等价 path('login/', obtain_jwt_token), ]
在postman中发post请求,带上用户名密码数据,即可得drf-jwt自动生成的jwt token:
{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJlbWFpbCI6IiIsInVzZXJuYW1lIjoid2VlciIsImVcCI6MTY2NDQ1MzU5NX0.L1Y9IAqc40X-AGcPvbvI1DDo-BD6iqYmg2BQN5zWYSU"}
drf-jwt使用
jwt内置类JSONWebTokenAuthentication
views.py
# jwt内置认证类使用——限制只有登录过的用户才能访问 from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated class OnlyLoginUserCanSeeAPIView(APIView): # 限制登录用户才能访问需要配以下两个内置类 authentication_classes = [JSONWebTokenAuthentication,] # 权限 permission_classes = [IsAuthenticated,] def get(self, request): return Response('login user') class VisitorCanSeeAPIView(APIView): # 游客/用户不登录也能访问只需将permission_classes去掉即可 authentication_classes = [JSONWebTokenAuthentication] def get(self, request): return Response('anonymous user')
urls.py
path('loginusersee/', views.OnlyLoginUserCanSeeAPIView.as_view()), path('visitorusersee/', views.VisitorCanSeeAPIView.as_view()),
此时使用postman get请求访问接口/loginusersee/必须携带登录后生成的jwt token(key为Authorization,value为JWT空格+token)才能访问到该视图函数下的信息("login user")
而访问/visitorusersee/则可以不携带jwt token信息即可访问到"anonymous user"数据
控制使用jwt的登录接口返回的数据
正常使用jwt的登录接口登录成功后只会返回一个token信息,可以配置返回其它信息 查看restframework-jwt配置文件settings.py源码: USER_SETTINGS = getattr(settings, 'JWT_AUTH', None) 说明我们要让jwt使用我们项目的配置,则需配置JWT_AUTH={…} 'JWT_RESPONSE_PAYLOAD_HANDLER': 'rest_framework_jwt.utils.jwt_response_payload_handler', 此源码为jwt内部配置的使用jwt_response_payload_handler方法返回登录成功后的信息, 点进去看该方法源码可得:内部只返回了一个token信息,且该方法会自动传user和request ∴我们可以重写jwt_response_payload_handler方法并在settings.py中配置来控制登录成功后返回的数据 utls.my_jwt_response_payload_handler.py def my_jwt_response_payload_handler(token, user=None, request=None): return { 'token': token, 'msg':'登录成功', 'name':user.username, 'method':request.method } settings.py JWT_AUTH = { 'JWT_RESPONSE_PAYLOAD_HANDLER': 'utils.my_jwt_response_payload_handler.my_jwt_response_payload_handler', } 之后登录jwt的接口成功后返回的就是我们控制的数据啦
自定制jwt认证类
发现jwt的内置认证类JSONWebTokenAuthentication是继承BaseJSONWebTokenAuthentication类的
我们要重写认证类,需继承BaseJSONWebTokenAuthentication,然后重写authenticate方法。但其实重写authenticate方法思路和逻辑和原方法差不多滴
-utls.MyJSONWebTokenAuthentication.py

from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication from rest_framework_jwt.utils import jwt_decode_handler import jwt # 内置模块,内含许多jwt异常/错误 from rest_framework.exceptions import AuthenticationFailed class MyJSONWebTokenAuthentication(BaseJSONWebTokenAuthentication): # 继承基类 def authenticate(self, request): jwt_value = request.META.get('HTTP_AUTHORIZATION') # 这样就直接带token就行了,不用JWT空格token if jwt_value: try: payload = jwt_decode_handler(jwt_value) # 还是使用的jwt的通过jwt token反解出payload的方法,需提前导入 # print(payload) #{'user_id': 1, 'email': '', 'username': 'weer',} # payload就是用户信息的字典 except jwt.ExpiredSignature: raise AuthenticationFailed('签证过期') except jwt.DecodeError: raise AuthenticationFailed('签证错误') except jwt.InvalidTokenError: raise AuthenticationFailed('用户非法') except Exception as e: raise AuthenticationFailed(str(e)) else: raise AuthenticationFailed('未携带认证信息') # 调用BaseJSONWebTokenAuthentication提供的通过payload解出user对象的方法-源码 user = self.authenticate_credentials(payload) # 也可自己设置方法-两种 # 第一种:查数据库 # from app01 import models # user = models.User.objects.filter(pk=payload.get('user_id')) # 第二种:不走数据库,创建临时对象 # from app01 import models # user = models.User(id=payload.get('user_id'), username= payload.get('username')) return user, jwt_value
-views.py

# jwt自定义认证类 from utils.MyJSONWebTokenAuthentication import MyJSONWebTokenAuthentication class OnlyLoginUserCanSeeAPIView2(APIView): # 限制登录用户才能访问需要配以下两个内置类 authentication_classes = [MyJSONWebTokenAuthentication,] permission_classes = [IsAuthenticated,] def get(self, request): return Response('login user2')
-urls.py

# jwt自定制认证类 path('loginusersee2/', views.OnlyLoginUserCanSeeAPIView2.as_view()),
也可继承BaseAuthentication写
使用jwt自动签发token+多方式登录
这里我们设置登录时不仅可由用户名密码登录,也可由电话或邮箱、密码登录,并对不同的登录方式自动签发token返回

utls.myser1.py from rest_framework import serializers from app01 import models import re from rest_framework.exceptions import ValidationError from rest_framework_jwt.utils import jwt_payload_handler,jwt_encode_handler class ManyWaysToLoginSerializers(serializers.ModelSerializer): username = serializers.CharField() # 重覆盖原来的username字段,否则原来的校验都过不了,会报原用户已存在的错,因为原username字段设置了unique=True,post请求认为你是保存数据就会报错 或命名为其它名字 class Meta: model = models.User fields = ['username', 'password'] # 在这写多种方式登录的校验逻辑 # 要校验两个,用全局钩子 def validate(self, attrs): # print(self.context) #{'test': 'test'}由视图传来,检验context这个交互媒介 # 获取登录数据 username = attrs.get('username') password = attrs.get('password') # 判断登录方式是哪种分别处理,用正则匹配,获取用户对象 if re.match('^1[3-9][0-9]{9}$',username): user_obj = models.User.objects.filter(phone=username).first() elif re.match('[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+',username): user_obj = models.User.objects.filter(email=username).first() else: user_obj = models.User.objects.filter(username=username).first() # 校验用户是否存在及密码 if user_obj: # 校验密码 if user_obj.check_password(password): # 密码是密文,用check_password # 签发token payload = jwt_payload_handler(user_obj) token = jwt_encode_handler(payload) self.context['username'] = user_obj.username # 如何把token传到视图函数中呢?→ self.context['token'] = token # 全局钩子校验完后必须返回所有数据 return attrs else: raise ValidationError('密码错误') else: raise ValidationError('用户不存在') views.py # from rest_framework.viewsets import ViewSetMixin # from rest_framework.views import APIView from rest_framework.viewsets import ViewSet from utils import myser1 # class ManyWaysToLogin(ViewSetMixin,APIView):等价 class ManyWaysToLogin(ViewSet): def login(self, request): # 1 需要一个序列化类,生成序列化对象 ser_obj = myser1.ManyWaysToLoginSerializers(data=request.data, context={'test':'test'}) # 2 在序列化类中写多方式登录校验逻辑 # 3 调用序列化累is_validated方法,校验过了才往下执行 ser_obj.is_valid(raise_exception=True) # 4 获取token token = ser_obj.context.get('token') # 5 返回数据 return Response({'status':100, 'user':ser_obj.context.get('username'), 'msg':'登录成功', 'token':token}) urls.py path('manywayslogin/', views.ManyWaysToLogin.as_view({'post':'login'})),
此时利用post发送post请求,用三种方式都能登录了
查看序列化基类源码(BaseSerializer),里面初始化__init__时就有一参数
def __init__(self, instance=None, data=empty, **kwargs):
self.instance = instance
if data is not empty:
self.initial_data = data
self.partial = kwargs.pop('partial', False)
self._context = kwargs.pop('context', {})
kwargs.pop('many', None)
super().__init__(**kwargs)
就是它提供了context,我们可以利用它作为媒介在里面放参数供序列化类和视图之间交互数据
如myser1.py中打印的{'test':'test'}就是由views.py中传来,views.py中的token就是myser1.py中传来
配置jwt token的过期时间
drf-jwt的settings.py中配置的过期时间是300s,要想更改,在我们项目的settings.py中配置如下: import datetime JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 配置jwt token过期时间为7天 }