Django REST Framework完整教程-认证与权限-JWT的使用-CSDN博客


阅读本文之前请读者先阅读 https://plugin.blog.csdn.net/article/details/133853377如果已经知晓Django REST Framework的基础可以继续阅读本文内容。

1.认证(Authentication)与权限(Permission)

认证(Authentication)与权限(Permission)不是一回事。认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。权限(Permission)的校验发生验证用户身份以后是由系统根据分配权限确定用户可以访问何种资源以及对这种资源进行何种操作这个过程也被称为授权(Authorization)。
无论是Django还是DRF, 当用户成功通过身份验证以后系统会把已通过验证的用户对象与request请求绑定这样一来你就可以使用request.user获取这个用户对象的所有信息了。

在前面的教程中我们编写ArticleList和ArticleView两个基于类的视图(如下所示)。前者如果收到GET请求会返回文章资源列表如果收到POST请求则添加文章后者如果收到GET请求就返回单篇文章资源如果收到PUT或DELETE请求就对文章资源进行修改或删除。

# coding=utf-8
from .models import Article
from .serializers import ArticleSerializer

# generic class-based views
from rest_framework import generics

class ArticleList(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

    # 将request.user与author绑定
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class =ArticleSerializer

以上两个视图其实是有很大问题的因为任何用户包括匿名用户也可以对文章资源进行修改。比如当你访问单篇文章资源时你不仅可以看到红色的delete按钮和修改文章内容的表单而且可以在未登录的情况对它们进行操作。

1.1.视图添加权限

在Django传统视图开发中你可能会使用@login_required和@permission_required这样的装饰器要求用户先登录或进行权限验证。在DRF中你不需要做这是因为REST framework 包含许多默认权限类我们可以用来限制谁可以访问给定的视图。在这种情况下我们需要的是 IsAuthenticatedOrReadOnly 类它将确保经过身份验证的请求获得读写访问权限未经身份验证的请求将获得只读读的权限。
现在修改我们的视图(zlblog/views.py)添加如下代码:

from rest_framework import generics
from rest_framework import permissions
from .permissions import IsOwnerOrReadOnly

class ArticleList(generics.ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

    # important
    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class =ArticleSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)

此时再访问文章资源列表或单篇文章资源时你会看到红色的delete按钮和添加修改表单都已消失。当你重新登录验证身份后你又可以看到delete按钮和修改表单了。

1.2.登录验证

你可能会问DRF中用户应该访问哪个url登录验证身份呢? 是admin吗当然不是admin页面因为只有管理员才能通过admin页面登录。DRF中你可以将登录页面api-auth添加到你的项目urls中如下所示

#apiproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('v1/', include('zlblog.urls')),#包含zlblog中的urls
    path('api-auth/', include('rest_framework.urls')), # 用户登录页面
]

然后访问http://127.0.0.1:8000/api-auth/login/你就可以看到专门的DRF的登录页面了如下所示
在这里插入图片描述

1.3.常用DRF自带权限类

除了IsAuthenticatedOrReadOnly 类DRF自带的常用权限类还包括

IsAuthenticated类仅限已经通过身份验证的用户访问
AllowAny类允许任何用户访问
IsAdminUser类仅限管理员访问
DjangoModelPermissions类只有在用户经过身份验证并分配了相关模型权限时才会获得授权访问相关模型。
DjangoModelPermissionsOrReadOnly类与前者类似但可以给匿名用户访问API的可读权限。
DjangoObjectPermissions类只有在用户经过身份验证并分配了相关对象权限时才会获得授权访问相关对象。通常与django-gaurdian联用实现对象级别的权限控制。

1.4.自定义权限类

IsAuthenticatedOrReadOnly 类并不能实现只有文章 article 的创建者才可以更新或删除它这时我们还需要自定义一个名为IsOwnerOrReadOnly 的权限类把它加入到ArticleDetail视图里。
首先我们在zlblog文件夹下创建permissions.py添加如下代码

# coding=utf-8
from rest_framework import permissions

class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    自定义权限只允许对象的创建者才能编辑它。
    """
    def has_object_permission(self, request, view, obj):
        # 读取权限被允许用于任何请求
        # 所以我们始终允许 GETHEAD 或 OPTIONS 请求。
        if request.method in permissions.SAFE_METHODS:
            return True
        # 写入权限只允许给 article 的作者。
        return obj.author == request.user

然后修改我们的视图IsOwnerOrReadOnly 的权限类把它加入到ArticleDetail视图的permission_classes里。这样就完美实现了文初我们想要实现的三个功能。DRF支持权限类的插拔是不是很帅?

1.5.全局权限

在前面的案例中我们都是在基于类的API视图里通过permission_classes属性设置的权限类。如果你有些权限是全局或全站通用的你还可以在settings.py中使用 DEFAULT_PERMISSION_CLASSES 全局设置默认权限策略。

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

如果未指定则此设置默认为允许无限制访问

'DEFAULT_PERMISSION_CLASSES': (
   'rest_framework.permissions.AllowAny',
)

1.6.函数视图权限

如果你习惯使用基于函数的视图编写API你可以按如下方式给你的函数视图添加权限。

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
    content = {
        'status': 'request was permitted'
    }
    return Response(content)

注意当通过类属性或装饰器设置新的权限类时您会告诉视图忽略 settings.py 文件上设置的默认列表。

2.认证详解

身份验证是将传入的请求对象(request)与一组标识凭据例如请求来自的用户或其签名的令牌token相关联的机制。REST framework 提供了一些开箱即用的身份验证方案并且还允许你实现自定义方案。

DRF的每个认证方案实际上是一个类。你可以在视图中使用一个或多个认证方案类。REST framework 将尝试使用列表中的每个类进行身份验证并使用成功完成验证的第一个类的返回的元组设置 request.user 和request.auth。

用户通过认证后request.user返回Django的User实例否则返回AnonymousUser的实例。request.auth通常为None。如果使用token认证request.auth可以包含认证过的token。

2.1.认证方案

Session认证SessionAuthentication类此认证方案使用Django的默认session后端进行身份验证。当客户端发送登录请求通过验证后Django通过session将用户信息存储在服务器中保持用户的请求状态。Session身份验证适用于与你的网站在相同的Session环境中运行的AJAX客户端 (注这也是Session认证的最大弊端)。

基本认证BasicAuthentication类此认证方案使用HTTP 基本认证针对用户的用户名和密码进行认证。使用这种方式后浏览器会跳出登录框让用户输入用户名和密码认证。基本认证通常只适用于测试。

远程认证RemoteUserAuthentication类此认证方案为用户名不存在的用户自动创建用户实例。这个很少用具体见文档。

Token认证TokenAuthentication类该认证方案是DRF提供的使用简单的基于Token的HTTP认证方案。当客户端发送登录请求时服务器便会生成一个Token并将此Token返回给客户端作为客户端进行请求的一个标识以后客户端只需带上这个Token前来请求数据即可无需再次带上用户名和密码。后面我们会详细介绍如何使用这种认证方案。

注意如果你在生产环境下使用BasicAuthentication和TokenAuthentication认证你必须确保你的API仅在https可用。

如何在DRF中使用你的认证方案
方式1settings.py中设置默认的全局认证方案

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )}

**方式2基于类的视图(CBV)中使用-authentication_classes **

from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)

方式3函数视图中使用

@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
    content = {
        'user': unicode(request.user),  # `django.contrib.auth.User` 实例。
        'auth': unicode(request.auth),  # None
    }
    return Response(content)

如何自定义认证方案?
要实现自定义的认证方案要继承BaseAuthentication类并且重写.authenticate(self, request)方法。如果认证成功该方法应返回(user, auth)的二元元组否则返回None。
以下示例将以自定义请求标头中名称为’X_USERNAME’提供的用户名作为用户对任何传入请求进行身份验证其它类似自定义认证需求比如支持用户同时按用户名或email进行验证。

from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions

class ExampleAuthentication(authentication.BaseAuthentication):
    def authenticate(self, request):
        username = request.META.get('X_USERNAME')
        if not username:
            return None

        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            raise exceptions.AuthenticationFailed('No such user')

        return (user, None)

2.2.如何使用TokenAuthentication

DRF自带的TokenAuthentication方案可以实现基本的token认证整个流程如下
首先你需要将修改settings.py, 加入如下app。

INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
    )

后文再单独讲解一个例子。

3.JSON Web Token(JWT)认证

然而JSON Web Token(JWT)是一种更新的使用token进行身份认证的标准。与DRF内置的TokenAuthentication方案不同JWT身份验证不需要使用数据库来验证令牌, 而且可以轻松设置token失效期或刷新token, 是API开发中当前最流行的跨域认证解决方案。本文将详细介绍JWT认证的工作原理以及如何通过djangorestframework-simplejwt 这个第三方包轻松实现JWT认证。

3.1.工作原理

JSON Web TokenJWT是一种开放标准它定义了一种紧凑且自包含的方式用于各方之间安全地将信息以JSON对象传输。由于此信息是经过数字签名的因此可以被验证和信任。JWT用于为应用程序创建访问token通常适用于API身份验证和服务器到服务器的授权。那么如何理解紧凑和自包含这两个词的含义呢?

  • 紧凑就是说这个数据量比较少可以通过url参数http请求提交的数据以及http header多种方式来传递。
  • 自包含这个字符串可以包含很多信息比如用户id用户名订单号id等如果其他人拿到该信息就可以拿到关键业务信息。

那么JWT认证是如何工作的呢? 首先客户端提交用户登录信息验证身份通过后服务器生成一个用于证明用户身份的令牌(token)也就是一个加密后的长字符串并将其发送给客户端。在后续请求中客户端以各种方式(比如通过url参数或者请求头)将这个令牌发送回服务器服务器就知道请求来自哪个特定身份的用户了。
在这里插入图片描述
JSON Web Token由三部分组成这些部分由点.分隔分别是header(头部)payload(有效负载)和signature(签名)。

header(头部): 识别以何种算法来生成签名
pyload(有效负载): 用来存放实际需要传递的数据
signature(签名): 安全验证token有效性防止数据被篡改。

通过http传输的数据实际上是加密后的JWT它是由两个点分割的base64-URL长字符串组成解密后我们可以得到header, payload和signature三部分。我们可以简单的使用 https://jwt.io/ 官网来生成或解析一个JWT如下所示
在这里插入图片描述

接下来我们将使用django-rest-framework-simplejwt这个第三方软件包进行JWT身份验证。
django-rest-framework-simplejwt为Django REST框架提供了JSON Web令牌认证后端。它提供一组保守的默认功能来涵盖了JWT的最常见用例。它还非常容易扩展。

3.2.安装

pip install djangorestframework-simplejwt

3.3.使用

首先我们需要告诉DRF我们使用jwt认证作为后台认证方案。修改apiproject/settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

其次我们需要提供用户可以获取和刷新token的urls地址这两个urls分别对应TokenObtainPairView和TokenRefreshView两个视图。

# coding=utf-8
# apiproject/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('v1/', include('zlblog.urls')), #包含其它页面的路由地址
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

最后我们可以开始使用postman测试了。通过POST方法发送登录请求到http://127.0.0.1:8000/token/, 请求数据包括username和password。如果登录成功你将得到两个长字符串一个是access token(访问令牌)还有一个是refresh token(刷新令牌)如下所示
在这里插入图片描述
但是这里引发了跨域问题。安装跨越库文件

pip install django-cors-headers

修改setting.py文件有两处如下所示

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',  # 注册跨域app
    'zlblog',
    'rest_framework.authtoken'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',  # 跨域中间件
    'django.middleware.locale.LocaleMiddleware',        #支持中文语言
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

然后再用postman仿真就可以得到结果
在这里插入图片描述
修改上文有一个受保护的视图(比如这里的/articles/)权限(permission_classes)是IsAuthenticated只有验证用户才可访问。如果未授权返回如下结果
在这里插入图片描述
访问这个保护视图时你只需要在请求头的Authorization选项里输入你刚才获取的access token即可如下所示
在这里插入图片描述
DRF接口会自定验证token的有效性。
不给这个access token默认只有5分钟有效。5分钟过后当你再次访问保护视图时你将得到如下token已失效或过期的提示。
那么问题来了Simple JWT中的access token默认有效期是5分钟那么refresh token默认有效期是多长呢? 答案是24小时

3.4.更改Simple JWT的默认设置

Simple 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),
    # 用于生成访问令牌和刷新令牌的序列化器。
    "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",
}

如果要覆盖Simple JWT的默认设置可以修改settings.py, 如下所示。下例将refresh token的有效期改为了15天。

from datetime import timedelta

SIMPLE_JWT = {
    'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
    'ROTATE_REFRESH_TOKENS': True,
}

3.5.自定义令牌(token)

如果你对Simple JWT返回的access token进行解码你会发现这个token的payload数据部分包括token类型token失效时间jti(一个类似随机字符串和user_id。如果你希望在payload部分提供更多信息比如用户的username这时你就要自定义令牌(token)了。
首先编写你的zlblog/seralizers.py添加如下代码。该序列化器继承了TokenObtainPairSerializer类。

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super(MyTokenObtainPairSerializer, cls).get_token(user)

        # Add custom claims
        token['username'] = user.username
        return token

其次不使用Simple JWT提供的默认视图使用自定义视图。修改zlblog/views.py, 添加如下代码

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.permissions import AllowAny
from .serializers import MyTokenObtainPairSerializer

class MyObtainTokenPairView(TokenObtainPairView):
    permission_classes = (AllowAny,)
    serializer_class = MyTokenObtainPairSerializer

最后修改zlblog/urls.py, 添加如下代码将/token/指向新的自定义的视图。注意本例中的app名为zlblog所以是从zlblog.views导入的MyObtainTokenPairView。

# coding=utf-8
from django.contrib import admin
from django.urls import path, include
from zlblog.views import MyObtainTokenPairView
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('token/', MyObtainTokenPairView.as_view(), name='token_obtain_pair'),#自定义token
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('v1/', include('zlblog.urls')), #包含其它页面的路由地址
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

重新发送POST请求到/token/你将获得新的access token和refresh token。采用 https://jwt.io/ 解码在负载部分将得到新的字段名
在这里插入图片描述
对重新获取的access token进行解码你将看到payload部分多了username的内容是不是很酷? 在实际API开发过程中通过Json Web Token传递更多数据非常有用。

3.6.自定义认证后台(Backend)

上面的演示案例是通过用户名和密码登录的如果我们希望后台同时支持邮箱/密码手机/密码组合登录怎么办? 这时我们还需要自定义认证后台(Backend)。
首先修改zlblog/views.py, 添加如下代码

# coding=utf-8
from django.contrib.auth.backends import ModelBackend
#django的Q对象将SQL表达式封装在Python对象中该对象可用于与数据库相关的操作。使用Q对象我们可以使用更少和更简单的代码进行复杂查询。
from django.db.models import Q
from django.contrib.auth import get_user_model

User = get_user_model()

class MyCustomBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        try:
            user = User.objects.get(Q(username=username) | Q(email=username) )
            if user.check_password(password):
                return user
        except Exception as e:
            return None

其次修改zlblog/settings.py, 把你自定义的验证方式添加到AUTHENTICATION_BACKENDS里去。

AUTHENTICATION_BACKENDS = (
    'zlblog.views.MyCustomBackend',
)

修改好后你使用postman发送邮箱/密码组合到/token/将同样可以获得access token和refresh token。是不是又学到了一招?

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go

“Django REST Framework完整教程-认证与权限-JWT的使用-CSDN博客” 的相关文章