Django REST framework-JWT认证
JWT 认证
官网:https://github.com/jpadilla/django-rest-framework-jwt
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT 原理分析
1 token:
- 加密的串:有三段 Header.Payload.Signature
- 头.载荷(数据).签名
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。
就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
- 使用bsse64转码
- 第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
JWT 认证流程
JWT如何保证安全
- 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。
- 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
- 后端检查是否存在,如存在验证JWT的有效性。例如:检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
JWT 开发流程
"""
1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中
2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以
请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户
注:登录接口需要做 认证 + 权限 两个局部禁用
"""
JWT 优势
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何违背形式都支持。 不需要在服务端保存会话信息,特别适用于分布式微服务。
JWT的构成
构成
1,头部(Header)
2,载荷(Payload)
3,签名(Signature)
因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature
Header
一般存放如何处理token的方式:加密的算法、是否有签名等
标头通常由两部分组成:
-
声明类型,这里是JWT
-
声明加密的算法 通常直接使用 ,例如:HMAC SHA256和RSA。
它会使用Base64编码组成JWT结构的第一部分。 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
完整的头部就像下面这样的JSON:
{
“typ”:“JWT”
"alg":“HS256”,
}
Payload
数据的主体部分:用户信息、发行者、过期时间等
令牌的第二部分是有效载荷,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分
{
"sub":“1234567890”,
“name”:“John Doe”,
“admin”:true
}
Signature
将header、payload再结合密码盐整体处理一下
-
前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使用编码后的header和payload以及我们提供的一个密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被篡改过
-
如: HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret);
签名目的
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形式的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
base64的使用
加码和解码
base64转码跟语言无关,任何语言都能编码,解码base64
bas64长度必须是4的倍数,如果不足,要用=补齐,最多使用===等号
# 导入python内置模块base64
import base64
import json
dic = {'id': 1, 'name': 'mzk'}
str_dic=json.dumps(dic)
print(str_dic)
# base64加码
res=base64.b64encode(str_dic.encode('utf-8'))
print(res)
## base64解码
# res=base64.b64decode(b'eyJpZCI6IDEsICJuYW1lIjogImxxeiJ9')
# res=base64.b64decode(b'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9')
# bas64长度必须是4的倍数,如果不足,要用=补齐
res=base64.b64decode(b'eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImxxeiIsImV4cCI6MTYxOTY2OTA1Mn0=')
# print(len('eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImxxeiIsImV4cCI6MTYxOTY2OTA1Mn0='))
print(res)
JWT 自动签发token和自动认证
1 借助于djangorestframework-jwt实现
# 安装
pip3 install djangorestframework-jwt
2 默认用的是auth的user表,可以在models.py 扩写auth的user表
from django.db import models
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
phone = models.BigIntegerField(null=True)
# 扩写auth的user表需要在settings.py 配置中配置
AUTH_USER_MODEL = "app01.UserInfo"
3 创建超级用户
python3 manage.py createsuperuser
4 不需要写登录功能(按照官方文档要求设置路由)
路由 urls.py(登录功能,djangorestframework-jwt已经写好了登录):
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path('login/', obtain_jwt_token),
]
5 也不需要写认证类,用postman测试
接口:http://127.0.0.1:8000/login/
向后端接口发送post请求,携带用户名密码,即可看到生成的token
postman发送访问请求(必须带jwt空格)
6 djangorestframework-jwt也有配置文件,也有默认配置,默认过期时间是5分钟
# 在settings.py配置中配置
import datetime
JWT_AUTH = {
# 过期时间1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 自定义认证结果:见下方自定义response(自定义响应格式)
# 如果不自定义,返回的格式是固定的,只有token字段
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.response.response_user_login',
}
JWT使用JWT内置的认证类,限制接口
### 限制某个接口必须登录才能用(加载views.py视图类中)
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
# 如果使用jwt内置的认证类,需要配合一个权限类(局部使用)
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated,]
# 如果只加了JSONWebTokenAuthentication,登录可以用,不登录也可以用
# 请求路径(需要在请求头中带)
# Authorization: jwt asdfasdfasfasdfasdtoken串
http://127.0.0.1:8000/api/books/ get请求
# 全局使用 在setting.py配置文件中配置(需要配合一个权限类)
REST_FRAMEWORK = {
# 认证模块
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
# 权限模块
"DEFAULT_PERMISSION_CLASSES":(
'rest_framework.permissions.IsAuthenticated',
),
}
JWTsetting.py配置文件中配置参数
JWT_AUTH = {
# Token编码方法
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
# Token解码方法
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
# payload添加
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
# 获取user_id
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
# 登录后payload返回
'JWT_RESPONSE_PAYLOAD_HANDLER':
'User_set.utils.JWTResponse.jwt_response_payload_handler',
'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
# 算法
'JWT_ALGORITHM': 'HS256',
# 开启验证
'JWT_VERIFY': True,
# 开启验证过期时间
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
# 一天后Token过期
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
# 开启Token更新
'JWT_ALLOW_REFRESH': True,
# 一天后刷新的Token过期
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 默认Token前缀
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_AUTH_COOKIE': None,
}
JWT自动签发token,自定义响应格式
# 自定义一个response.py文件,写一个函数
def response_user_login(token, user=None, request=None):
print(request.method)
return {
'status':100,
'msg':'登录成功',
'username':user.username,
'token':token
}
#在settings.py 配置文件中配置
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.response.response_user_login',
}
JWT手动签发token
自定义登录功能
自己定义的user表,手写登录功能
from rest_framework.response import Response
from rest_framework.views import APIView
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 UserLoginViwe(APIView):
def post(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
user = models.User.objects.filter(username=username, password=password).first()
if user:
# 签发token
payload = jwt_payload_handler(user) # 先拿到载荷 payload
token=jwt_encode_handler(payload) # 通过payload得到token
return Response({'status':100,'msg':'登录成功','token':token})
else:
return Response({'status':101,'msg':'用户名或密码错误'})
自定义认证类
import jwt
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.settings import api_settings
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
from app01 import models
class JsonAuthentication(JSONWebTokenAuthentication):
def authenticate(self, request):
jwt_value=self.get_jwt_value(request)
# jwt_value=request.GET.get('token')
# 验证签名,验证是否过期
try:
payload = jwt_decode_handler(jwt_value) # 得到载荷
# 取当前用户,拿到user对象,每登录一个人就要去数据库查一次,效率不高
# user=models.User.objects.filter(id=payload['user_id']).first()
# 效率更高一写,不需要查数据库了
user=models.User(id=payload['user_id'],username=payload['username'])
# user={'id':payload['user_id'],'username':payload['username']}
except jwt.ExpiredSignature:
msg = 'token过期'
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = '签名错误'
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed('反正就不对')
# 这个不能用,因为它找的是auth的user表,我们是去自己表中查,这个需要自己写
# user = self.authenticate_credentials(payload)
return (user, jwt_value)
多方式登录
1 登录功能,继承CreateAPIView,
重写create方法,认证用户名密码是否正确,签发token,在序列化类中写
2 多方式登录
输入用户名,输入手机号,输入邮箱加密码都能登录成功
视图 views.py
from rest_framework.generics import CreateAPIView
from app01 import models
from app01 import serializer
from rest_framework.response import Response
from rest_framework.viewsets import ViewSetMixin
# class UserLogin(CreateAPIView): # 需要自己配路径
class UserLogin(ViewSetMixin,CreateAPIView): # 自动生成路由
queryset = models.UserInfo.objects.all()
serializer_class = serializer.UserInfoModelSerializer
def create(self, request, *args, **kwargs):
# context是上下文,是视图类和序列化类沟通的桥梁
ser = self.get_serializer(data=request.data)
if ser.is_valid():
token = ser.context['token']
username = ser.context['username']
return Response({'status': 100, 'msg': '登录成功', 'token': token, 'username': username})
else:
print(ser.errors)
return Response({'status': 100, 'msg': '用户名或密码错误'})
序列化类 serializer.py
from app01 import models
from rest_framework import serializers
import re
from rest_framework.exceptions import ValidationError
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 UserInfoModelSerializer(serializers.ModelSerializer):
# 这么写,字段没有自己的校验规则了(针对于扩写的auth_user表)
username = serializers.CharField()
class Meta:
model = models.UserInfo
fields = ['username', 'password']
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
# 多方式登录,username有可能是手机号,邮箱,用户名
if re.match('^1[3-9][0-9]{9}$', username):
# 用手机号登录
user = models.UserInfo.objects.filter(phone=username).first()
elif re.match(r'^.+@.+$', username):
# 以邮箱登录
user = models.UserInfo.objects.filter(email=username).first()
else:
# 以用户名登录
user = models.UserInfo.objects.filter(username=username).first()
if user and user.check_password(password):
# 登录成,签发token
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
self.context['username'] = user.username
return attrs
else:
# 校验不通过
raise ValidationError('用户名或密码错误')
路由 urls.py
from django.contrib import admin
from django.urls import path,include
from rest_framework.routers import SimpleRouter
from app01 import views
router = SimpleRouter()
router.register('login', views.UserLogin)
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls)),
# path('login1/', views.UserLogin.as_view())
]