菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
0
0

一步步教你如何在 Django REST API 中构建使用 JWT 验证

原创
05/13 14:22
阅读数 795

基于令牌的身份验证,允许后端服务与前端(无论是web端,原生移动端或其他端)分离,并驻留在不同域中。 JSON Web Tokens (JWT) 是一种流行的令牌认证实现, 在本文中,我们使用它来验证,通过Django REST 框架.构建notes 的 API中的用户。

我们将设置用户注册和身份验证,并定义 notes 模型。确保当前登录用户对他创建的便笺有所有权,而且用户只能对自己的便笺进行读写操作。

最终项目的完整源代码在此: https://github.com/bonfirealgorithm/notes-api

1. 创建 Django 项目

$ mkdir notes && cd notes

创建虚拟环境并激活它。

$ python -m venv venv
$ source venv/bin/activate

安装 Django, DjangoREST 和 django-cors-headers:

$ pip install django django-rest-framework django-cors-headers

执行上面的命令安装 Django 以及 Django REST Framework, 帮助我们实现API。我们也安装了 django-cors-headers, 实现了跨域的功能。

接下来我们来创建一个新的Django项目:

$ django-admin startproject project .# 在当前目录内部创建名称为project的项目

上面这行命令最后的 . 表示将在当前目录内部创建Django项目。

之后我们可以通过以下命令来创建一个名为 notes 的 app,同时生成数据库迁移文件:

$ python manage.py startapp notes  # 创建名为 notes 的 app
$ python manage.py migrate # 执行数据库迁移文件,修改数据库

修改 project 下的 settings.py 配置文件,将新创建的 app 都加入到 installed apps 目录中,同时添加 REST Framework 相关配置信息:

project/settings.py

# project/settings.py

INSTALLED_APPS = [                           
    "django.contrib.admin",                              
    "django.contrib.auth",                          
    "django.contrib.contenttypes",                          
    "django.contrib.sessions",                          
    "django.contrib.messages",                          
    "django.contrib.staticfiles",                          
    "rest_framework",  # 新加入 rest_framework 应用               
    "corsheaders",  # 新加入 corsheaders 应用            
    "notes",  # 新加入 notes 应用            
]

MIDDLEWARE = [                          
    "corsheaders.middleware.CorsMiddleware",  # 配置跨域中间件
    "django.middleware.security.SecurityMiddleware",                          
    "django.contrib.sessions.middleware.SessionMiddleware",                          
    "django.middleware.common.CommonMiddleware",                          
    "django.middleware.csrf.CsrfViewMiddleware",                          
    "django.contrib.auth.middleware.AuthenticationMiddleware",                          
    "django.contrib.messages.middleware.MessageMiddleware",                          
    "django.middleware.clickjacking.XFrameOptionsMiddleware",                       
]   

# 允许所有客户端跨域                    
CORS_ORIGIN_ALLOW_ALL = True                                               

REST_FRAMEWORK = {                          
    "DEFAULT_PERMISSION_CLASSES":
        ["rest_framework.permissions.AllowAny",],                          
    "DEFAULT_PARSER_CLASSES":["rest_framework.parsers.JSONParser",],                    
    }

在确保配置无误的情况下,开始运行服务器:

$ python manage.py runserver # 运行 Django 服务器

在浏览器中访问 http://localhost:8000

你应该会看到 Django 的欢迎界面。

但我们仍然没有数据和 API 相关代码 来驱动 API 服务,让我们先创建一个名为 notes 的模型类。

2. 创建一个便签模型和超级用户

在 notes/models.py 文件中添加如下内容:

from django.db import models
from django.contrib.auth.models import User


class Note(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    title = models.CharField(max_length=200)
    content = models.TextField()
    def __str__(self):
        return self.title

我们创建了一个有三个属性的便签模型。它的拥有者是一个用户类型的对象,这个对象我们通过 Django 的认证组件引入。稍后,当我们添加完认证,我们将保证这个拥有者设置到当前登录的用户。剩下的两个属性是便签的标题和内容,分别是一个短字符串和一个长文本字段。

同时注册这个模型到管理后台文件 notes/admin.py :

from django.contrib import admin
from .models import Note

admin.site.register(Note)

由于我们已创建了一个新的模型类,因此需要通过创建并运行数据库迁移文件,使得数据库和模型类同步:

$ python manage.py makemigrations  # 生成数据库迁移文件
$ python manage.py migrate # 根据数据库执行文件修改数据库

让我们来创建一个管理员账号,这样就可以在后台管理面板中检查并添加 notes 对象。(你可以选择默认的用户名,并在出现提示时跳过电子邮箱输入。)

$ python manage.py createsuperuser # 创建管理员账号

让我们再次启动服务器。

$ python manage.py runserver # 启动服务器

然后访问 http://localhost:8000/admin/,并用之前创建的管理员账号密码登录。

你应该会看到如下的画面:

添加一些 notes 信息,以便我们来处理相关数据。

3. 用 Notes 数据构建 API

现在我们需要添加一些请求地址,这样就能访问到 notes 相关 API 了。

修改 project/urls.py 文件,如下所示:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls), # 后台管理
    path('api/', include('notes.urls')) # API 服务,include 用于 urls 分发
]

接下来在 notes 目录下新建 urls.py 文件,加入以下内容:

from django.urls import path
from rest_framework.routers import SimpleRouter
from .views import NoteViewSet

router = SimpleRouter() # 创建 SimpleRouter() 对象
router.register('notes', NoteViewSet, base_name="notes") # 注册路由
urlpatterns = router.urls # 将路由加入到 urls 中

我们正在使用 rest_framework 的 SimpleRouter 来自动创建路由。尽管我们还未创建 NoteViewSet,但我们已经导入了它。 接下来我们将创建 NoteViewSet , 在这之前,首先需要为我们的模型类创建一个序列化类。

rest_framework 中的序列化类提供了序列化功能,可以将模型中的数据转换成适合 API 的格式,也就是所谓的 JSON。 序列化类能执行数据验证,并根据指定字段生成序列化数据。

在 notes 目录下新建 serializers.py 文件:

from rest_framework import serializers
from .models import Note


class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ("id", "owner", "title", "content") # 模型中需要序列化的字段
        model = Note # 指定模型类

接下来我们会在 notes/views.py 中创建视图类:

# notes/views.py
from rest_framework import viewsets
from .models import Note
from .serializers import NoteSerializer

class NoteViewSet(viewsets.ModelViewSet):
    queryset = Note.objects.all() # 指定 queryset
    serializer_class = NoteSerializer # 指定序列化类

确保你已经以管理员身份成功登录后台, 然后访问 http://localhost:8000/api/notes/

你应该能看到已经创建的 notes 数据,还能添加新的 note 数据。

4. 认证和权限

为了便于研究认证和权限,需要在后台管理面板中创建新用户。(你只需要添加用户名和密码就可以了。)

然后在 project/urls.py 文件中的 urlpatterns 列表中添加以下内容:

path('auth/', include('rest_framework.urls')),

这样就能确保我们拥有可以使用 django_rest 登录的请求路径。你会在右上角看到一个根据用户名显示的向下的箭头:

点击它可以注销用户,也能切换用户。

现在有一个很明显的问题,就是说每个用户都能看到其他用户的 notes 数据。 更糟糕的是,未登录的用户也能看到甚至创建 note 数据。(你可以先登录然后访问 http://localhost:8000/api/notes/ 来验证这个问题。)

让我们来解决这个问题。

为什么未登录的用户也有权访问 notes 数据呢?因为我们在 rest_framework 设置了 AllowAny。所以需要进入 project/settings.py 文件,进行以下更改:

# project/settings.py

REST_FRAMEWORK = {                          
    "DEFAULT_PERMISSION_CLASSES":
        ["rest_framework.permissions.IsAuthenticated",], # 修改权限为认证过才能访问                       
    "DEFAULT_PARSER_CLASSES":["rest_framework.parsers.JSONParser",],                    
}

然后尝试访问 http://localhost:8000/api/notes/

你应该无法看到任何 notes 数据了, 但是会收到一条消息:“detail”: “Authentication credentials were not provided.”(未提供认证凭据)

重新登录,你会看到 notes 相关数据。我们该如何解决登录用户可以查看其他用户的 notes 数据这一问题呢?

对 notes/views.py 文件做出以下更改:

# notes/views.py

from rest_framework import viewsets
from rest_framework import permissions
from .models import Note
from .serializers import NoteSerializer
from rest_framework.exceptions import PermissionDenied


class IsOwner 的类(permissions.BasePermission):,用于权限校验。这样就能确保

    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user


class NoteViewSet(viewsets.ModelViewSet):
    serializer_class = NoteSerializer
    permission_classes = (IsOwner,)

    # 确保用户只能看到自己的 Note 数据。
    def get_queryset(self):
        user = self.request.user
        if user.is_authenticated:
            return Note.objects.filter(owner=user)
        raise PermissionDenied()

    # 设置当前用户为 Note 对象的所有者
    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

我们在类中移除了 queryset 属性并重写了 get_queryset 方法,这样就可以过滤 notes 数据,确保只返回属于当前用户的 note 数据。

此外我们添加了一个 IsOwner 类,用于权限校验。这样就能确保用户只能修改(更新/删除)他自己的相关数据。

最后我们重写了 perform_create 方法,这样就能保证创建一个新的 note 对象时,它的所有者始终是当前用户。

如果你访问 http://localhost:8000/api/notes/ ,那应该只能看到属于当前登录用户的 notes 数据。如果你尝试创建一个新的 note 对象,并将它的所有者设置为其他用户(非当前登录用户),也能成功创建 note 对象,但无论如何,这个 note 对象中的所有者仍然会是当前登录用户。

完成这些修改之后,由于用户只能处理自己的 note 数据,所以我们需要在序列化类中删除 owner 字段。对 notes/serializers.py 文件做出如下更改:

# notes/serializers.py

from rest_framework import serializers
from .models import Note


class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        fields = ("id", "title", "content") # 删除 owner 字段
        model = Note

我们现在已经实现了权限和认证功能,但 django_rest 仍然在使用 session 认证方式。

我们想要实现的是,对于任何客户端,无论是浏览器端还是其他客户端,都能够进行用户注册,登录,注销,在任何地方都能进行认证。为此,我们将使用 JWT 认证方式。

5. 加入 JWT

为了用JWT实现token(令牌)认证,我们将要用到一个库, Simple JWT:

$ pip install djangorestframework_simplejwt

然后我们需要在project/settings.py文件中,将其添加到认证类的列表中:

# project/settings.py

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [ 
        'rest_framework.permissions.IsAuthenticated',
    ],
    "DEFAULT_PARSER_CLASSES": [
        "rest_framework.parsers.JSONParser", 
    ],
    "DEFAULT_AUTHENTICATION_CLASSES":                                 # 新添加
        "rest_framework.authentication.SessionAuthentication",        # 新添加
        "rest_framework_simplejwt.authentication.JWTAuthentication",  # 新添加 
    ],
}

并且添加一个新端点到project/urls.py文件中:

# project/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('notes.urls')),
    path('auth/', include('rest_framework.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # 新添加
    path('api/refresh/', TokenRefreshView.as_view(), name='token_refresh'),      # 新添加
]

这些新的端点提供了用户登录所需要的一些东西。登录路由应当为/api/token/。而/api/refresh/路由则用于在旧的token(令牌)过期前获取新的token(令牌)。

事实上,我们并不需要为退出登录设置一个端点,因为服务器并不会维护这个状态。退出登录,我们只需要删除客户端上的token(令牌)值就行。token(令牌)就会"自动"过期失效(可以使用Simple JWT设置来设置时间)。

但是我们眼下还缺少的是注册用户和返回JWT token的方法。

用户注册

这不属于Notes应用,因为这是另一个域,即身份验证。 因此,我们将从创建一个新应用 jwtauth 开始:

$ python manage.py startapp jwtauth

将应用添加到 project/settings.py:

# project/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages", 
    "django.contrib.staticfiles",
    "rest_framework",
    "corsheaders",
    "notes",
    "jwtauth", # 新添加
]

接下来我们需要序列化User对象. 新建 jwtauth/serializers.py:

# jwtauth/serializers.py

from django.contrib.auth import get_user_model
from rest_framework import serializers

User = get_user_model()


class UserCreateSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=True, style={
                                     "input_type":   "password"})
    password2 = serializers.CharField(
        style={"input_type": "password"}, write_only=True, label="Confirm password")

    class Meta:
        model = User
        fields = [
            "username",
            "email",
            "password",
            "password2",
        ]
        extra_kwargs = {"password": {"write_only": True}}

    def create(self, validated_data):
        username = validated_data["username"]
        email = validated_data["email"]
        password = validated_data["password"]
        password2 = validated_data["password2"]
        if (email and User.objects.filter(email=email).exclude(username=username).exists()):
            raise serializers.ValidationError(
                {"email": "Email addresses must be unique."})
        if password != password2:
            raise serializers.ValidationError(
                {"password": "The two passwords differ."})
        user = User(username=username, email=email)
        user.set_password(password)
        user.save()
        return user

我们将使用Django自带的User模型,通过get_user_model()方法来调用。用这种方式导入是一个好的习惯,而不是直接导入User模型,因为这可以保证尽管我们已经对其进行了自定义,我们也能得到当前有效的用户模型。

我们也可以重写create()方法,并检验确认密码是否一致,以及没有其他用户使用同一个邮箱地址。

接下来我们要添加一个视图到 jwtauth/views.py 中:

# jwtauth/views.py

from django.contrib.auth import get_user_model
from rest_framework import permissions
from rest_framework import response, decorators, permissions, status
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import UserCreateSerializer

User = get_user_model()

@decorators.api_view(["POST"])
@decorators.permission_classes([permissions.AllowAny])
def registration(request):
    serializer = UserCreateSerializer(data=request.data)
    if not serializer.is_valid():
        return response.Response(serializer.errors, status.HTTP_400_BAD_REQUEST)        
    user = serializer.save()
    refresh = RefreshToken.for_user(user)
    res = {
        "refresh": str(refresh),
        "access": str(refresh.access_token),
    }
    return response.Response(res, status.HTTP_201_CREATED)

这是我们首次使用基于view的函数视图而不是基于view的类视图。我们选择一个函数视图是因为它只需要响应POST这个http动词,并且我们使用了装饰器来确保它,以及设置了settings.py文件中定义的权限中的异常,以允许任何人仅访问此端点。

我们进行检查以查看序列化程序是否已验证了我们获得的数据,如果未返回,则返回错误对象。 如果一切正常,我们将保存序列化程序,该序列化程序将返回新创建的用户对象。 然后,我们可以获得该用户的JWT令牌并返回它。

我们需要创建一个urls文件,并向其中添加视图, jwtauth/urls.py:

# jwtauth/urls.py

from django.conf.urls import path
from .views import registration

urlpatterns = [
    path('register/', registration, name='register')
]

最后,在project / urls.py中包含 jwtauth 路由:

# project/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('notes.urls')),
    path('auth/', include('rest_framework.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('api/jwtauth/', include('jwtauth.urls'), name='jwtauth'), # 新添加
]

就是这样,我们现在会有一个新的端点: http://localhost:8000/api/jwtaut/register/

我们可以在Postman中测试一下:

如果验证通过,它将返回带有刷新以及访问令牌的对象。

6. 添加 Swagger 文档

最后我们要做的事情就是添加Swagger文档,这样API用户才能看到哪些端点是可用的。

$ pip install django-rest-swagger

将其添加到project/settings.py的INSTALLED_APPS列表中。在REST_FRAMEWORK设置中还包括新的DEFAULT_SCHEMA_CLASS:

# project/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "rest_framework_swagger", # 新添加
    "corsheaders",
    "notes",
    "jwtauth",
]

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
    "DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser",],
    "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", # 新添加
}

也将其包含在你的project/urls.py文件中。最后我们还要做一些重构。现在刷新和令牌是单独的两个端点。因为他们都是与用户验证有关的,最好让他们都在jwtauth应用中。让我们将他们移到一起,同时也向文档添加一个路由。

# project/urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_swagger.views import get_swagger_view
schema_view = get_swagger_view(title="Notes API")
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('notes.urls')),
    path('auth/', include('rest_framework.urls')),
    path('api/jwtauth/', include('jwtauth.urls'), name='jwtauth'),
    path('api/docs/', schema_view),
]

然后将刷新和令牌放到jwtauth/urls.py中:

# jwtauth/urls.py

from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import registration

urlpatterns = [
    path("register/", registration, name="register"),
    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

http://localhost:8000/api/docs/ 看到完整的API列表. 你也应该能看到令牌端点现在属于/jwtauth组中。

就是这样。你现在拥有一个功能齐全的Notes API了,客户端可以从任何一个地方使用它。

发表评论

0/200
0 点赞
0 评论
收藏
为你推荐 换一批