签发token
一. 前戏
cookie,session,token
-cookie是存放在客户端浏览器的键值对
-session是存放在服务端的键值对
客户端:sessionId:asdfasdf----> 放到cookie中
服务端:
asdfasdf:{id:3,name:lqz}
dddddd:{id:4,name:pyy}
-token: 字符串:分三段
-第一段:头--> 公司信息,加密方式。。。
-第二段:荷载--> 放用户信息---->{id:3,name:lqz}
-第三段:签名---> 把第一段和第二段通过某种加密方式+秘钥加密得到一个字符串 asdfads
-把三段使用base64编码后拼到一起 -----类似-----> dasfasd.asdfasdasd.asdfads 这种
-【token 的签发】---> 登录成功后,服务端生成
- 把token串给前端---> 前端拿着
-后面前端只要发请求,就携带token串到后端
-【token的验证】--》拿到第一段和第二段使用同样的加密方式+秘钥再加密---> 得到字符串跟第三段比较,如果一样,表示没有被篡改,如果不一样,表明被篡改了,token不能用了
-如果没有被篡改,取出第二段 当前登录用户的信息 id
二. jwt组成
base64编码样子,可以用 base64 进行解码查看。
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ.ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM
# 第一段:头部:header
# 声明加密算法,公司信息。。。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
# 第二段:荷载:payload
# 用户id,用户信息,token过期时间 exp,token签发时间 iat
eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ
# 第三段:签名 signature
# 通过某种加密方式+秘钥,把头和荷载加密后得到
# 使用它,做到防篡改,防伪造
ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM
三. 旧的 JWT
1. 使用导航
# 1. 下载
pip3 install djangorestframework-jwt
# 2. 路由层配置
"""必须是auth的user表中有用户, 自己创的表不行"""
"""通过 createsuperuser 创建用户"""
## 导入
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
## 注册使用
path('login/', obtain_jwt_token),
]
# 3. 视图类配置
class CarView(ViewSet):
# 认证类《必须配和下面的权限类一起使用》
authentication_classes = [JSONWebTokenAuthentication,]
# 权限类
permission_classes = [IsAuthenticated,]
# 4. 访问
## (1)发送post请求
http://127.0.0.1:8009/login/
## (2)携带参数 《 json 类型 》
{
# key 必须是 username, password
"username": "admin",
"password": "123456"
}
## (3)携带参数 《 请求头中 》,key 必须是 Authorization
{key: value} ===> {Authorization : jwt token串}
# token 串样式
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjgxMjA4MzE2LCJlbWFpbCI6IjEyM0BxcS5jb20ifQ.ZrQRrw5yolp40cWsDAU2JirrKhKQLydZH-hbvCYzYuM
2. 修改还回格式
配合标题 (1) 中的《使用导航》接着写
(1). 函数
新建一个py文件, 写一个函数
def jwt_response_payload_handler(token, user=None, request=None):
return {'code': 100, 'msg': '登陆成功', 'token': token, 'username': user.username}
(2). 配置
在配置文件中配置
JWT_AUTH = {
# value值是函数的位置
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}
3. 自定义
(1). 登陆时签发 token
可以自己定义用户表,实现签发token功能
from rest_framework_jwt.settings import api_settings
from rest_framework.decorators import action
from .models import User
class UserView(ViewSetMixin, APIView):
@action(methods=['POST', 'GET'], detail=False)
def login(self, request):
username = request.data.get('username')
password = request.data.get('password')
print('username', username, 'password', password) # {'pyy':'123'}
user = User.objects.filter(username=username, password=password).first()
if user:
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 根据当前登录用户获取荷载
print(payload) # {'user_id': 1, 'username': 'pyy', 'exp': datetime.datetime(2023, 4, 11, 11, 25, 37, 297881)}
token = jwt_encode_handler(payload) # 根据荷载生成token
return Response({'code': 100, 'msg': '登录成功', 'token': token, 'user': username})
else:
return Response({'code': 101, 'msg': '用户名或密码错误'})
(2). 认证类(验证token)
新建一个py文件,编写认证类,重写authenticate方法
import jwt
from rest_framework import exceptions
from rest_framework_jwt.settings import api_settings
from rest_framework.authentication import BaseAuthentication
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
from .models import User
from rest_framework.response import Response
class TokenAuthentication(BaseAuthentication):
def authenticate(self, request):
# 这里获取不一定非要在请求头中(可以放在别的地方《但必须指明接口文档》)
# jwt_value = request.query_params.get('token') or request.data.get('token')
jwt_value = request.META.get('HTTP_TOKEN') # 1 取出 token
"""
eg: 请求头传递数据 header
参数名:token, 参数值:"cdsvsdvmkdvkk"
取值方式: request.META.get('HTTP_TOKEN')
会转成 HTTP_ 加上 参数名(大写) ===> HTTP_TOKEN
Dbug 模式貌似查看不到
"""
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = 'token过期了'
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = 'token解码错误或为None'
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
msg = '解析token未知错误'
raise exceptions.AuthenticationFailed(msg)
print(payload) # 荷载---> user_id
user = User.objects.filter(pk=payload['user_id']).first()
return user, jwt_value
# 效率问题 可以 return user_id, jwt_value 防止接口访问量过大时,因上一步的查询, 造成数据库的负载过重
(3). 注册使用
在别的视图类中使用
class CarView(ViewSet):
# 注册使用
authentication_classes = [TokenAuthentication, ]
# 下面还要配和一个权限类。
permission_classes = [IsAuthenticated,]
......
"""准备"""
# 新建项目没问题,如果是老项目,迁移过数据了,按照如下操作
1 删数据库
2 删除项目中app的迁移文件
3 删除源码中 admin和auth中得迁移记录
4 扩写auth的user表
5 重新迁移
四. 新的 JWT
1. 使用导航
# 1. 安装
pip install djangorestframework-simplejwt
# 2. setting.py 注册
# (1). 注册 《和 django-rest-framework 一样,属于app》
INSTALLED_APPS = [
...
'rest_framework_simplejwt',
...
]
# (2). 配置过期时间
import datetime
SIMPLE_JWT = {
# ACCESS token有效时长 《用来发请求获取数据》
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=30),
# REFRESH token有效时长 《用来刷新 access》
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=1),
}
# 3. 迁移表,创建用户 《自己扩写的表也行》
createsuperuser
# 4. 使用
# (1). 路由层
from rest_framework_simplejwt.views import token_obtain_pair, token_verify, token_refresh
urlpatterns = [
path('login/', token_obtain_pair),
path('verify/', token_verify),
path('refresh/', token_refresh),
]
# (2). 局部或全局注册 *** 必须配合权限类 ***
## <1>. 局部注册
class BookView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
## <2>. 全局注册
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
5. 获取双 token 时,《post请求》
{
"username": "",
"password": ""
}
6. 前端访问携带 token,《 发请求获取数据时 》
"""*** 式必须如下,放在请求头中 ***"""
# Authorization 作为 key
# Bearer + 空格 + access的token
# 如下:
Authorization :Bearer access的token
7. 样式:《 双 token 》
{
# 用来发请求刷新新的 access
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTcxMjkwNjA1MCwiaWF0IjoxNzEyODE5NjUwLCJqdGkiOiIyODIxYzg3Y2FjZGM0NTZkOGJkZTJmNDY3ZWMxMjRlNiIsInVzZXJfaWQiOjF9.ThssxrQuZobwWRQId_toG_d1ZXMa5QPiM_DoGzqsI7c",
# 用来发请求获取数据
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzEyODIxNDUwLCJpYXQiOjE3MTI4MTk2NTAsImp0aSI6IjZiMzA3ODQwMzBmZjQ2OGI4YzNkNThmYjk0YTJmMDZhIiwidXNlcl9pZCI6MX0.01UXhWe9DOfGh2PDi-WCG9BzvPcQUHYDZostE9-INf4"
}
2. 源码解析
1 中的代码好像只加了个 “路由” 和 “简单的配置” 就能进行一系列的操作。
好像有点蒙, 下面做个简单的解释
# 代码 1
urlpatterns = [
# ctrl + 左键 点击 token_obtain_pair,见代码 2
path('login/', token_obtain_pair),
path('verify/', token_verify),
path('refresh/', token_refresh),
]
"""-------------以下是 djangorestframework-simplejwt 的源码---------------"""
# 代码 2
# 实际上是执行了 TokenObtainPairView 这个视图类
# ctrl + 左键 点击 TokenObtainPairView,见代码 3
token_obtain_pair = TokenObtainPairView.as_view()
# 代码 3
class TokenRefreshView(TokenViewBase):
"""里面就写了下面一行代码"""
# ctrl + 左键 点击 父类 TokenViewBase,见代码 4
# 这里就是源码自己的 序列化类
_serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER
# 代码 4
class TokenViewBase(generics.GenericAPIView):
# 部分代码省略
......
# 可以看到有个 post 方法, 返回的数据就是我们需要的数据
def post(self, request: Request, *args, **kwargs) -> Response:
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
# serializer.validated_data 就是序列化校验后的数据, 见代码 5
return Response(serializer.validated_data, status=status.HTTP_200_OK)
# 代码 5 《 在源码中可以找到, 上述用的就是这个 “序列化类” 》
# 序列化类
class TokenObtainPairSerializer(TokenObtainSerializer):
token_class = RefreshToken
def validate(self, attrs: Dict[str, Any]) -> Dict[str, str]:
data = super().validate(attrs)
# 签发 token 见代码 6
refresh = self.get_token(self.user)
# 将 refresh(token)access(token) 放到 data 中,以便后续使用。
data["refresh"] = str(refresh)
data["access"] = str(refresh.access_token)
if api_settings.UPDATE_LAST_LOGIN:
update_last_login(None, self.user)
return data
# 代码 6
@classmethod
def get_token(cls, user):
# for_user(user)真正签发 token, 见代码 7
return cls.token_class.for_user(user)
# 代码 7
@classmethod
def for_user(cls, user):
"""
Returns an authorization token for the given user that will be provided
after authenticating the user's credentials.
返回将提供的给定用户的授权令牌在验证了用户的凭据之后。
"""
user_id = getattr(user, api_settings.USER_ID_FIELD)
if not isinstance(user_id, int):
user_id = str(user_id)
token = cls()
token[api_settings.USER_ID_CLAIM] = user_id
if api_settings.CHECK_REVOKE_TOKEN:
token[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(
user.password
)
return token
3. 定制还回格式
# 1. 重写 TokenObtainPairSerializer 中 validate 方法,返回什么,前端看到什么
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
# 往荷载中加东西
@classmethod
# user就是登录成功,查到的用户
def get_token(cls, user):
# 签发token
token = super().get_token(user)
# 往荷载中加用户名
token['name'] = user.username
return token
def validate(self, attrs):
old_data = super().validate(attrs)
data = {
'code': 200,
'msg': '登录成功!!!',
'username': self.user.username,
'refresh': old_data['refresh'],
'access': old_data['access']
}
return data
2. 配置文件配置
SIMPLE_JWT = {
"TOKEN_OBTAIN_SERIALIZER": "app01.serializer.MyTokenObtainPairSerializer",
}
4. 多方式登录
** 手机号,用户名 或 邮箱进行登录。**
# 下面三种的 “路由层”
urlpatterns = [
path('mylogin/', views.UserView.as_view()),
]
(1). 简易版
“视图类” 中编写
class UserView(APIView):
authentication_classes = ()
permission_classes = ()
def post(self, request):
# 1 request取出用户名和密码
username = request.data.get('username')
password = request.data.get('password')
# 2 使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
if re.match(r'^1[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match(r'^.+@.+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
# 3 校验密码
if user and user.check_password(password):
# 4 签发token
refresh = TokenObtainPairSerializer.get_token(user)
# 5 返回给前端
return Response({'code': 100, 'msg': '成功', 'access': str(refresh.access_token), 'refresh': str(refresh)})
else:
return Response({'code': 101, 'msg': '用户名或密码错误'})
(2). 封装版
“视图类” 配和 “序列化类”
"""-------------------视图类--------------------"""
from rest_framework.generics import GenericAPIView
from .serializer import LoginSerializer
class UserView(GenericAPIView):
authentication_classes = ()
permission_classes = ()
serializer_class = LoginSerializer
def post(self, request):
ser = self.get_serializer(data=request.data)
# 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
if ser.is_valid():
# context 是视图类和序列化列之间沟通的桥梁
access = ser.context.get('access')
refresh = ser.context.get('refresh')
username = ser.context.get('username')
return Response({'code': 100, 'msg': '成功', 'username': username, 'access': access, 'refresh': refresh})
else:
return Response({'code': 101, 'msg': '用户名或密码错误11'})
"""-------------------序列化类--------------------"""
from rest_framework import serializers
from .models import User
import re
from rest_framework.exceptions import ValidationError
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
# 写全局钩子
def validate(self, attrs):
# 校验用户,签发token
username = attrs.get('username')
password = attrs.get('password')
# 2 使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
if re.match(r'^1[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match(r'^.+@.+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
if user and user.check_password(password):
# 3 校验密码
# 4 签发token
refresh = TokenObtainPairSerializer.get_token(user)
self.context['access'] = str(refresh.access_token)
self.context['refresh'] = str(refresh)
self.context['username'] = user.username
return attrs
else:
raise ValidationError('用户名或密码错误')
(2). 最终版
"""-------------------视图类--------------------"""
class UserView(GenericAPIView):
authentication_classes = ()
permission_classes = ()
serializer_class = LoginSerializer
def post(self, request):
ser = LoginSerializer(data=request.data)
if ser.is_valid(): # 会执行字段自己的校验(没有),执行局部钩子(没有),执行全局钩子(写了:校验用户,签发token)
# ser.validated_data # 字典,校验过后的数据
return Response(ser.validated_data )
else:
return Response({'code': 101, 'msg': '用户名或密码错误11'})
"""-------------------序列化类--------------------"""
class LoginSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
# 写全局钩子
def validate(self, attrs):
# 校验用户,签发token
username = attrs.get('username')
password = attrs.get('password')
# 2 使用正则判断用用户名是邮箱,手机号还是用户名,分别去查询当前用户
if re.match(r'^1[3-9][0-9]{9}$', username):
user = User.objects.filter(mobile=username).first()
elif re.match(r'^.+@.+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
if user and user.check_password(password):
# 3 校验密码
# 4 签发token
refresh = TokenObtainPairSerializer.get_token(user)
data = {'code': 100,
'msg': '登录成功成功',
'username': self.user.username,
'refresh':str(refresh),
'access': str(refresh.access_token)
}
return data
else:
raise ValidationError('用户名或密码错误')
五. jwt默认配置
# JWT配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期
# 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整
# 是否自动刷新Refresh Token
'ROTATE_REFRESH_TOKENS': False,
# 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256', # 加密算法
'SIGNING_KEY': settings.SECRET_KEY, # 签名密匙,这里使用Django的SECRET_KEY
# 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间
"UPDATE_LAST_LOGIN": False,
# 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。
"VERIFYING_KEY": "",
"AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。
"ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。
"JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。
"JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。
"LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。
# 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer"
"AUTH_HEADER_TYPES": ("Bearer",),
# 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION"
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# 用户模型中用作用户ID的字段。默认为"id"。
"USER_ID_FIELD": "id",
# JWT负载中包含用户ID的声明。默认为"user_id"。
"USER_ID_CLAIM": "user_id",
# 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
# 用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
# JWT负载中包含令牌类型的声明。默认为"token_type"。
"TOKEN_TYPE_CLAIM": "token_type",
# 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JWT负载中包含JWT ID的声明。默认为"jti"。
"JTI_CLAIM": "jti",
# 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
# 滑动令牌的生命周期。默认为5分钟。
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
# 滑动令牌可以用于刷新的时间段。默认为1天。
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
# 用于生成access和刷refresh的序列化器。
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
# 用于刷新访问令牌的序列化器。默认
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
# 用于验证令牌的序列化器。
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
# 用于列出或撤销已失效JWT的序列化器。
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
# 用于生成滑动令牌的序列化器。
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
# 用于刷新滑动令牌的序列化器。
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
# 如何配置到我们项目的 settings.py 中
from datetime import timedelta
SIMPLE_JWT ={
# Access Token的有效期
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
# Refresh Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
# token 前缀,默认是 Bearer
"AUTH_HEADER_TYPES": ("TOKEN",),
}