Skip to content

restful

相关资料参考:

restful规范

1. 用https代替http

2. 域名体现

1832669309696999424.png

1832669310342922240.png

3. 版本

1832669310984650752.png

4. 路径

5. 请求方法

6. 搜索条件

7. 返回数据

8. 状态码

9. 错误处理

案例

这里列举一个遵循上述规则的案例:

python

快速上手

安装

注意,截止到目前(2021/12/4)restful框架的最新版本是3.12.4,而该框架对Django和Python解释器的版本都有要求:

text
如果你的要使用最新的版本,请注意版本,版本要求:
djangorestframework==3.12.4
	Python (3.5, 3.6, 3.7, 3.8, 3.9)
	Django (2.2, 3.0, 3.1, 3.2)

你也可以使用较老的版本来使用,版本要求:
djangorestframework==3.11.2
	Python (3.5, 3.6, 3.7, 3.8)
	Django (1.11, 2.0, 2.1, 2.2, 3.0)

而我的环境则是3.9+Django3.2,所以我可以下载当前最新的版本:

# pip install django==3.2
pip install djangorestframework==3.12.4

Django4.2,你就下载:

bash
pip install djangorestframework==3.14.0

配置

下载完成后,还需要在你的项目中的settings.py文件中进行配置:

python
INSTALLED_APPS = [
    ...
    # 注册rest_framework(drf)
    'rest_framework',
]

# drf相关配置以后编写在下面的字典中 
REST_FRAMEWORK = {}

完事之后,别忘了进行数据库迁移:

python manage.py makemigrations
python manage.py migrate

简单示例

这里需要你有Django的CBV和FBV相关的知识作为铺垫

另外,示例的项目名是drf,app名字命名规则app01、app02.......

python
# urls.py
from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserView.as_view()),
]


# views.py
from rest_framework.response import Response
from rest_framework.views import APIView


class UserView(APIView):

    def get(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

浏览器访问http://127.0.0.1:8000/api/users/

1832669311655739392.png

ok了,我们的第一个drf示例就成功了,这也意味着你的环境没有问题了。

session

版本管理

在restful规范中,后端的api需要体现版本,drf框架中支持5种版本的设置。

所以这部分主要介绍5种版本配置方式,除此之外,我们还要学习在drf中,如何反向生成url。

url的get参数传递(常用)

局部配置,为指定的试图类设置api版本

为单独的视图类指定api版本的设置如下:

python
# urls.py
from django.contrib import admin
from django.urls import path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserView.as_view()),
]

# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.versioning import QueryParameterVersioning  # 需要导入


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = QueryParameterVersioning

    def get(self, request, *args, **kwargs):
        # 浏览器的url上的version参数可以通过request.version直接获取
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

# settings.py暂时不配置

如果经过上述配置之后,浏览器的urlhttp://127.0.0.1:8000/api/users/?version=v1上,必须携带版本,而且key还必须是version,否则后端拿不到。

全局配置api版本

如果所有的api需要配置版本,那么按照上面的配置就比较麻烦了,因为我们要在每个视图类中定义静态属性versioning_class = QueryParameterVersioning

所以,我们还可以通过在settings.py中,进行一个全局的配置来让版本管理更灵活。

python
# settings.py
REST_FRAMEWORK = {
    # url上的key可以通过下面的设置自定义,就不用非要用version了,比如我想用v=v1
    "VERSION_PARAM": "v",
    # 设置默认api版本,这样,url上就可以不用携带了 http://127.0.0.1:8000/api/users/
    "DEFAULT_VERSION": "v1",
    # 可以限制版本的值,防止有人篡改
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"],
    # 全局配置api版本,应用于所有的视图类,这样每个视图类中就不用特别的声明静态属性了
    "DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.QueryParameterVersioning"
}

注意,如果在settings.py中配置了"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.QueryParameterVersioning",另外又在视图类中也配置了静态属性,那么视图类中的配置会覆盖掉全局的配置。

url路径传递(常用)

这种在url路径上传递版本的方式有别于get参数传递:

text
# get参数传递
http://127.0.0.1:8000/api/users/?version=v1

# url路径传递
http://127.0.0.1:8000/api/v1/users/

所以,配置也稍有不同,主要体现在urls.py中。

局部配置,为指定的试图类设置api版本

python
# urls.py
from django.contrib import admin
from django.urls import path, re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    # path('api/users/', views.UserView.as_view()),
    # 法1,用Django3新支持的写法
    path('api/<str:version>/users/', views.UserView.as_view()),
    # 法2,传统形式,用正则表达式
    # re_path(r'api/(?P<version>\w+)/users/', views.UserView.as_view()),
]

# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
# 这次导入的就是URLPathVersioning
from rest_framework.versioning import URLPathVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = URLPathVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

# settings.py暂时不配置

settings.py文件暂时无需配置,然后浏览器就可以访问了:http://127.0.0.1:8000/api/v1/users/

全局配置api版本

也可以在settings.py文件进行配置,以适应全局:

python
# settings.py
REST_FRAMEWORK = {
    # 可以限制版本的值,防止有人篡改
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"],
    # 全局配置api版本,应用于所有的视图类,这样每个视图类中就不用特别的声明静态属性了
    "DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning"
}

注意,如果在settings.py中配置了"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning",另外又在视图类中也配置了静态属性,那么视图类中的配置会覆盖掉全局的配置。

请求头传递(不常用)

局部配置,为指定的试图类设置api版本

在请求头传递版本,就要设置添加一对儿请求头了:

{"Accept": "application/json;version=v1"}

但通过浏览器无法实现,所以这里可以使用postman来做,携带方式参考下图:

1832669312301662208.png

后台的配置:

python
# urls.py
from django.contrib import admin
from django.urls import path, re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserView.as_view()),  # 这里不变

]

# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
# 这次导入的就是 AcceptHeaderVersioning
from rest_framework.versioning import AcceptHeaderVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = AcceptHeaderVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

# settings.py暂时不配置

全局配置api版本

python
# settings.py
REST_FRAMEWORK = {
    "VERSION_PARAM": "v",  # 自定义请求头中的key
    "DEFAULT_VERSION": "v1",  # 请求头中也可以不携带版本信息,用这里指定的默认值也行
    "ALLOWED_VERSIONS": ["v1", "v2", "v3"],  # 版本值限制
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning"  # 全局配置
}

# urls.py和veiws.py中不用配置了

注意,如果在settings.py中配置了"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.AcceptHeaderVersioning",另外又在视图类中也配置了静态属性,那么视图类中的配置会覆盖掉全局的配置。

二级域名传递版本(不常用)

这个用的也比较少,所以仅作了解。

通过二级域名传递版本的url通常是这样的:

http://v1.zhangkai.com/api/users
http://v2.zhangkai.com/api/users

如果你想跟着做操作,那么就要费点功夫来处理一下二级域名的映射了。

如果你是Windows平台,你需要修改hosts文件,它位于C:\Windows\System32\drivers\etc这个目录,打开之后,添加下面两行映射:

127.0.0.1	v1.zhangkai.com
127.0.0.1	v2.zhangkai.com
127.0.0.1	v3.zhangkai.com

如果无法保存,就自行百度吧!

如果你是mac系统或者Linux系统,就在终端中,首先获取root权限,然后vim /etc/hosts,也是添加下面两行映射:

text
127.0.0.1	v1.zhangkai.com
127.0.0.1	v2.zhangkai.com
127.0.0.1	v3.zhangkai.com

完事保存。

接下来,我们看Django这边怎么配置吧。

局部配置,为指定的试图类设置api版本

python
# settings.py
ALLOWED_HOSTS = ["*"]  # 谁都可以访问

# urls.py
from django.contrib import admin
from django.urls import path, re_path
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserView.as_view()),

]


# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
# 这次导入的就是 HostNameVersioning
from rest_framework.versioning import HostNameVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = HostNameVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

然后浏览器就可以访问了:

  • http://v1.zhangkai.com:8000/api/users/
  • http://v2.zhangkai.com:8000/api/users/

全局配置api版本

python
# settings.py
ALLOWED_HOSTS = ["*"]  # 谁都可以访问
REST_FRAMEWORK = {
    "ALLOWED_VERSIONS": ["v1", "v2"],  # 限制版本值
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.HostNameVersioning" # 全局配置,不用多说
}


# urls.py 不用修改


# views.py 可以不用写静态属性了
from rest_framework.response import Response
from rest_framework.views import APIView
# 这次导入的就是 HostNameVersioning
from rest_framework.versioning import HostNameVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    # versioning_class = HostNameVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

注意,如果在settings.py中配置了"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.HostNameVersioning",另外又在视图类中也配置了静态属性,那么视图类中的配置会覆盖掉全局的配置。

经过settings这么一配置,浏览器就可以访问:

  • 可以:http://v1.zhangkai.com:8000/api/users/
  • 可以:http://v2.zhangkai.com:8000/api/users/
  • 不可以:http://v3.zhangkai.com:8000/api/users/,因为settings中限制了。

ok,测试完之后,就可以将之前的hosts文件、settings.py中的ALLOWED_HOSTS都还原了哈。

通过路由中的namespace传递版本(不常用)

这个用的也比较少,我们简单了解下就可以了。

局部配置,为指定的试图类设置api版本

这个要通过路由的namespace参数体现,所以:

python
# drf\urls.py 总路由中使用including进行分发
from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    # path('api/users/', views.UserView.as_view()),
    # 下面三个版本都将指向子路由中的同一个视图类
    path('api/v1/', include(("app01.urls", "app01"), namespace="v1")),
    path('api/v2/', include(("app01.urls", "app01"), namespace="v2")),
    path('api/v3/', include(("app01.urls", "app01"), namespace="v3")),

]

# app01\urls.py 子路由直接写
from django.urls import path, re_path
from app01 import views

urlpatterns = [
    path('users/', views.UserView.as_view()),
]

# app01\views.py
from rest_framework.response import Response
from rest_framework.views import APIView
# 这次导入的就是 NamespaceVersioning
from rest_framework.versioning import NamespaceVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = NamespaceVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})

# settings.py 暂不配置

然后,浏览器就可以访问了:

  • http://127.0.0.1:8000/api/v1/users/
  • http://127.0.0.1:8000/api/v2/users/
  • http://127.0.0.1:8000/api/v3/users/

可以看到,这种方式很麻烦,所以用的少。

全局配置api版本

python
REST_FRAMEWORK = {
    "ALLOWED_VERSIONS": ["v1", "v2"],  # 限制版本值
    "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning"  # 全局配置,不用多说
}

注意,如果在settings.py中配置了"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.NamespaceVersioning",另外又在视图类中也配置了静态属性,那么视图类中的配置会覆盖掉全局的配置。

以上就是drf中支持的5种版本管理的类的使用和配置,前两种用的最多,后面三种用的较少,仅作了解即可。

反向生成url

反向生成url的原理就是drf内部调用了Django的reverse来反向生成url,然后再拼接上版本。

来看代码:

python
# urls.py
from django.contrib import admin
from django.urls import path, re_path, include
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/users/', views.UserView.as_view(), name='u1'),
    path('api/users/<int:pk>/', views.UserView.as_view(), name='u2'),

]

# views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.versioning import QueryParameterVersioning


class UserView(APIView):
    # 想要让哪个视图类应用上版本,需要定义一个静态属性:versioning_class
    versioning_class = QueryParameterVersioning

    def get(self, request, *args, **kwargs):
        print(request.version)  # v1

        # 反向生成url部分
        # 请求 http://127.0.0.1:8000/api/users/?version=v1
        # url1 = request.versioning_scheme.reverse("u1", request=request)
        # print("url1", url1)  # url1 http://127.0.0.1:8000/api/users/?version=v1

        print(11111, args)  # 11111 ()
        print(22222, kwargs)  # 22222 {'pk': 11}
        # 请求 http://127.0.0.1:8000/api/users/11/?version=v1
        # pk就是路由中的<int:pk>
        # args的值是一个元组
        url2 = request.versioning_scheme.reverse("u2", args=(kwargs.get('pk'),), request=request)
        print("url2", url2)  # url2 http://127.0.0.1:8000/api/users/11/?version=v1
        return Response({"code": 1000, "data": "xxx"})

    def post(self, request, *args, **kwargs):
        return Response({"code": 1000, "data": "xxx"})
    
# settings.py 无配置

然后浏览器就可以访问了:

  • http://127.0.0.1:8000/api/users/?version=v1,把上面代码中url1部分放开注释,然后把url2部分注释掉。
  • http://127.0.0.1:8000/api/users/11/?version=v1,把上面代码中url2部分放开注释,然后把url1部分注释掉。

认证

认证功能是网站必不可少的功能,例如:

  • 无需认证,就可以访问并获取数据。
  • 需认证,用户需先登录,后续发送请求需携带登录时发放的凭证。

来看drf中提供了哪些认证方式。

python
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication'
    ]
}

drf中,默认就这两个认证:

  • BasicAuthentication:基本没有实现什么认证的东西,是SessionAuthentication的父类,所以这个没啥好说的。
  • SessionAuthentication:基于django的session和auth实现的认证类,所以,如果你的项目中如果没有使用这两个组件,那么这个认证类也基本废了。

除了上面这两个认证类,我们也可以自定义认证类,下面在着重展开说。

全局认证

由于BasicAuthentication没有具体实现认证功能,所以这里我们主要来看SessionAuthentication这个认证类(前提是你settings中INSTALLED_APPS使用auth和session组件),下面是认证类在配置中的基本写法。

settings.py

python
REST_FRAMEWORK = {
    # 认证类列表,那个类写在前面,优先使用哪个
    'DEFAULT_AUTHENTICATION_CLASSES': [
        # session认证,admin后台其实就使用的session认证,其实接口开发很少用到session认证,所以我们通过配置可以改为其他认证,比如后面项目里面我们用到jwt,JSON WEB TOKEN认证,或者一些配合redis的认证
        'rest_framework.authentication.SessionAuthentication',
        # 基本认证,工作当中可能一些测试人员会参与的话,他们会将一些认证数据保存在内存当中,然后验证的,我们基本上用不上
        'rest_framework.authentication.BasicAuthentication',
        # 这个列表中也可以写我们自己的认证类,来替换上面的
        'api.views.CustomAuthentication1', 
    ]
}

视图:

python
from rest_framework.views import APIView
from rest_framework.response import Response


class AuthView(APIView):
    """ 视图类 """
    def get(self, request, *args, **kwargs):
        # 认证成功,会给request对象赋值如下两个属性,这里我们先知道就行,后面细说
        print(111, request.user)  # 111 root
        print(111, request.auth)  #  111 None
        return Response({"code": 0, "data": "get"})

完事你通过python manage.py createsuperuser命令创建一个超级用户,并且通过http://127.0.0.1:8000/admin/后台登录成功,然后浏览器访问某个路由,你的页面中右上角就会有当前登录的用户名了。

1832669313232797696.png

如果将认证类写在了全局的drf的配置中,那么意味着认证类将全局有效,如果你想让某个认证类豁免认证或者自定义认证,就额可以在当前认证类中添加如下属性:

python
from rest_framework.views import APIView
from rest_framework.response import Response


class AuthView(APIView):
    """ 视图类 """
    # 如果列表为空,则该视图类不需要认证
    # authentication_classes = []
    # 如果在列表中指定了一个或多个认证类,首先会覆盖掉全局的认证类
    # 其次就是列表中靠前的认证类优先被使用
    authentication_classes = ["xxx认证类"]
    def get(self, request, *args, **kwargs):
        return Response({"code": 0, "data": "get"})

自定义认证

在全局或者局部可以使用drf默认的认证之外,我们也可以自定义认证类。

关于自定义认证类有两种写法。

1. 继承drf的BaseAuthentication

python
from rest_framework.authentication import BaseAuthentication


class CustomAuthentication1(BaseAuthentication):
    """
    自定义认证类,必须继承BaseAuthentication
    这里暂时只需要实现下面两个方法即可
    当然了,你也可以将自定义的认证类单独放到一个文件中,这里写在views中是为了写示例方便
    """
    def authenticate(self, request):
        """
        需要你自己实现的认证逻辑写在这个方法中
        :param request: 
        :return: request.user, request.auth
        """""
        token = request.data.get("token")
        # print(token)  # 764d92bb-88bc-47cf-a2f1-d96d288c171a
        if not token:  # 如果请求没有携带token
            # 抛出异常,终止认证过程并返回认证失败
            raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败"})
        # 如果认证成功,我们将返回下面两个值,这两个值在认证内部的源码部分,分别赋值给
        # request.user
        # request.auth
        return user_obj, token

        # 注意,如果上面的认证逻辑都不写,直接返回两个None,None
        # 表示跳过认证,或者认为认证通过/成功
        # return None, None

    def authenticate_header(self, request):
        """
        如果认证失败,该方法的返回值会当作响应头给用户返回,这里我们先不管,照抄写死即可
        :param request: 
        :return: 
        """""
        return 'Bearer realm="API"'

2. 自己写认证类

这个相比上面那种写法,就是不用继承drf的BaseAuthentication类,但是内部的两个方法还是要写的。

python

class CustomAuthentication2(object):
    """
    自定义认证类,必须继承BaseAuthentication
    这里暂时只需要实现下面两个方法即可
    当然了,你也可以将自定义的认证类单独放到一个文件中,这里写在views中是为了写示例方便
    """
    def authenticate(self, request):
        """
        需要你自己实现的认证逻辑写在这个方法中
        :param request: 
        :return: request.user, request.auth
        """""
        token = request.data.get("token")
        # print(token)  # 764d92bb-88bc-47cf-a2f1-d96d288c171a
        if not token:  # 如果请求没有携带token
            # 抛出异常,终止认证过程并返回认证失败
            raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败"})
        # 如果认证成功,我们将返回下面两个值,这两个值在认证内部的源码部分,分别赋值给
        # request.user
        # request.auth
        return user_obj, token

        # 注意,如果上面的认证逻辑都不写,直接返回两个None,None
        # 表示跳过认证,或者认为认证通过/成功
        # return None, None

    def authenticate_header(self, request):
        """
        如果认证失败,该方法的返回值会当作响应头给用户返回,这里我们先不管,照抄写死即可
        :param request: 
        :return: 
        """""
        return 'Bearer realm="API"'

简单的认证

通过一个简单的认证案例,来快速的了解认证过程中都有哪些必不可少的环节吧。

app01\models.py

python
from django.db import models

class UserInfo(models.Model):
    user = models.CharField(verbose_name='用户名', max_length=32)
    pwd = models.CharField(verbose_name='密码', max_length=32)
    token = models.CharField(verbose_name='token', max_length=64, null=True, blank=True)

    def __str__(self):
        return self.user

然后手动的执行数据库迁移命令:

python manage.py makemigrations
python manage.py migrate

完事手动的往表里录入两条用户信息。

1832669314063269888.png

urls.py

python
from django.contrib import admin
from django.urls import path, re_path, include
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', views.AuthView.as_view()),
    path('api/orders/', views.OrdersView.as_view()),
    path('api/pay/', views.PayView.as_view()),

]

app01\views.py

python
import uuid
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class TokenAuthentication(BaseAuthentication):
    """
    自定义认证类,必须继承BaseAuthentication
    这里暂时只需要实现下面两个方法即可
    当然了,你也可以将自定义的认证类单独放到一个文件中,这里写在views中是为了写示例方便
    """

    def authenticate(self, request):
        """
        需要你自己实现的认证逻辑写在这个方法中
        :param request: 
        :return: 
        """""
        token = request.data.get("token")
        # print(token)  # 764d92bb-88bc-47cf-a2f1-d96d288c171a
        if not token:  # 如果请求没有携带token
            # 抛出异常,终止认证过程并返回认证失败
            raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败"})
        # 如果认证成功,我们将返回下面两个值,这两个值在认证内部的源码部分,分别赋值给
        # request.user
        # request.auth
        return user_obj, token

        # 注意,如果上面的认证逻辑都不写,直接返回两个None,None
        # 表示跳过认证,或者认为认证通过/成功
        # return None, None

    def authenticate_header(self, request):
        """
        如果认证失败,该方法的返回值会当作响应头给用户返回,这里我们先不管,照抄写死即可
        :param request: 
        :return: 
        """""
        return 'Bearer realm="API"'


class AuthView(APIView):
    def post(self, request, *args, **kwargs):
        # print(request.data) # {'username': 'zhangkai', 'password': 123}
        username = request.data.get("username")
        password = request.data.get("password")
        user_obj = models.UserInfo.objects.filter(user=username, pwd=password).first()
        if user_obj:
            token = str(uuid.uuid4())
            user_obj.token = token
            user_obj.save()
            return Response({"code": 0, "data": {"token": token, "username": username}})
        else:
            return Response({"code": 1000, "data": "用户名或者密码错误"})


class OrdersView(APIView):
    # 想要使用认证,按照下面的形式写即可
    # 请求来了会自动的去执行TokenAuthentication类中的认证方法
    # 如果认证成功,会执行当前这个接口的方法
    # 如果认证失败,直接在认证部分就给用户返回了
    authentication_classes = [TokenAuthentication, ]

    def post(self, request, *args, **kwargs):
        # 认证成功就能拿到认证方法返回的两个值
        print(request.user, request.auth)  # zhangkai 764d92bb-88bc-47cf-a2f1-d96d288c171a
        return Response({"code": 0, "data": {"orders": ['o1', 'o2']}})


class PayView(APIView):
    authentication_classes = [TokenAuthentication, ]

    def post(self, request, *args, **kwargs):
        # 认证成功就能拿到认证方法返回的两个值
        print(request.user, request.auth)  # zhangkai 764d92bb-88bc-47cf-a2f1-d96d288c171a
        return Response({"code": 0, "data": {"pay_list": ['p1', 'p2']}})

settigns.py暂时不做配置。

然后你就可以用postman做测试了:

1832669314222653440.png

关于return None的二三事

在视图类的 authentication_classes 中定义认证类时,传入的是一个列表,支持定义多个认证类。

当出现多个认证类时,drf内部会按照列表的顺序,逐一执行认证类的 authenticate 方法,如果 返回元组 或 抛出异常 则会终止后续认证类的执行;如果返回None,则意味着继续执行后续的认证类。

如果所有的认证类authenticate都返回了None,则默认 request.user="AnonymousUser"request.auth=None,也可以通过修改配置文件来修改默认值。

python
REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
}

即,在认证方法中,有三种返回情况:

  • 返回元组。
    • return user_obj, token,这种情况是通过认证了。
    • return None, None,这种是跳过认证或者认证成功,只不过返回的值有点不太对劲就是了,一般不这么用。
  • 抛出异常,终止认证过程并返回认证失败。
    • raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
  • 返回None,这个是返回一个None,跟上面返回两个None还有点区别。
    • 在多个认证类的情况下,表示跳过当前认证,进入下一个认证类。

这里我们来说说返回None的应用场景之一,单个认证类的情况下,当某个API,"已认证"和"未认证"的用户都可以方法时,比如:

  • 已认证用户,访问API返回该用户的所有视频播放记录列表。
  • 未认证用户,访问API返回可预览的视频列表。

注意:不同于之前的案例,之前案例是:必须认证成功后才能访问,而此案例则是已认证和未认证均可访问。

下面的示例,改自于上面的案例,我们以ordes接口来模拟返回视频的场景。

settings.py

python
REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
}

urls.py无需改动。

app01\views.py,只需要调整下面来两个类的代码,其他代码不变。

python
import uuid
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class TokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.data.get("token")
        # print(token)  # 8eb9e7e6-3cdd-4bd2-b7ec-f31bb7f3569
        if not token:  # 如果请求没有携带token,表示未登录的用户,返回可预览的视频列表
            # raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
            return None
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败"})
        # 如果认证成功,返回用户的所有视频列表
        return user_obj, token

    def authenticate_header(self, request):
        return 'Bearer realm="API"'



class OrdersView(APIView):
    authentication_classes = [TokenAuthentication, ]
    def post(self, request, *args, **kwargs):
        # 认证成功就能拿到认证方法返回的两个值
        print(request.user, request.auth)
        if request.user:
            return Response({"code": 0, "data": {"video_list": ['v1', 'v2', 'v3', 'v4']}})
        return Response({"code": 0, "data": {"video_list": ['v1', 'v2']}})

然后就可以测试了:

1832669315271229440.png

多个认证类

一般情况下,编写一个认证类足矣。

当项目中可能存在多种认证方式时,就可以写多个认证类。例如,项目认证支持:

  • 在请求中传递token进行验证。
  • 请求携带cookie进行验证。
  • 请求携带jwt进行验证。
  • 请求携带的加密的数据,需用特定算法解密(一般为app开发的接口都是有加密算法)
  • ...

此时,就可以编写多个认证类,并按照需要应用在相应的视图中。

下面的例子我们做这样的测试:

  • 访问auth接口,生成cookie和token。
  • 访问orders接口,不携带cookie:
    • 携带正确的token,效果就是得到正确的返回,即token认证类生效,不在走cookie认证类。
    • 携带错误的token,没有通过token认证类的认证,直接返回认证失败信息,也不走cookie认证类。
  • 访问pay接口,不携带token,那么token认证类在认证时,发现没有找到token,然后return None,即跳过当前认证类,交给下个认证类认证。
    • 携带正确的cookie,得到正确的返回。
    • 携带错误的cookie,直接返回认证失败信息。
  • 还有一种情况,就是既不携带cookie,也不携带token,同样会认证成功,但request.userrequest.auth都会返回None值,且这个None值,我们可以也可以在配置文件中设置。

上代码,settings.py,保持不变:

python
REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
}

urls.py也保持不变:

python
from django.contrib import admin
from django.urls import path, re_path, include
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', views.AuthView.as_view()),
    path('api/orders/', views.OrdersView.as_view()),
    path('api/pay/', views.PayView.as_view()),
]

app01\views.py就有一些变动了:

python
import uuid
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class CookieAuthentication(BaseAuthentication):
    def authenticate(self, request):
        user_uuid = request.COOKIES.get("user_uuid")
        if not user_uuid:  # 如果请求没有携带cookie,即跳过当前认证类
            return None
        user_obj = models.UserInfo.objects.filter(token=user_uuid).first()
        if not user_obj:  # 如果携带的cookie是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败", "type": "cookie"})
        return user_obj, user_uuid

    def authenticate_header(self, request):
        return 'Bearer realm="API"'


class TokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.data.get("token")
        if not token:  # 如果请求没有携带token,即跳过当前认证类
            return None
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败", "type": "token"})
        return user_obj, token

    def authenticate_header(self, request):
        return 'Bearer realm="API"'


class AuthView(APIView):
    def post(self, request, *args, **kwargs):
        username = request.data.get("username")
        password = request.data.get("password")
        user_obj = models.UserInfo.objects.filter(user=username, pwd=password).first()
        if user_obj:  # 校验成功,生成token和cookie
            token = str(uuid.uuid4())
            user_obj.token = token
            user_obj.save()
            res = Response({"code": 0, "data": {"token": token, "username": username}})
            res.set_cookie('user_uuid', token)
            return res
        else:
            return Response({"code": 1000, "data": "用户名或者密码错误"})


class OrdersView(APIView):
    authentication_classes = [TokenAuthentication, CookieAuthentication]
    def post(self, request, *args, **kwargs):
        return Response({"code": 0, "data": {"video_list": ['v1', 'v2']}})


class PayView(APIView):
    authentication_classes = [TokenAuthentication, CookieAuthentication]
    def post(self, request, *args, **kwargs):
        return Response({"code": 0, "data": {"pay_list": ['p1', 'p2']}})

综上所述,在多个认证类的场景中,如果当前认证类return None表示跳过当前认证类,交给下个认证类处理。

全局配置

当然了,认证也可以全局配置,就是在配置文件中,例如:

python
REST_FRAMEWORK = {
    "UNAUTHENTICATED_USER": lambda: None,
    "UNAUTHENTICATED_TOKEN": lambda: None,
    "DEFAULT_AUTHENTICATION_CLASSES":["你的app名字.app下的某个文件.认证类名","你的app名字.app下的某个文件.认证类名",]
    # 注意,如果是全局配置,认证类不要放在views.py中(我上面那么写是为了省事儿),会发生循环导入进而引发导入报错的问题
    # 你单独创建个文件,存放多个认证类就好了
    # "DEFAULT_AUTHENTICATION_CLASSES": ["app01.auth.CookieAuthentication", "app01.auth.TokenAuthentication"]
}

注意,想要某个接口无需认证,就需要单独在某个接口类中写个空列表,如下:

python
# views.py
class AuthView(APIView): 
    # 认证类列表为空,表示当前接口无需认证
    authentication_classes = []

注意

Failed to load resource: the server responded with a status of 401 ()

这是遇到了401的Unauthorized报错,按照如下方式解决即可。

1832669316470800384.png

权限

认证,根据用户携带的 token/session等获取当前用户信息。

权限,读取认证中获取的用户信息,判断当前用户是否有权限访问,例如:普通用户、管理员、超级用户,不同用户具有不同的权限。

drf中权限控制可以限制用户对于视图的访问和对于具体数据模型对象的访问。

  • 在执行视图的as_view()方法的dispatch()方法前,会先进行视图访问权限的判断
  • 在通过get_object()获取具体模型对象时,会进行模型对象访问权限的判断

默认的drf提供权限有:

  • AllowAny : 允许所有用户访问。
  • IsAuthenticated: 仅允许登录认证通过的用户访问。
  • IsAdminUser: 仅允许管理员用户登录认证通过的用户访问。
  • IsAuthenticatedOrReadOnly: 已经通过登录认证通过的用户可以对数据进行增删改查操作,没有登录认证成功的用户只能查看数据。

基本使用

下面的demo演示了这样的一个场景,两个用户的角色不同,对应的权限不同。

然后这两个用户分别访问auth接口,获取到token,然后再分别向orders和pay接口发起请求,而这两个接口同时要进行认证校验和权限校验。

首先修改app01\modes.py中的表结构。

python
from django.db import models


class UserInfo(models.Model):
    role_choices = ((1, "员工"), (2, "主管"), (3, "CEO"))
    role = models.IntegerField(verbose_name='角色', choices=role_choices, default=1)
    user = models.CharField(verbose_name='用户名', max_length=32)
    pwd = models.CharField(verbose_name='密码', max_length=32)
    token = models.CharField(verbose_name='token', max_length=64, null=True, blank=True)

    def __str__(self):
        return self.user

然后别忘了做数据库迁移和手动的添加一些用户。我这里准备两个用户,张开是员工,李开是主管,用这两个用户去完成接下来的测试过程。

1832669317938806784.png

app01\permissions.py文件(permissions.py文件需要手动创建)中编写权限类:

python
from rest_framework.permissions import BasePermission


class PermissionA(BasePermission):
    """  权限校验类 """
    message = {"code": 1003, "data": "权限校验失败,无权访问"}

    def has_permission(self, request, view):
        """ 自定义权限校验逻辑,校验成功,返回True,否则返回False """
        # 当用户的角色是"主管"时,通过校验
        if request.user.role == 2:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        """ 试图部分再说这个方法有啥用,这里先return True """
        return True

app01\auth.py编写认证类:

python
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class CookieAuthentication(BaseAuthentication):

    def authenticate(self, request):
        user_uuid = request.COOKIES.get("user_uuid")
        print(user_uuid)
        if not user_uuid:
            return None
        user_obj = models.UserInfo.objects.filter(token=user_uuid).first()
        if not user_obj:
            raise AuthenticationFailed({"code": 1002, "data": "认证失败", "type": "cookie"})
        return user_obj, user_uuid

    def authenticate_header(self, request):
        return 'Bearer realm="API"'


class TokenAuthentication(BaseAuthentication):
    def authenticate(self, request):
        """
        需要你自己实现的认证逻辑写在这个方法中
        :param request: 
        :return: 
        """""
        token = request.data.get("token")
        if not token:  # 如果请求没有携带token,表示未登录的用户,返回可预览的视频列表
            # raise AuthenticationFailed({"code": 1001, "data": "认证失败"})
            return None
        user_obj = models.UserInfo.objects.filter(token=token).first()
        if not user_obj:  # 如果携带的token是错误的
            raise AuthenticationFailed({"code": 1002, "data": "认证失败", "type": "token"})
        # 如果认证成功,返回用户的所有视频列表
        return user_obj, token

        # 注意,如果上面的认证逻辑都不写,直接返回两个None,None,表示认证成功
        # return None, None

    def authenticate_header(self, request):
        """
        如果认证失败,该方法的返回值会当作响应头给用户返回,这里我们先不管,照抄写死即可
        :param request: 
        :return: 
        """""
        return 'Bearer realm="API"'

settings.pyurls.py暂时不动:

python
# settings.py
REST_FRAMEWORK = {}

# urls.py
from django.contrib import admin
from django.urls import path, re_path, include
from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', views.AuthView.as_view()),
    path('api/orders/', views.OrdersView.as_view()),
    path('api/pay/', views.PayView.as_view()),
]

app01\views.py

python
import uuid
from rest_framework.response import Response
from rest_framework.views import APIView
# 导入权限和认证类
from app01.permissions import PermissionA
from app01.auth import TokenAuthentication, CookieAuthentication
from app01 import models


class AuthView(APIView):
    authentication_classes = []

    def post(self, request, *args, **kwargs):
        username = request.data.get("username")
        password = request.data.get("password")
        user_obj = models.UserInfo.objects.filter(user=username, pwd=password).first()
        if user_obj:
            token = str(uuid.uuid4())
            user_obj.token = token
            user_obj.save()
            res = Response({"code": 0, "data": {"token": token, "username": username}})
            res.set_cookie('user_uuid', token)
            return res
        else:
            return Response({"code": 1000, "data": "用户名或者密码错误"})


class OrdersView(APIView):
    authentication_classes = [TokenAuthentication]
    # 为该视图加权限类,当然也可以写多个
    permission_classes = [PermissionA]

    def post(self, request, *args, **kwargs):
        if request.user:
            return Response({"code": 0, "data": {"video_list": ['v1', 'v2', 'v3', 'v4']}})
        return Response({"code": 0, "data": {"video_list": ['v1', 'v2']}})


class PayView(APIView):
    authentication_classes = [TokenAuthentication, CookieAuthentication]
    # 为该视图加权限类,当然也可以写多个
    permission_classes = [PermissionA]
    def post(self, request, *args, **kwargs):
        return Response({"code": 0, "data": {"pay_list": ['p1', 'p2']}})

然后通过postman进行测试,结果参考如下图:

1832669318140133376.png

多个权限类

当开发过程中需要用户同时具备多个权限(缺一不可)时,可以用多个权限类来实现。

权限组件内部处理机制:按照列表的顺序逐一执行 has_permission 方法,如果返回True,则继续执行后续的权限类;如果返回None或False,则抛出权限异常并停止后续权限类的执行。

接下来的示例,我们继续修改表结构,使其可以为用户绑定多个权限。

app01\models.py

python
from django.db import models


class Role(models.Model):
    """  角色表 """
    title = models.CharField(verbose_name='角色名称', max_length=32)

    def __str__(self):
        return self.title


class UserInfo(models.Model):
    """ 用户表 """
    user = models.CharField(verbose_name='用户名', max_length=32)
    pwd = models.CharField(verbose_name='密码', max_length=32)
    token = models.CharField(verbose_name='token', max_length=64, null=True, blank=True)
    roles = models.ManyToManyField(verbose_name='角色', to='Role')

    def __str__(self):
        return self.user

别忘了数据库迁移命令,然后手动调整下用户数据。

1832669318920273920.png

settings.pyurls.pyapp01\auth.py这几个文件代码都不变。

app01\permissions.py

python
from rest_framework.permissions import BasePermission


class PermissionA(BasePermission):
    """  权限校验类 """
    message = {"code": 1003, "data": "权限校验失败,无权访问"}

    def has_permission(self, request, view):
        """ 自定义权限校验逻辑,校验成功,返回True,否则返回False """
        # 当用户的角色是"主管"时,通过校验
        obj = request.user.roles.filter(title="主管").exists()
        if obj:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        """ 视图部分再说这个方法有啥用,这里先return True """
        return True


class PermissionB(BasePermission):
    """  权限校验类 """
    message = {"code": 1003, "data": "权限校验失败,无权访问"}

    def has_permission(self, request, view):
        """ 自定义权限校验逻辑,校验成功,返回True,否则返回False """
        # 当用户的角色是"主管"时,通过校验
        obj = request.user.roles.filter(title="CEO").exists()
        if obj:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        """ 试图部分再说这个方法有啥用,这里先return True """
        return True

app01\views.py

python
import uuid
from rest_framework.response import Response
from rest_framework.views import APIView
from app01.permissions import PermissionA, PermissionB
from app01.auth import TokenAuthentication, CookieAuthentication
from app01 import models


class AuthView(APIView):
    authentication_classes = []

    def post(self, request, *args, **kwargs):
        username = request.data.get("username")
        password = request.data.get("password")
        user_obj = models.UserInfo.objects.filter(user=username, pwd=password).first()
        if user_obj:
            token = str(uuid.uuid4())
            user_obj.token = token
            user_obj.save()
            res = Response({"code": 0, "data": {"token": token, "username": username}})
            res.set_cookie('user_uuid', token)
            return res
        else:
            return Response({"code": 1000, "data": "用户名或者密码错误"})


class OrdersView(APIView):
    authentication_classes = [TokenAuthentication]
    # 只有一个权限校验类
    permission_classes = [PermissionA]

    def post(self, request, *args, **kwargs):
        if request.user:
            return Response({"code": 0, "data": {"video_list": ['v1', 'v2', 'v3', 'v4']}})
        return Response({"code": 0, "data": {"video_list": ['v1', 'v2']}})


class PayView(APIView):
    authentication_classes = [TokenAuthentication, CookieAuthentication]
    # 有两个权限校验类
    permission_classes = [PermissionA, PermissionB]
    def post(self, request, *args, **kwargs):
        return Response({"code": 0, "data": {"pay_list": ['p1', 'p2']}})

如下截图,zhangkai只能访问其中一个接口。

1832669319188709376.png

likai能访问两个接口,因为他同时具有主管和CEO的权限,截图就省略了。

所以,多个权限类的情况,要所有的权限类都必须通过,才可以通过,只通过其中一个或者多个,都不行。

全局配置

无需多说,就是在settings中进行配置了:

python
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ['app01.permissions.PermissionA', 'app01.permissions.PermissionB'],
}

然后视图中就不用单独写了,那么所有的视图都应用上了。你也可以为单独的视图单独配置以覆盖全局的配置,或者让其列表为空,使其跳过权限校验。

限流

https://q1mi.github.io/Django-REST-framework-documentation/api-guide/throttling_zh/

限流(Throttle)就是限制客户端对server端API的访问频率。

可选的限流类

DRF中有三种限流类(限流器):

  • AnonRateThrottle,限制所有匿名未认证用户,使用IP区分用户。使用DEFAULT_THROTTLE_RATES['anon'] 来设置频次。
  • UserRateThrottle,限制认证用户,使用User id 来区分。使用DEFAULT_THROTTLE_RATES['user'] 来设置频次。
  • ScopedRateThrottle,限制用户对于每个视图的访问频次进行自定义限制。

限流频率

DRF限制频率的指定格式为 "最大访问次数/时间间隔",例如设置为 5/min,则只允许一分钟内最多调用接口 5 次。

其他时间间隔的写法可以是:

's'		'sec' 	 'second'
'm'		'min'	 'minute'
'h'		'hour' 
'd'		'day'

设置缓存

默认的DRF的限流类使用django的缓存,即内存缓存,所以只需要在settings.py中添加:

python
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',  # 唯一标识的key
    }
}

你也可以指定其他的缓存类型,参考:https://www.cnblogs.com/Neeo/articles/15846153.html

全局使用限流类

无需多言,在settings.py中进行配置:

python

自定义限流类

https://www.bilibili.com/video/BV18Q4y1y7E7?p=12&vd_source=f56f96c7f7894594fdc04129b7d97ff6

快速使用

首先要搭配django的缓存,settings.py

python
# drf相关配置以后编写在下面的字典中
REST_FRAMEWORK = {}

# 限流搭配的缓存
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'uniqueThrottle',  # 唯一标识的key
    }
}

多个限流类

适用于不同的频次,比如一分钟十次,一个小时500次这种不同频次的设置。

注意,多个限流类时,每个限流类的scope名字都不能相同。

分页

filtering

异常处理