REST framework 定义的异常
异常名称 | 说明 |
---|---|
APIException | 所有异常的父类 |
ParseError | 解析错误 |
AuthenticationFailed | 认证失败 |
NotAuthenticated | 尚未认证 |
PermissionDenied | 权限拒绝 |
NotFound | 未找到 |
MethodNotAllowed | 请求方式不支持 |
NotAcceptable | 要获取的数据格式不支持 |
Throttled | 超过限流次数 |
ValidationError | 校验失败 |
REST framework 提供了异常处理,我们可以自定义异常处理函数
from rest_framework.views import exception_handler
def custom_exception_handler(exc,context):
# 先调用 REST framework 默认的异常处理方法获得标准错误响应对象
response = exception_handler(exc,context)
# 补充自定义的异常处理
if response is not None:
response.data['status_code'] = response.status_code
return response
在配置文件中声明自定义的异常处理
REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "demo_app.utils.custom_exception_handler", # 自定义的异常处理
}
from rest_framework import status
from django.db import DatabaseError
from rest_framework.response import Response
def custom_exception_handler(exc,context):
# 先调用 REST framework 默认的异常处理方法获得标准错误响应对象
response = exception_handler(exc,context)
# 补充自定义的异常处理
if response is None:
view = context['view']
if isinstance(exc,DatabaseError):
print('[%s]:%s'%(view,exc))
response = Response({'detail':'服务器错误'},status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return response
REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "demo_app.exception.CustomExceptionHandler", # 自定义的异常处理
}
# json_response.py
# -*- coding: utf-8 -*-
from rest_framework.response import Response
class SuccessResponse(Response):
"""
标准响应成功的返回, SuccessResponse(data)或者SuccessResponse(data=data)
(1)默认code返回2000, 不支持指定其他返回码
"""
def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False,
content_type=None,page=1,limit=1,total=1):
std_data = {
"code": 200,
"data": {
"page": page,
"limit": limit,
"total": total,
"data": data
},
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class DetailResponse(Response):
"""
不包含分页信息的接口返回,主要用于单条数据查询
"""
def __init__(self, data=None,
msg='success',
status=None,
template_name=None,
headers=None,
exception=False,
content_type=None,):
std_data = {
"code": 200,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class ErrorResponse(Response):
"""
标准响应错误的返回,ErrorResponse(msg='xxx')
"""
def __init__(self, data=None, msg='error', code=500, status=None, template_name=None, headers=None,
exception=False, content_type=None):
std_data = {
"code": code,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
# -*- coding: utf-8 -*-
# exception
"""自定义异常处理"""
import logging
import traceback
from django.db.models import ProtectedError, RestrictedError
from django.http import Http404
from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, PermissionDenied
from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED
from rest_framework.views import set_rollback, exception_handler
from .json_response import ErrorResponse
logger = logging.getLogger(__name__)
def CustomExceptionHandler(ex, context):
"""
统一异常拦截处理
目的:(1)取消所有的500异常响应,统一响应为标准错误返回
(2)准确显示错误信息
:param ex:
:param context:
:return:
"""
msg = ""
code = 999
# 调用默认的异常处理函数
response = exception_handler(ex, context)
if isinstance(ex, AuthenticationFailed):
# 如果是身份验证错误
if response and response.data.get("detail") == "Given token not valid for any token type":
code = 401
msg = ex.detail
elif response and response.data.get("detail") == "Token is blacklisted":
# token在黑名单
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
else:
code = 401
msg = ex.detail
elif isinstance(ex, Http404):
code = 400
msg = "接口地址不正确"
elif isinstance(ex, DRFAPIException):
set_rollback()
msg = ex.detail
if isinstance(ex, PermissionDenied):
msg = f'{msg} ({context["request"].method}: {context["request"].path})'
if isinstance(msg, dict):
for k, v in msg.items():
for i in v:
msg = "%s:%s" % (k, i)
elif isinstance(ex, (ProtectedError, RestrictedError)):
set_rollback()
msg = "无法删除:该条数据与其他数据有相关绑定"
# elif isinstance(ex, DatabaseError):
# set_rollback()
# msg = "接口服务器异常,请联系管理员"
elif isinstance(ex, Exception):
logger.exception(traceback.format_exc())
msg = str(ex)
return ErrorResponse(msg=msg, code=code)
可以在配置文件中配置全局默认的认证方案
# settings.py
REST_FRAMEWORK = {
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式配置
"DATE_FORMAT": "%Y-%m-%d",
"DEFAULT_AUTHENTICATION_CLASSES": (
# "rest_framework_simplejwt.authentication.JWTAuthentication",# JWT 认证 # pip install djangorestframework-simplejwt
"rest_framework.authentication.BasicAuthentication", # 基础认证
"rest_framework.authentication.SessionAuthentication", # session 认证
)
}
也可以在视图中通过设置 authentication_classess 属性来设置 局部认证
from django.db.models import Q
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.authentication import SessionAuthentication,BasicAuthentication
from .serializers import BookSerializer
from .models import Book
from rest_framework.response import Response
class BookModelViewSet(ModelViewSet):
authentication_classes = (SessionAuthentication,BasicAuthentication)
...
认证失败会有两种可能的返回值
pip install djangorestframework-simplejwt
# settings.py
INSTALLED_APPS = [
# ...
'rest_framework_simplejwt',
# 下面这个app用于刷新refresh_token后,将旧的加到到blacklist时使用
'rest_framework_simplejwt.token_blacklist'
# ...
]
# ================================================= #
# ***************** REST_FRAMEWORK配置 ************ #
# ================================================= #
REST_FRAMEWORK = {
...
"DEFAULT_AUTHENTICATION_CLASSES": (
...
"rest_framework_simplejwt.authentication.JWTAuthentication",# JWT 认证 # pip install djangorestframework-simplejwt
)
}
# ================================================= #
# ****************** simplejwt配置 ***************** #
# ================================================= #
# 选择自己需要配置即可
from datetime import timedelta
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': 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),
# 用于生成访问令牌和刷新令牌的序列化器。
"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",
}
# ================================================= #
# ****************** simplejwt配置 ***************** #
# ================================================= #
# 我选择的
from datetime import timedelta
SIMPLE_JWT = {
# token有效时长
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
# token刷新后的有效时间
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
# 设置前缀
"AUTH_HEADER_TYPES": ("JWT",),#
# 是否自动刷新Refresh Token
"ROTATE_REFRESH_TOKENS": True,
# 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
'BLACKLIST_AFTER_ROTATION': True,
# 用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
}
执行数据库迁移:
python manage.py makemigrations
python manage.py migrate
当配置了 AUTH_HEADER_TYPES 指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer";在使用 postman和其他的一些自带的认证中会在请求同中增加Bearer + token 字符串。在自定义Header 中可以自己进行增加内容。如我使用的是JWT开头的那么在 postman中
源码查看
每次访问该视图时,都会调用**JSONWebTokenAuthentication.authenticate**
进行认证.
from rest_framework_simplejwt.authentication import JWTAuthentication
user, tokrn = JWTAuthentication().authenticate(request)
在源码 authenticate 方法中获取
当不匹配时携带请求头会出现: 身份认证信息未提供。
from django.urls import path,include,re_path
from rest_framework.documentation import include_docs_urls
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView
)
urlpatterns = [
# jwt 认证
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# 下面这个是用来验证 token 的,根据需要进行配置
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
]
该序列化器需继承TokenObtainPairSerializer类,可以在任意一个app中的seralizers.py中增加该自定义的序列化器,并重写了get_token()方法。在这个方法中,我们可以自定义Payload,将用户的信息添加到Token中。
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class LoginSerializer(TokenObtainPairSerializer):
def get_token(cls, user):
token = super().get_token(user)
# 增加想要加到token中的信息
token['username'] = user.username
token['email'] = user.email
return token
# 改写simple JWT提供的默认视图,在app01/views中新增一个视图,该视图需继承至默认的视图类TokenObtainPairView
class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
#urls.py
from demo_app.login import LoginView
urlpatterns = [
# 登录
path('login/',LoginView.as_view(),name='login'),
]
TokenObtainSerializer 中登录登录失败提示:
重写登陆提示:
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
from django.contrib.auth.models import User # 使用的是默认的django 用户
class LoginSerializer(TokenObtainPairSerializer):
class Meta:
model = User
fields = "__all__"
read_only_fields = ["id","username","email"]
def get_token(cls,user):
''' 重写 获取 token中保存的内容 '''
token = super().get_token(user)
# 增加想要加到token中的信息
token['username'] = user.username
token['email'] = user.email
return token
# 登录失败提示
default_error_messages = {"no_active_account": '账号/密码错误'}
# 重写登录方法
def validate(self,attrs):
''' 重写登录方法 '''
# 获取传入的 用户名和密码
username = self.initial_data.get("username", None)
password = self.initial_data.get("password", None)
print("username:",username,"\npassword:",password)
data = super().validate(attrs)#获取 返回的 token内容
return {"code": 200, "msg": "请求成功", "data": data}
class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
# -*- coding: utf-8 -*-
# 统一返回格式
from rest_framework.response import Response
class SuccessResponse(Response):
"""
标准响应成功的返回, SuccessResponse(data)或者SuccessResponse(data=data)
(1)默认code返回2000, 不支持指定其他返回码
"""
def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False,
content_type=None,page=1,limit=1,total=1):
std_data = {
"code": 200,
"data": {
"page": page,
"limit": limit,
"total": total,
"data": data
},
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class DetailResponse(Response):
"""
不包含分页信息的接口返回,主要用于单条数据查询
(1)默认code返回 200, 不支持指定其他返回码
"""
def __init__(self, data=None,
msg='success',
status=None,
template_name=None,
headers=None,
exception=False,
content_type=None,):
std_data = {
"code": 200,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class ErrorResponse(Response):
"""
标准响应错误的返回,ErrorResponse(msg='xxx')
(1)默认错误码返回 500, 也可以指定其他返回码:ErrorResponse(code=xxx)
"""
def __init__(self, data=None, msg='error', code=500, status=None, template_name=None, headers=None,
exception=False, content_type=None):
std_data = {
"code": code,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
# -*- coding: utf-8 -*-
from rest_framework.response import Response
class SuccessResponse(Response):
"""
标准响应成功的返回, SuccessResponse(data)或者SuccessResponse(data=data)
(1)默认code返回2000, 不支持指定其他返回码
"""
def __init__(self, data=None, msg='success', status=None, template_name=None, headers=None, exception=False,
content_type=None,page=1,limit=1,total=1):
std_data = {
"code": 200,
"data": {
"page": page,
"limit": limit,
"total": total,
"data": data
},
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class DetailResponse(Response):
"""
不包含分页信息的接口返回,主要用于单条数据查询
(1)默认code返回 200, 不支持指定其他返回码
"""
def __init__(self, data=None,
msg='success',
status=None,
template_name=None,
headers=None,
exception=False,
content_type=None,):
std_data = {
"code": 200,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
class ErrorResponse(Response):
"""
标准响应错误的返回,ErrorResponse(msg='xxx')
(1)默认错误码返回 500, 也可以指定其他返回码:ErrorResponse(code=xxx)
"""
def __init__(self, data=None, msg='error', code=500, status=None, template_name=None, headers=None,
exception=False, content_type=None):
std_data = {
"code": code,
"data": data,
"msg": msg
}
super().__init__(std_data, status, template_name, headers, exception, content_type)
异常处理
# -*- coding: utf-8 -*-
"""自定义异常处理"""
import logging
import traceback
from django.db.models import ProtectedError, RestrictedError
from django.http import Http404
from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, PermissionDenied
from rest_framework.status import HTTP_407_PROXY_AUTHENTICATION_REQUIRED, HTTP_401_UNAUTHORIZED
from rest_framework.views import set_rollback, exception_handler
from .json_response import ErrorResponse
logger = logging.getLogger(__name__)
def CustomExceptionHandler(ex, context):
"""
统一异常拦截处理
目的:(1)取消所有的500异常响应,统一响应为标准错误返回
(2)准确显示错误信息
:param ex:
:param context:
:return:
"""
msg = ""
code = 500
# 调用默认的异常处理函数
response = exception_handler(ex, context)
if isinstance(ex, AuthenticationFailed):
# 如果是身份验证错误
if response and response.data.get("detail") == "Given token not valid for any token type":
code = 401
msg = ex.detail
elif response and response.data.get("detail") == "Token is blacklisted":
# token在黑名单
return ErrorResponse(status=HTTP_401_UNAUTHORIZED)
else:
code = 401
msg = ex.detail
elif isinstance(ex, Http404):
code = 400
msg = "接口地址不正确"
elif isinstance(ex, DRFAPIException):
set_rollback()
msg = ex.detail
if isinstance(ex, PermissionDenied):
msg = f'{msg} ({context["request"].method}: {context["request"].path})'
if isinstance(msg, dict):
for k, v in msg.items():
for i in v:
msg = "%s:%s" % (k, i)
elif isinstance(ex, (ProtectedError, RestrictedError)):
set_rollback()
msg = "无法删除:该条数据与其他数据有相关绑定"
# elif isinstance(ex, DatabaseError):
# set_rollback()
# msg = "接口服务器异常,请联系管理员"
elif isinstance(ex, Exception):
logger.exception(traceback.format_exc())
msg = str(ex)
return ErrorResponse(msg=msg, code=code)
登录
# coding=utf-8
'''
@date:2023/11/29 11:15
@mail:[email protected]
@Content:
'''
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
from django.contrib.auth.models import User
from rest_framework_simplejwt.tokens import RefreshToken, AccessToken
from rest_framework.exceptions import APIException
from rest_framework.views import APIView
from django.contrib.auth.models import User
from .json_response import DetailResponse
class LoginSerializer(TokenObtainPairSerializer):
class Meta:
model = User
fields = "__all__"
read_only_fields = ["id","username","email"]
def get_token(cls,user):
''' 重写 获取 token中保存的内容 '''
token = super().get_token(user)
# 增加想要加到token中的信息
token['username'] = user.username
token['email'] = user.email
return token
default_error_messages = {"no_active_account": '账号/密码错误'}
# 重写登录方法
def validate(self,attrs):
''' 重写登录方法 '''
#
username = self.initial_data.get("username", None)
password = self.initial_data.get("password", None)
print("username:",username,"\npassword:",password)
# 可以 增加图片验证码验证,短信验证码验证等登录验证因子
# if True:
# raise CustomValidationError("验证码不正确")
# 获取 返回的 token内容
data = super().validate(attrs)
return {"code": 2000, "msg": "请求成功", "data": data}
# 改写simple JWT提供的默认视图,在app01/views中新增一个视图,该视图需继承至默认的视图类TokenObtainPairView
class LoginView(TokenObtainPairView):
"""
登录接口
"""
serializer_class = LoginSerializer
permission_classes = []
class CustomValidationError(APIException):
"""
继承并重写验证器返回的结果,避免暴露字段
"""
def __init__(self, detail):
self.detail = detail
settings.py 中修改登录配置
# ================================================= #
# ******************** 登录方式配置 ******************** #
# ================================================= #
AUTHENTICATION_BACKENDS = ["demo_app.backends.CustomBackend"]
重写等
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.hashers import check_password
from django.db.models import Q
from django.contrib.auth.models import User
from rest_framework.exceptions import APIException
class CustomValidationError(APIException):
"""
继承并重写验证器返回的结果,避免暴露字段
"""
def __init__(self, detail):
self.detail = detail
class CustomBackend(ModelBackend):
"""
重写认证登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
# 查询用户对象 可通过密码和邮箱登录
user = User.objects.filter(Q(username=username) | Q(email=username)).first()
# 判断 用户,密码, 是否是管理员
if user and check_password(password, user.password) and user.is_staff:
return user # 验证通过返回对象 否则返回None
else:
raise CustomValidationError("登录用户账号或密码错误!")