第十篇:DRF之实现JWT认证
第十篇:DRF之实现JWT认证
一、JWT的构成
在用户注册或登录之后,我们想要记录用户的登录状态,或者为用户创建身份认证的凭证,我们不在使用Session
认证机制,而是使用Json Web Token
(本质就是token)认证机制。
JWT(Json Web Token)就是一段字符串,由三段信息构成的,将这三段信息文本用.
连接在一起就构成了JWT字符串。类似于如下。
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjI3Mjk2ODc4LCJlbWFpbCI6IiJ9.dR5BTVVTGKmMxYCktBqLOqNZrl2sgf_htm_sgsrlPqA"
第一部分我们成为头部(header),第二部分称其为载荷(payload),第三部分是签发(signature)。
二、JWT认证图
三、JWT工作原理
-
jwt分三段式:头.体.签名 (header.payload.signature)。
-
头和体使用base64进行可逆转码(不是加密,以为可以反解),让服务器可以反解出user对象,而签名是不可逆加密,从而保证整个token的安全性。
-
头体签名三部分,都是采用json格式的字符串,进行加密,可逆转码使用base64算法,不可逆加密使用md5算法。
-
头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息。
{ "company": "公司信息", ... }
-
体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间【每次登录请求发来的token都不一样,因为过期时间不同】.
{ "user_id": 1, ... }
-
签名中的内容是安全信息:头的转码结果 + 体的转码结果 + 服务器不对外公开的安全密钥,将其全部进行md5(或其他方式)加密。
{ "head": "头的转码字符串", "payload": "体的转码字符串", "secret_key": "安全密钥" }
四、JWT校验流程
我们需要将客户端发来的token数据进行校验,来验证用户的身份。具体步骤如下。
- 将token按照
.
拆分成三段字符串,第一段是头加密字符串,我们一般不需要做任何处理。 - 第二段是体加密字符串,我们需要反接出用户主键,通过主键从User表中就能得到登录用户。而过期时间和设备信息都属于安全信息,是为了确保token没过期,和来自同一设备的。
- 再将 第一段 + 第二段 + 服务器安全密钥,使用md5进行不可逆加密,与得到的第三段签名字符串进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户。
五、DRF项目中实现jwt认证
"""步骤"""
1、用账号密码访问登录接口,登录接口逻辑中调用签发token的算法,得到token
返回给客户端,客户端自己存到cookies中。
2、校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,
所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,
在视图类中用request.user就能访问登录的用户。
"""补充"""
登录接口不能做 认证和权限校验,必须进行局部禁用。
1、安装djangorestframework-jwt
第三方应用。
pip3 install djangorestframework-jwt
2、创建一个项目,模型表中的用户表,继承AbstractUser,重写auth_user表。
"""settings.py"""
# 配置AUTH_USER_MODEL
AUTH_USER_MODEL = 'app.userinfo'
3、创建超级用户。
username: admin
password: admin123
4、简单使用。
"""urls.py"""
# jwt相关
from rest_framework_jwt.views import ObtainJSONWebToken, VerifyJSONWebToken, RefreshJSONWebToken, obtain_jwt_token
urlpatterns = [
# jwt相关
url(r'^login/', obtain_jwt_token),
# 登录之后测试其他视图类
url(r'^test/', views.Test.as_view())
]
- 全局配置
"""settings.py"""
# 全局配置jwt认证
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# 'rest_framework_jwt.authentication.JSONWebTokenAuthentication'
]
}
- 局部配置
# 测试视图类
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class Test(APIView):
authentication_classes = [JSONWebTokenAuthentication]
# 必须进行权限校验才可以生效
permission_classes = [IsAuthenticated]
...
自定义实现jwt认证
"""auth.py"""
from rest_framework_jwt.authentication import BaseAuthentication, BaseJSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework import exceptions
class MyToken(BaseJSONWebTokenAuthentication):
def authenticate(self, request):
# 拿到get请求在请求头中的AUTHORIZATION键对应的token值
jwt_value = str(request.META.get('HTTP_AUTHORIZATION'))
print(jwt_value) # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjI3Mjk5MzUzLCJlbWFpbCI6IiJ9.hBnpxweihHhaNWSrwjCvpbJx2YDpiAS2pzUhX0teD9o
# 认证
try:
# 将token值的第二段转化成一个用户信息【没有密码】 还认证是否篡改,是否过期
payload = jwt_decode_handler(jwt_value)
print(payload) # {'user_id': 1, 'username': 'admin', 'exp': 1627299353, 'email': ''}
except Exception:
raise exceptions.AuthenticationFailed("认证失败")
# 根据payload得到用户对象
user = self.authenticate_credentials(payload)
print(user) # admin 用户对象
# 将用户对象返回
return user, None
"""views.py"""
from app.auth import MyToken
# 测试视图类
class Test(APIView):
authentication_classes = [MyToken]
# 如何进行token认证
def get(self, request):
print(request.user) # admin 拿到用户对象
print(type(request.user)) # <class 'app.models.UserInfo'>
return Response('经过token校验,才能显示。')
最终效果如下所示。
我们访问登录路由。
然后我们访问测试路由,用来验证jwt认证是否生效。
如果输入错误token,则验证失败,效果如下。
六、控制用户登录才能访问和不登陆就能访问
- 路由配置
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
# 登录接口测试
url(r'^login/', obtain_jwt_token),
# 订单视图测试
url(r'^order1/', views.OrderView1.as_view()),
url(r'^order2/', views.OrderView2.as_view())
]
- views.py
"""用户登录才能访问"""
class OrderView1(APIView):
# 导入系统默认的认证类
authentication_classes = [JSONWebTokenAuthentication]
# 判断是否通过认证校验
permission_classes = [IsAuthenticated]
# 视图
def get(self, request, *args, **kwargs):
return Response('这是测试的响应1。')
"""用户不登陆就能访问"""
class OrderView2(APIView):
# 如此不生效,因为权限没有验证认证是否通过
authentication_classes = [JSONWebTokenAuthentication]
# permission_classes = [IsAuthenticated]
# 视图
def get(self, request, *args, **kwargs):
return Response('这是测试的响应2。')
用户登录才能访问,效果如下所示。
访问登录才能访问的视图。
访问未登录就能访问的视图。
七、控制登录接口返回的数据
1、第一种:自己写登录接口
2、第二种:使用内置提供的登录接口返回需要的数据格式
我们分析源码,发现jwt的配置中存在这个属性,用来控制登录返回的数据格式。
from rest_framework_jwt import settings
from rest_framework_jwt.utils import jwt_response_payload_handler
我在新建的utils.py
文件中书写如下代码。
def my_jwt_response_payload_handler(token, user=None, request=None): # 返回什么,前端就能看到什么样子
return {
'token': token,
'msg': '登录成功',
'status': 100,
'username': user.username,
'password': user.password
}
然后再全局进行JWT配置。
# JWT的配置
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.utils.my_jwt_response_payload_handler'
}
即可实现如下效果。
八、多方式登录手动签发token
在很多情况下,我们发现有的网站可以使用多种方式进行登录,比如用户名、手机号或者邮箱,那么是如何实现的呢?
前端一般需要传递的数据格式是
{
"username":"admin/1345679845/11@qq.com",
"password":"admin123"
}
具体代码如下所示。
- urls.py
urlpatterns = [
# 多方式登陆接口测试
url(r'^login2/', views.LoginView2.as_view(actions={'post': 'login'}))
]
- ser.py
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler
from api import models
import re
# 多用户登录的序列化类
class LoginModelSerializer(serializers.ModelSerializer):
"""重新覆盖username字段,数据中username是unique,登录是post提交,默认认为是保存数据,如果有则校验不通过"""
username = serializers.CharField()
class Meta:
model = models.UserInfo
fields = ['username', 'password']
# 使用全局钩子进行校验
def validate(self, attrs):
"""从视图函数传过来的数据,可以放在context字典中,用来实现视图函数和序列化组件之间的交互"""
print(self.context) # {'request': <rest_framework.request.Request object at 0x000001A63185CCC0>}
"""在序列化中书写逻辑,当然,也可以在视图函数中书写逻辑"""
# username中有三种类型
username = attrs.get('username')
print(username, type(username)) # 1345679845
password = attrs.get('password')
print(password) # admin123
"""使用正则进行用户名、手机号、邮箱之间的校验"""
# 如果是手机号
if re.match('^1[3-9][0-9]{9}$', username):
# 拿到用户对象
user_obj = models.UserInfo.objects.filter(phone=username).first()
# 如果是邮箱[正则随便写的]
elif re.match('^.+@.+$', username):
user_obj = models.UserInfo.objects.filter(email=username).first()
else:
user_obj = models.UserInfo.objects.filter(username=username).first()
# 如果用户存在,判断密码是否正确
if user_obj:
# 校验密码,因为是密文,要用check_password
if user_obj.check_password(password):
"""签发token"""
# 将用户对象传入,生成payload
payload = jwt_payload_handler(user_obj)
# 将payload传入,生成token
token = jwt_encode_handler(payload)
# 将token数据传入context中,实现与视图函数的交互
self.context['token'] = token
self.context['username'] = user_obj.username
return attrs
else:
raise ValidationError('密码错误')
else:
raise ValidationError('该用户不存在')
- views.py
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
# ViewSet = ViewSetMixin + APIView
from rest_framework.viewsets import ViewSet
from api import ser
class LoginView2(ViewSet):
def login(self, request, *args, **kwargs):
# 对传过来的数据进行序列化,生成序列化对象
login_ser = ser.LoginModelSerializer(data=request.data, context={'request': request})
# 调用序列化对象的is_valid
login_ser.is_valid(raise_exception=True)
# 从序列化类中的context中拿到数据
token = login_ser.context.get('token')
# 返回相应的数据
return Response(data={
'status': 100,
'msg': '登陆成功',
'token': token,
'username': login_ser.context.get('username')
})
九、配置Token过期时间
import datetime
# JWT的配置
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'api.utils.my_jwt_response_payload_handler',
# 将token的过期时间配置为7天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7)
}
十、base64使用
"""介绍"""
base64: 可变长,可反解
md5: 固定长度,不可反解
"""base64的编码和解码"""
import base64
import json
dic = {'name': 'yangyi', 'age': 18}
"""编码"""
# 先将数据类型转化成json格式的字符串
res = json.dumps(dic)
# TypeError: a bytes-like object is required, not 'dict'
res1 = base64.b64encode(res.encode('utf-8'))
print(res1) # b'eyJuYW1lIjogInlhbmd5aSIsICJhZ2UiOiAxOH0='
"""解码"""
res2 = base64.b64decode(res1)
print(res2) # b'{"name": "yangyi", "age": 18}'