DRF组件之JWT认证模块
JWT认证简介
jwt:json web token,是一种前后端的登录认证方式,
它分token的签发和认证,签发的意思是用户登录成功,生成三段式的token串;
认证指的是用户访问某个带有用户登录权限的接口,需要携带token串过来,完成认证。
三段式分为头、荷载和签名。
认证通过头和荷载,通过加密得到签名,然后比较两个签名是否一样,一样就通过不一样就不通过。
JWT的构成
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。
JWT三段式:头.体.签名 (head.payload.sign)
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature).
JWT的原理
1)jwt分三段式:头.体.签名 (head.payload.sign)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性的
3)头体签名三部分,都是采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息 { "company": "公司信息", ... }
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间 { "user_id": 1, ... }
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密 { "head": "头的加密字符串", "payload": "体的加密字符串", "secret_key": "安全码" }
JWT认证算法:签发+校验
签发
根据登录请求提交来的 账号 + 密码 + 设备信息 签发 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就能访问登录的用户 注:登录接口需要做 认证 + 权限 两个局部禁用
base64编码解码
import base64 import json dic_info={ "sub": "1234567890", "name": "lqz", "admin": True } byte_info=json.dumps(dic_info).encode('utf-8') # base64编码 base64_str=base64.b64encode(byte_info) print(base64_str) # base64解码 base64_str='eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9' str_url = base64.b64decode(base64_str).decode("utf-8") print(str_url)
JWT的安装
pip install djangorestframework-jwt
JWT的简单使用
新建一个项目,在模型类中继承 AbstractUser 表
# models.py from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone = models.CharField(max_length=11) icon = models.ImageField(upload_to='icon')
在配置文件中配置
# settings.py AUTH_USER_MODEL = 'api.User' # '应用名.表名'
# 一定要在执行数据库迁移命令前配置上面这条配置信息,否则会创建django的au_user表
执行数据库迁移命令并创建超级用户
# Terminal终端 # 分别执行下面两条数据库迁移命令 python manage.py makemigrations python manage.py migrate # 创建超级用户 python manage.py createsuperuser
路由配置
# urls.py from django.urls import path,re_path from rest_framework_jwt.views import ObtainJSONWebToken,RefreshJSONWebToken,VerifyJSONWebToken from rest_framework_jwt.views import obtain_jwt_token
from api import views
''' 基类:JSONWebTokenAPIView继承了APIView ObtainJSONWebToken,RefreshJSONWebToken,VerifyJSONWebToken都继承了JSONWebTokenAPIView obtain_jwt_token = ObtainJSONWebToken.as_view() refresh_jwt_token = RefreshJSONWebToken.as_view() verify_jwt_token = VerifyJSONWebToken.as_view() '''
urlpatterns = [ # path('login/', ObtainJSONWebToken.as_view()), path('login/', obtain_jwt_token), # 本质跟上面的那条路由相同
path('userinfo/',views.UserInfoView.as_view()),
path('order/',views.OrderView.as_view()), ]
postman测试
1.postman中向http://127.0.0.1:8000/api/login/发送post请求,
请求体中携带用户名和密码,即可看到生成的token
视图类书写测试接口
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated class UserInforView(APIView): # jwt认证 authentication_classes = [JSONWebTokenAuthentication,] # 权限控制 permission_classes = [IsAuthenticated,] def get(self,request,*args,**kwargs): return Response('用户信息,登录才能看') # 控制用户登录才能访问需要加上jwt认证和权限控制 # 如果用户不登录就能访问 只需要将权限控制去掉即可 class OrderView(APIView): # jwt认证 authentication_classes = [JSONWebTokenAuthentication,] # 权限控制 如果不登录就能看 去除权限控制即可 # permission_classes = [IsAuthenticated, ] def get(self,request,*args,**kwargs): return Response('订单测试,游客也能看')
在postman中访问http://127.0.0.1:8000/api/user/测试
1. 用get请求访问http://127.0.0.1:8000/api/user/ 2.在headers中加参数Authorization,对应的value值:JWT+空格+token值
只要Authorization对应的value值JWT后的token值错误,就会直接校验不通过
这样,我们就通过JWT完成了一个简单的登录校验功能,但是却存在很大的弊端
如果我们在Authorization对应的value值中不加上JWT,
JWT就会默认不对该功能进行认证校验,所以我们需要自己写一个认证类
控制登录接口返回的数据格式(两种方案)
方案一,自己写登录接口,我们自己就能控制返回数据的格式
方案二,用内置,控制登录接口返回的数据格式,具体如下:
jwt的settings.py中有这个属性: 'JWT_RESPONSE_PAYLOAD_HANDLER':'rest_framework_jwt.utils.jwt_response_payload_handler', from rest_framework_jwt.utils import jwt_response_payload_handler def my_jwt_response_payload_handler(token, user=None, request=None): return { 'token': token, } # 源代码中jwt_response_payload_handler方法只返回token, # 所以我们可以通过重写该方法,返回我们需要的数据类型
重写jwt_response_payload_handler方法
def my_jwt_response_payload_handler(token, user=None, request=None): return { 'token': token, 'msg':'登录成功', 'status':100, 'user':user.username, } # 还需要在settings.py中配置我们重写后的 # my_jwt_response_payload_handler方法
settings.py中配置
import datetime JWT_AUTH = { # 还可以配置过期时间 1天 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), # 如果不自定义,返回的格式是固定的,只有token字段 'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.my_jwt_response_payload_handler', }
再次访问登录接口,返回的数据格式就是我们自己定制的数据格式了,如下:
jwt自带的认证类必须严格按照其固定方式传参,否则就不会触发认证功能,使用起来很不方便 #jwt自带认证功能要按照jwt的规则在请求头headers中传入: # key为:Authorization # value为:JWT + 空格 + token 的形式 因此我们需要自己定义一个认证类,继承jwt的功能,同时又可以不用按照它规定的方式传参
下面,我们就自己来定义一个基于jwt的认证类:
继承 BaseJSONWebTokenAuthentication/BaseAuthentication
继承以上两个类任意其中一个都可以(本质是一样的)
# app_auth.py # 方案一:基于BaseAuthentication类 import jwt from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework_jwt.settings import api_settings from api import models jwt_decode_handler = api_settings.JWT_DECODE_HANDLER class MyAuthentication(BaseAuthentication): def authenticate(self, request): # 获取前端传的token串 从请求头还是请求地址拿由自己决定 jwt_value = request.META.get('HTTP_TOKEN') if not jwt_value: raise AuthenticationFailed('未携带token') try: # 获取荷载 直接用jwt模块提供的,缺什么导什么 payload = jwt_decode_handler(jwt_value) except jwt.ExpiredSignature: msg = 'token已过期' raise AuthenticationFailed(msg) except jwt.DecodeError: msg = 'token被篡改' raise AuthenticationFailed(msg) except jwt.InvalidTokenError: raise AuthenticationFailed('未知错误') # 获取用户对象 用自定义的User表获取对象 user = models.UserInfo.objects.filter(pk=payload['user_id']).first() # 上面的方法每次认证都要查数据库,下面有两种方法做优化,减少数据库压力 # 这是实例化得到user对象,没有去数据库查表,提高了性能,但是只能取出你传的字段数据 user=models.User(id=payload.get('user_id'),username=payload.get('username')) # 直接组织成字典,因为我们后续主要用的是用户id,视图类中按字典取值就行了 user={'id':payload.get('user_id'),'username':payload.get('username')} # 把对象和token返回 return user, jwt_value # 方案二:基于BaseJSONWebTokenAuthentication类 from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication from rest_framework.exceptions import AuthenticationFailed import jwt from rest_framework_jwt.settings import api_settings from api import models jwt_decode_handler = api_settings.JWT_DECODE_HANDLER class MyJwtAuthentication(BaseJSONWebTokenAuthentication): def authenticate(self, request): jwt_value=request.META.get('HTTP_AUTHORIZATION') if jwt_value: try: #jwt提供了通过三段token,取出payload的方法,并且有校验功能 payload=jwt_decode_handler(jwt_value) except jwt.ExpiredSignature: raise AuthenticationFailed('签名过期') except jwt.InvalidTokenError: raise AuthenticationFailed('用户非法') except Exception as e: # 所有异常都会走到这 raise AuthenticationFailed(str(e)) user=self.authenticate_credentials(payload) # 通过这个方法直接在payload取出user信息 return user,jwt_value # 没有值,直接抛异常 raise AuthenticationFailed('您没有携带认证信息')
# 上面两种方案任选一种均可
全局使用
# setting.py REST_FRAMEWORK = { # 认证模块 'DEFAULT_AUTHENTICATION_CLASSES': ( 'users.app_auth.MyAuthentication', ), }
局部启用禁用
# 局部禁用 authentication_classes = [] # 局部启用 from user.authentications import JSONWebTokenAuthentication authentication_classes = [JSONWebTokenAuthentication] # views.py 代码如下:全局已经配置了jwt认证 from user.authentications import JSONWebTokenAuthentication class UserInforView(APIView): # jwt认证 # authentication_classes = [JSONWebTokenAuthentication,]
def get(self,request,*args,**kwargs): return Response('用户信息,登录才能看') class OrderView(APIView): # jwt认证 authentication_classes = [] # 局部禁用jwt认证 def get(self,request,*args,**kwargs): return Response('订单测试,游客也能看')
1.获取用户提交的用户名和密码 2.因为用户名可能是手机、邮箱、用户名,所以用正则进行判断 3.校验成功后签发token
方式一:逻辑写在视图类中
views.py
# views.py from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin,ViewSet from rest_framework.response import Response from app02.general_serializer import LoginModelSerializer from rest_framework_jwt.settings import api_settings import re jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER # class LoginView(APIView,ViewSetMixin): class LoginView(ViewSet): # 本质跟上面的一样 # 继承ViewSetMixin扩展类可利用as_view(action={'post':'login'})进行反射 def login(self, request, *args, **kwargs): username=request.data.get('username') # 用户名有三种方式 password=request.data.get('password') import re from api import models from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler # 通过判断,username数据不同,查询字段不一样 # 正则匹配,如果是手机号 if re.match('^1[3-9][0-9]{9}$',username): user=models.User.objects.filter(mobile=username).first() elif re.match('^.+@.+$',username):# 邮箱 user=models.User.objects.filter(email=username).first() else: user=models.User.objects.filter(username=username).first() if user: # 存在用户 # 校验密码,因为是密文,要用check_password if user.check_password(password): # 签发token payload = jwt_payload_handler(user) # 把user传入,得到payload token = jwt_encode_handler(payload) # 把payload传入,得到token return Response()
方式二:逻辑写在序列化器中(推荐)
views.py
from rest_framework.views import APIView from rest_framework.viewsets import ViewSetMixin,ViewSet from rest_framework.response import Response from app02.general_serializer import LoginModelSerializer # class LoginView(APIView,ViewSetMixin): class LoginView(ViewSet): # 本质跟上面的一样 # 继承ViewSetMixin扩展类可利用as_view(action={'post':'login'})进行反射 def login(self,request,*args,**kwargs): back_dict = {'status':'100','msg':'成功'} login_ser = LoginModelSerializer(data=request.data) # login_ser.is_valid(raise_exception=True) # 或用if判断也可以 if login_ser.is_valid(): # context字典是序列化器与视图函数沟通的桥梁 # 两方都可从context中取出对方放进去的数据 token = login_ser.context.get('token') # 从序列化器内获取token username = login_ser.context.get('username') back_dict['token'] = token back_dict['username'] = username return Response(back_dict) else: back_dict['status'] = 101 back_dict['msg'] = login_ser.errors return Response(back_dict)
general_serializer.py
# general_serializer.py from api import models from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework_jwt.settings import api_settings import re jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER class LoginModelSerializer(serializers.ModelSerializer): # 这里需要重新覆盖username字段,数据库中它是unique唯一 # post请求,它会认为你新增create数据,已经有了无法新增 username = serializers.CharField() class Meta: model = models.User fields = ['username','password'] def validate(self, attrs): # username有三种方式:用户名/手机号/邮箱 username = attrs.get('username') password = attrs.get('password') # 通过正则匹配来判断,username是哪种类型的数据 if re.match('^1[0-9][0-9]{9}$',username): user_obj = models.User.objects.filter(phone=username).first() elif re.match('^.+@.+$',username): # 邮箱 user_obj = models.User.objects.filter(email=username).first() else: user_obj = models.User.objects.filter(username=username).first() # 如果用户对象存在 判断密码是否正确 if user_obj: # 密码是密文 所以需要用check_password if user_obj.check_password(password): payload = jwt_payload_handler(user_obj) # 传入user_obj得到payload token = jwt_encode_handler(payload) # 传入payload得到token self.context['token'] = token # 在context中传入token 视图类可以从context拿到 self.context['username'] = user_obj.username return attrs else: raise ValidationError('密码错误') else: raise ValidationError('用户不存在') ''' jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) # 把user传入,得到payload token = jwt_encode_handler(payload) # 把payload传入,得到token '''
urls.py
# urls.py urlpatterns = [ path('login/',views.LoginView.as_view(actions={'post':'login'})), ]
# settings.py # jwt的配置 import datetime JWT_AUTH={ 'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler', 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,手动配置 }