如果你已经构建了一个Django API,但不知道如何添加认证机制,以确保每个用户只能访问自己的数据,那么你来对地方了。

大多数Django教程都会介绍基于会话的认证方式。当前端和后端运行在同一个服务器上时,这种认证方式确实很有效。但一旦将它们分开——比如让一个运行在Netlify上的React应用与运行在PythonAnywhere上的Django API进行交互——就会遇到问题,因为会话机制在这种情况下就无法正常工作了。

Cookie在不同域名之间无法顺利传输,因此登录系统也会突然失效。

这时,JSON Web Tokens(JWT)就派上用场了。JWT提供了一种无需使用Cookie的状态无关型认证方式,它可以在不同的域名、设备和平台上无缝运行。服务器根本不需要记住任何用户信息,只需验证token的签名即可确定请求者是谁。

不过,认证只是解决问题的一半。一旦确定了用户的身份,接下来还需要控制他们能够查看哪些数据。这时,权限控制就显得非常重要了。

权限控制的目的是确保每个用户只能访问自己的数据。例如,用户A绝对不能读取、编辑或删除用户B的数据(在我们的例子中就是笔记),即使他们猜对了用户的ID也是如此。

在本教程中,你将构建一个个人笔记记录API,让用户能够注册账号、使用JWT令牌登录,并存储只有自己才能访问的笔记。

在整个开发过程中,你还将实现一个自定义的用户模型,配置SimpleJWT以实现基于token的认证机制,同时编写一些权限控制相关的代码,确保每个用户的数据只能通过自己的凭证来访问。

我们将涵盖的内容:

本教程涵盖以下内容:

  1. 如何设置自定义用户模型(以及为何应始终这样做)

  2. 如何配置SimpleJWT以实现访问令牌和刷新令牌的身份验证

  3. 如何编写能够保护敏感字段的序列化器

  4. 如何限定API接口的范围,使用户只能看到自己的数据

  5. 如何使用Postman测试整个流程

让我们开始吧

先决条件

在开始之前,请确保您已经掌握了以下内容:

  1. Django基础:您应该了解Django项目及应用程序的运作原理,包括模型、视图、URL以及数据迁移。

  2. Django REST Framework基础:您需要熟悉序列化器、视图集或API接口,以及DRF如何处理请求和响应。

  3. 基本的命令行使用方法:在本教程中,您需要在终端中执行各种命令。

您需要安装以下工具:

  • Python 3.8或更高版本

  • pip(Python的包管理器)

  • 像Visual Studio Code这样的代码编辑器

  • Postman(或任何API测试工具),用于测试您的接口端点。您将使用它向API发送请求。

什么是JWT?为什么要在会话认证之外使用它?

在编写任何代码之前,了解JWT能解决什么问题,以及为什么Django内置的会话认证机制并不总是足够用的,这一点非常重要。

会话认证的工作原理

Django提供了基于会话的身份验证系统。其工作原理大致如下:

  1. 用户将用户名和密码发送给服务器。

  2. 服务器验证这些凭证,并创建一个会话记录,该记录存储在数据库中,用于表明“该用户已登录”。

  3. 服务器会将会话ID以cookie的形式返回给浏览器。浏览器会自动保存这个cookie。

  4. 在后续的所有请求中,浏览器都会将这个cookie发送回服务器。服务器会在数据库中查找该会话ID,然后确认“这是用户A,允许其访问资源”。

该信息图展示了Django会话认证的具体流程

当前端和后端位于同一域名下时,这种认证机制可以完美地发挥作用。浏览器会自动处理cookie,而Django也会在后台悄无声息地管理会话信息。

然而,这种方法也存在一些局限性。

  1. 跨域问题: 如果你的 React 前端程序位于 app.example.com,而 Django API 位于 api.example.com,那么 cookie 的使用就会变得复杂。浏览器对于哪些域名可以发送和接收 cookie 有着严格的规定。

    你可以通过设置 CORS(跨源资源共享)头部信息以及特殊的 cookie 配置来解决这个问题,但这样做会增加系统的复杂性,而且这些解决方案也可能存在漏洞。

  2. 可扩展性问题: 每个处于活动状态的会话都会被存储在服务器的数据库中。如果同时有 10,000 名用户登录系统,那么服务器就需要在每次请求时查询这 10,000 条会话记录。随着应用程序规模的扩大,这种查询操作就会成为性能瓶颈。

  3. 移动设备问题: 移动应用的处理 cookie 的方式与浏览器不同。如果你开发的 API 需要同时为网页应用和移动应用提供服务,那么会话 cookie 就会带来额外的麻烦。

JWT认证的工作原理

JWT采用了一种完全不同的方式。它没有将会话数据存储在服务器上,而是直接将认证信息嵌入到令牌本身中。

其工作流程如下:

  1. 用户向服务器提交他们的用户名和密码。

  2. 服务器验证这些凭证,并生成一个JWT——这是一个包含用户ID以及令牌有效期等信息的加密字符串。

  3. 服务器将这个令牌发送回客户端。客户端会将其存储起来(通常是在内存或本地存储中)。

  4. 在后续的每次请求中,客户端都会在请求头中包含这个令牌。服务器会读取该令牌,验证其签名,然后确认“这是用户A,允许其通过”。

需要注意一个关键点:服务器从不存储任何会话信息

它不会在数据库中查找会话记录,而是直接读取令牌,检查其加密签名以确保没有人篡改过它,然后提取用户信息。正因为如此,JWT被称为无状态认证机制——服务器不会保存关于哪些用户已登录的状态信息。

这种方式解决了跨域问题,因为令牌是通过请求头发送的,而不是以cookie的形式传输。无论请求来自哪个域名,请求头的处理方式都是一样的。

这也解决了可扩展性问题,由于服务器不需要存储会话信息,验证令牌只需要进行快速的加密检查,而无需查询数据库。

对于移动设备来说,这也是一个很好的解决方案,因为任何能够发送HTTP请求头的客户端都可以使用JWT。移动应用、桌面应用,甚至是其他服务器,它们都可以采用同样的认证方式。

该信息图展示了JWT认证过程中的各个步骤

步骤1:如何设置项目并安装依赖项

1.1 如何创建项目

打开终端,导航到你想要存放项目的目录,然后运行以下命令:

mkdir notes-project

cd notes-project

该图片展示了创建notes项目文件夹的过程

1.2 如何创建虚拟环境并安装所需依赖项

你需要在这里创建一个虚拟环境。请输入以下命令:

python3 -m venv venv

该图片展示了执行命令后虚拟环境文件夹的创建过程

上述命令会在名为venv的文件夹中创建一个虚拟环境。第一个venv是命令本身,而第二个venv则表示该文件夹的名称。你可以给这个文件夹起任何名字,不过通常人们更喜欢使用venv

要激活虚拟环境,我们需要使用以下命令:

在macOS/Linux系统中:

source venv/bin/activate

在Windows系统中:

venv\Scripts\activate

当你在终端提示符的前面看到(venv)时,就说明虚拟环境已经激活了。从这一刻起,你安装的任何Python包都只会存在于这个虚拟环境中

该图片展示了虚拟环境的激活过程

在虚拟环境激活之后,可以使用以下命令来安装Django、Django Rest Framework以及Simple JWT Framework:

pip install django djangorestframework djangorestframework-simplejwt 

该图片展示了执行pip命令后包的安装过程

你可以通过运行以下命令来验证所有包是否都已正确安装:

pip list

你应该能看到这三个包以及它们的依赖项都被列了出来。

该图片展示了所有依赖项的列表,包括刚刚安装的那些依赖项

1.3 如何创建项目及应用

运行以下命令来创建Django项目:

django-admin startproject notes_core .

命令末尾的点非常重要,它告诉Django在当前目录中创建项目文件,而不是创建一个额外的嵌套文件夹。

现在让我们输入以下命令来创建应用:

python manage.py startapp notes

该图片展示了Django项目及其应用的文件夹结构

1.4 如何注册应用程序及Django Rest Framework

打开notes_core/settings.py文件,在INSTALLED_APPS列表中添加rest_frameworknotes

该图片显示了DRF和notes应用程序被添加到已安装的应用程序列表中

现在,Django已经认识到了你的新应用程序以及REST框架。接下来,让我们来看看在这个项目中你需要做出的最重要的架构决策。

步骤2:如何创建自定义用户模型

如果你之前曾经开发过Django项目,那么你可能使用过Django默认的用户模型。对于快速制作原型来说,这种做法确实很合适。但是,对于任何你计划长期维护或进一步发展的项目而言,从一开始就使用自定义用户模型是一种绝对不能忽视的最佳实践。

原因如下:Django默认的User模型将username字段作为主要识别依据。如果你后来决定让用户使用电子邮件地址进行登录,或者需要添加个人资料图片字段、电话号码字段等等,那么你就会遇到麻烦。

使用自定义用户模型可以让你完全掌控自己的应用程序中“用户”这一概念的含义。你不必受限于username这个字段;对于健身应用或移动应用来说,你可以选择使用电子邮件地址或电话号码作为登录凭证。此外,你还可以直接在用户模型中添加诸如角色(如诊所系统中的医生、患者、接待员)或出生日期之类的字段,而无需另外管理个人资料信息。

这样做还能让你的项目具备更好的可扩展性。如果你一开始使用的是默认模型,后来又决定将登录方式从用户名改为电子邮件地址,或者需要添加新的字段,那么进行这样的修改将会非常困难且充满风险。而从一开始就使用自定义用户模型,就可以避免这些问题,使你的认证系统随着应用程序的发展而更加容易进行调整。

即使你创建的自定义用户模型与默认模型完全相同,从一开始就采用这种做法也能让你在未来随时方便地进行修改,而不会遇到任何麻烦。

2.1 如何定义自定义用户模型

打开notes/models/py文件,然后添加以下代码:

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

class CustomUser(AbstractUser):
    pass

该图片显示了自定义用户模型的代码

你在这里导入了Django内置的AbstractUser类。

可以将 `AbstractUser` 视为一种现成的用户模型模板。它已经包含了用户名、密码、电子邮件、名字、姓氏等字段,同时还包含了相应的认证逻辑。pass语句表示你目前还没有添加任何额外的字段。
但关键在于,这个模型是属于你的。因此,它的行为与Django默认的用户模型完全相同,不过它有一个巨大的优势:你可以随时对其进行自定义。
如果三个月后你需要添加一个phone_number字段,或者改为使用电子邮件进行登录,你只需要在这个类中添加相应的字段,然后运行迁移命令即可。

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

class CustomUser(AbstractUser):
    phone_number = models.CharField(max_length=15)

你还可以看到CustomUser类从AbstractUser类继承了哪些字段。
要查看这些信息,我们可以使用Python shell。输入以下命令:

python manage.py shell

输入这条命令时,请确保虚拟环境已经激活:
该图片展示了在虚拟环境已激活的情况下进入Python shell所需的命令
之后,在shell中导入CustomUser模型:

from notes.models import CustomUser

接着输入以下代码:

[fields.name for field in CustomUser._meta.get_fields()]

上述代码会列出CustomUser类中包含的所有字段。
该图片展示了<code>CustomUser</code>模型继承的所有字段的列表” height=”400″ loading=”lazy” src=”https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/5a1b3314-7b48-41f6-98a9-dc3133cfce4c.png” style=”display:block;margin:0 auto” width=”600″/></p>
<h3 id=2.2 如何让Django使用你自定义的用户模型

现在来到关键部分。打开notes_core/settings.py文件,添加这一行代码:

AUTH_USER_MODEL = 'notes.CustomUser'

这个设置告诉Django在处理所有与认证相关的功能时,使用你自定义的CustomUser模型,而不是内置的模型。
关于应该将这段代码添加到文件中的哪个位置,并没有严格的规定,但最佳实践是将其放在文件的末尾附近。
该图片展示了上述代码被添加到settings.py文件中的情景
你可以通过调用get_user_model()方法来查看Django当前使用的是哪个用户模型。

再次打开Python shell,然后导入get_user_model()方法:

from django.contrib.auth import get_user_model 

接着使用get_user_model()并打印输出结果:

user = get_user_model()
print(user)

你应该会看到所使用的模型名称:

895d5bcc-6880-4c4d-9007-96d44e9fa496

如果你没有在settings.py文件中添加AUTH_USER_MODEL,那么Django会使用默认的用户模型:

该图片显示了Django使用的默认用户模型

注意:在运行第一个迁移之前,你必须先完成这一步操作。如果你在没有设置AUTH_USER_MODEL的情况下就运行了migrate命令,Django会为默认的用户模型创建相应的表结构,之后再更改设置就会带来很多麻烦。

2.3 如何运行迁移

现在创建并应用初始的迁移文件:

python manage.py makemigrations
python manage.py migrate

该图片显示了执行上述命令后的输出结果

Django会为你自定义的用户模型创建必要的表结构,同时也会生成所有内置的Django表。

我们可以再次深入了解一下,看看Django具体使用了哪些SQL语句来创建这些表,尤其是CustomUser表。

输入以下命令:

python manage.py sqlmigrate notes 0001

这里notes代表应用程序的名称,0001表示迁移编号。

执行这条命令后,你应该会得到如下输出结果:

该图片显示了sqlmigrate命令执行后的输出结果

我们还需要创建一个超级用户,这样以后你就可以登录到管理员面板进行调试了:

python manage.py createsuperuser
按照提示填写用户名、电子邮件(可选)和密码。

该图片显示了超级用户的创建过程

步骤3:如何定义笔记模型

现在,让我们为应用程序的核心创建数据模型。首先,添加一个新的导入语句,以便使用settings对象。

from django.conf import settings

然后在CustomUser类下方添加以下代码:

class Notes(models.Model):
    owner = models.ForeignKey(
        settings.auth_user_MODEL,
        on_delete=models.CASCADE,
        related_name='notes'
    )
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return f"{self.title} (由 {self.owner.username} 创建)"

以下是完整的model.py代码:

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.conf import settings

class CustomUser(AbstractUser):
    pass

class Notes(models.Model):
    owner = models.ForeignKey(
        settings.auth_user_MODEL,
        on_delete=models.CASCADE,
        related_name='notes'
    )
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return f"{self.title} (由 {self.owner.username} 创建)"

该图片展示了完整的models.py文件

让我们逐一了解这些字段的含义:

  1. owner = models.ForeignKey(settings.auth_user_MODEL, ...):这种设置建立了每条笔记与用户之间的关系。ForeignKey字段告诉Django,每条笔记只能属于一个用户,但一个用户可以拥有多条笔记。

    请注意,我们使用了settings.auth_userMODEL,而不是直接导入CustomUser。这种做法是推荐的做法,因为它能使代码更具灵活性。如果将来您修改了设置中指定的用户模型,这个外键会自动进行相应的调整。

    on_delete=models.CASCADE表示:如果某个用户被删除,那么他所有的笔记也会被同时删除。

    related_name='notes'使得可以通过user.notes.all()来获取某用户的所有笔记。

  2. title = models.CharField(max_length=200):这个字段用于存储任务标题,长度最多为200个字符。

  3. body = models.TextField():这个字段用于保存笔记的实际内容。TextField没有字符长度限制,因此用户可以随意输入内容。

  4. created_at = models.DateTimeField(auto_now_add=True):这个字段会自动记录任务创建的时间和日期。您完全不需要手动设置这一值。

    __str__()方法用于为每条笔记生成一个便于阅读的表示形式。在管理员界面或调试过程中,看到的不会是“Note object (1)”这样的信息,而是像“会议笔记(由Solina创建)”这样的文字。

3.2 如何申请进行数据迁移

运行以下命令来创建`Notes`表:

python manage.py makemigrations
python manage.py migrate

该图片展示了迁移`notes`模型后的结果

与之前一样,我们可以看到Django用于创建`notes`表的SQL查询语句:

该图片展示了用于创建`notes`表的SQL查询语句,以及早先创建的自定义用户表的相关信息

3.3 如何在管理员界面中注册模型

打开`notes/admin.py`文件,将这两个模型注册到管理员系统中,这样就可以通过管理员面板查看数据了:

from django.contrib import admin
from .models import CustomUser, Notes

admin.site.register(CustomUser)
admin.site.register(Notes)

该图片展示了`admin.py`文件中的代码内容

在开发过程中,这种方式非常有用,因为它能让你快速检查数据是否被正确保存。

步骤4:如何创建序列化器

在DRF中,序列化器就像是连接数据库与前端应用程序的桥梁。

Django模型会将数据存储为Python对象。但当你需要将这些数据发送到前端应用(比如React或移动应用)时,直接发送Python对象是不行的。你需要使用一种所有人都能理解的格式进行传输,而这种格式通常就是JSON。

序列化器主要承担三项任务:

  1. 序列化: 将复杂的Python对象转换为Python字典(这种字典可以很容易地被转换成JSON格式)。

  2. 反序列化: 将从用户端传来的JSON数据重新转换回复杂的Python对象。

  3. 验证: 在将数据保存到数据库之前,检查这些数据是否合法有效。

该图片展示了序列化与反序列化的过程

4.1 如何创建`UserSerializer`

创建一个名为`notes/serializers.py`的新文件,然后添加以下代码:

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

User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data.get('email', ""),
            password=validated_data['password']
        )
        return user

让我们来详细分析这个序列化器。

  1. UserSerializer用于处理用户注册功能。

  2. User = get_user_model()用于获取你正在使用的用户模型,并将其存储在变量User中。在我们的例子中,我们使用的是CustomUser模型。

  3. class UserSerializer(serializers.ModelSerializer)::这里创建了UserSerializer类,它继承自ModelSerializer

    ModelSerializer是一种简化方式,它会根据模型类中的字段自动生成相应的序列化器类。

    当我们使用ModelSerializer时,DRF会自动完成以下操作:

    1. 根据模型类生成所需的字段;这样你就无需手动编写这些代码了。
    2. 自动添加模型中定义的字段验证规则。
    3> 实现create()update()方法。ModelSerializer会知道应该使用哪个模型,以及如何更新或创建该模型。如果你需要自定义这些行为,也可以重写create()update()方法。在上面的代码中,你确实重写了create()方法。

  4. password = serializers.CharField(write_only=True):这一行代码非常重要。write_only=True表示在注册过程中可以输入密码,但该密码永远不会出现在任何API响应中。如果没有这个设置,每次返回用户信息时,API都会把密码原封不动地返回出来。

    因此,用户虽然可以创建账户,但他们的密码永远不会被公开显示。

  5. class Meta:在Meta类中,你可以指定序列化器应该使用哪个模型。在这个例子中,使用的模型是User,同时也会指定需要处理的字段。

  6. create()方法:这是最重要的部分。当我们创建新用户时,就会调用这个方法。你并没有使用默认的.create()方法,而是对其进行了重写。

    理解为什么我们要重写这个方法非常重要。默认的create()方法并不适合用于安全地创建用户账户。

    默认情况下,这个方法会以明文形式存储密码。这是一个严重的问题,因为密码绝对不能以原始格式保存。必须对密码进行加密处理,这样即使数据库被入侵,密码也不会泄露。

    Django提供了一个名为create_user()的特殊方法,它可以自动完成密码的加密操作,并正确设置用户信息以便后续的身份验证。

该图片展示了对上述代码的详细解释

4.2 如何创建 NoteSerializer

在创建了 UserSerializer 类之后,接下来我们再创建 NoteSerializer 类。这个类用于处理笔记数据。

首先,你需要在代码中添加对 Notes 类的导入语句。在最后的导入语句后面添加这一行:from .models import Notes

将这段代码放在 UserSerializer 类之后:

class NoteSerializer(serializers.ModelSerializer):
    owner = serializersReadOnlyField(source='owner.username')
    class Meta:
        model = Notes
        fields = ['id', 'owner', 'title', 'body', 'created_at']

现在我们来详细解释这段代码:

  1. owner = serializersReadOnlyField(source='owner.username'):这是代码中最重要的一行。这一行使得 owner 字段变为只读字段。这意味着,API会显示笔记的所有者(即他们的用户名),但没有人能够通过 API 来更改笔记的所有者。

    如果没有这种保护机制,恶意用户就可以发送一个包含 "owner": 5 的 POST 请求,从而将某人的笔记分配给另一个用户的账户;更糟糕的是,他们还可以通过修改所有权来篡改他人的笔记。

    source='owner.username' 这一行告诉 DRF 应该显示所有者的用户名而不是他们的数字 ID,这样 API 返回的数据就会更加易于阅读。

  2. class Meta: …… 和之前一样,Meta 类用于指定序列化器所使用的模型,以及 API 将暴露哪些字段。

    以下是完整的代码,位于 serializers.py 文件中。

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

User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data.get('email', ""),
            password=validated_data['password']
        )
        return user

class NoteSerializer(serializers.ModelSerializer):
    owner = serializersReadOnlyField(source='owner.username')
    class Meta:
        model = Notes
        fields = ['id', 'owner', 'title', 'body', 'created_at']

该图片展示了 serializers.py 文件的完整代码

步骤 5:如何配置 SimpleJWT

现在我们来设置认证系统。在这个步骤中,你需要告诉 DRF 使用 JWT 进行认证,而不是使用会话机制。这个步骤非常重要,因为如果不进行这样的配置,DRF 将默认使用基于会话的认证方式。

SimpleJWT为DRF提供了完整的JWT实现方案,因此您无需从头开始开发令牌生成、签名或验证的相关功能。

访问令牌是客户端在每次发送API请求时都会附带的信息。这种令牌被设计成具有较短的有效期。可以把它想象成办公楼里的访客通行证:它能让您进入大楼,但会在一天结束时就失效。如果有人窃取了这种令牌,造成的危害也会有限,因为该令牌很快就会失去效力。

刷新令牌的有效期较长,它的唯一用途就是在当前访问令牌过期时获取新的访问令牌。客户端会安全地存储刷新令牌,并且只会在向特定的服务器端点发送请求时使用它。可以把刷新令牌想象成员工的身份证:您每天早上都会用它来换取新的访客通行证,但并不会在每一道门前都出示它。

这种分离机制是出于安全考虑。如果有效期较短的访问令牌被窃取(由于它会在每次请求时都被发送出去,因此这种情况更有可能发生),攻击者在令牌失效之前只有很短的时间来利用它。而刷新令牌由于发送频率较低,被截获的风险也就更低。

让我们来看看访问令牌和刷新令牌是如何协同工作的。

  1. 用户登录后,服务器会同时提供访问令牌和刷新令牌。

  2. 用户使用访问令牌来发送请求。

  3. 访问令牌失效。

  4. 应用程序向服务器发送刷新令牌。

  5. 服务器验证该刷新令牌后,会重新颁发一个新的访问令牌。

  6. 用户无需再次登录,就可以继续使用服务。

该图片展示了访问令牌和刷新令牌的使用过程

5.1 如何更新REST框架设置

打开文件notes_core/settings.py,然后添加以下代码:

from datetime import timedelta
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.simplejwt.authenticationJWTAuthentication',
    ),

    'DEFAULT_PERMISSION_classes': (
        'restframework.permissions.IsAuthenticated',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}

该图片展示了正在被添加到settings.py文件中的代码

让我们来详细解释每一部分的作用。

DEFAULT_AUTHENTICATION_CLASSES这个设置告诉DRF,对于所有的API端点,都应该使用JWT作为认证方式。因此,每一个传入的请求都会在Authorization头部被检查是否包含有效的JWT令牌。

DEFAULT_PERMISSION_classes这个设置将IsAuthenticated设置为全局权限控制机制。这意味着,您的API中的所有端点默认都是受保护的,只有拥有有效令牌的用户才能访问这些端点。

这是一种“默认即安全”的解决方案:无需单独为每个接口设置保护措施,所有内容都会被自动保护起来,而只有那些需要公开访问的接口才需要你明确地开放它们(比如注册接口,在下一步中你会处理这个接口)。

SIMPLE_JWT字典用于控制令牌的行为。访问令牌的有效期为30分钟,客户端会在每个请求中包含这个令牌;如果有人截获了该令牌,所造成的影响也仅限于这30分钟内。而刷新令牌的有效期则为一天。

当访问令牌过期时,客户端可以使用刷新令牌来获取新的访问令牌,而无需用户再次登录。这意味着一旦刷新令牌失效,用户就必须使用自己的用户名和密码重新登录。在后续使用Postman进行测试时,你会清楚地看到这一机制的具体运作方式。

5.2 如何添加令牌相关的URL接口

SimpleJWT提供了用于获取和刷新令牌的现成接口,你只需要将这些接口与相应的URL关联起来即可。

打开notes_core/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('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

token/接口接受用户名和密码作为输入,然后返回访问令牌和刷新令牌。

token/refresh/接口接收刷新令牌作为输入,并返回新的访问令牌。在测试过程中你会看到这些接口的实际作用。

步骤6:如何构建认证逻辑

打开notes/views.py文件,然后添加以下代码:

from rest_framework import generics, permissions
from django.contrib.auth import get_user_model
from .serializers import UserSerializer

User = get_user_model()

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]

现在让我们来详细分析这段代码。

首先是一些导入语句,随后我们使用了get_user_model()方法来获取CustomUser模型。

接下来是RegisterView类。这个类继承自generics.CreateAPIView,后者是DRF提供的一个内置视图,专门用于处理创建新对象的POST请求。

正因为如此,你无需手动编写处理POST请求、验证数据或将数据保存到数据库的逻辑代码——DRF会在后台自动完成这些工作。

在这段代码中,queryset = Users.objects.all()定义了这个视图可以操作的用户对象集合。

serializer_class = UserSerializer指定了用于验证传入数据并创建用户对象的序列化器。

最后,permission_classes = [permissions.AllowAny]覆盖了你在之前通过DEFAULT_PERMISSION_CLASSES设置的全局权限IsAuthenticated。这意味着即使没有登录,任何人也可以访问注册端点——对于注册端点来说,这种设计是合理的,因为新用户此时还没有账户。

你的API中的其他所有视图都会继承全局的IsAuthenticated权限,因此只有这个注册端点是开放给所有用户的。

步骤7:如何实现受限访问功能

这是本教程的核心内容。你已经配置了认证机制,让API能够识别发出请求的用户身份。现在你需要确保每个用户只能操作自己的笔记。

可以这样理解:认证就像公寓大楼前门的锁,它能阻止陌生人进入;而受限访问功能则相当于每套公寓内部的门锁——即使你住在同一栋楼里,也不能随意进入邻居的公寓。

如果没有这种限制机制,已认证的用户可能会看到数据库中所有的笔记,甚至更糟的是,修改属于其他用户的笔记。但在你的视图集中通过两种方法进行了相应的限制,从而完全避免了这种情况的发生。

该图片展示了有无受限访问功能时对资源访问权限的区别

7.1 如何创建NoteViewSet

现在我们来创建NoteViewSet。首先,在文件的开头添加以下导入语句,这些代码用于导入视图集、序列化器以及模型。

from .models import Note
from .serializers import UserSerializer, NoteSerializer
from rest_framework import generics, viewsets, permissions

notes/views.py文件中,在之后添加以下代码:

class NoteViewSet(viewsets.ModelViewSet):
    serializer_class = NoteSerializer

    def get_queryset(self):
        return Notes.objects.filter(owner=self.request.user).order_by('-created_at')

    def perform_create(self, serializer):
        serializer.save.owner=self.request.user)

现在我们来详细分析这段代码。

你创建了一个名为NoteViewSet的新类,它继承自DRF的ModelViewSet类。这样你就可以执行完整的CRUD操作了——既可以列出所有的笔记,也可以检索某一条具体的笔记,同时还可以创建、更新或删除笔记。

该图片显示了模型视图集被导入的过程

接下来的代码serializer_class = NoteSerializer告诉Django使用NoteSerializer类来在Python对象和JSON数据之间进行转换。

但真正关键的是你重写的这两个方法:get_queryset()perform_create()

get queryset()方法决定了API会返回哪些笔记。如果你没有重写这个方法,它就会返回Note.objects.all()(这样所有用户都能访问数据库中的所有笔记)。

但在这里,你重写了这个方法,使其能够根据当前登录的用户来筛选笔记。

接下来是perform_create()方法,当笔记被保存时就会调用这个方法。你重写了这个方法,使得它只会保存当前登录用户的笔记。如果你没有重写这个方法,那么无论哪个用户登录,它都会保存所有笔记。

注意,在filter()函数中你传递了self.request.user参数。这段代码就是用来将当前登录的用户设置为笔记的所有者的。

还记得你在序列化器中是如何将“所有者”字段设为只读的吗?这就是这项安全措施的另一部分内容。

6db94c2f-673f-480a-bf20-730ed4af4bdb

用户无法通过API请求来设置笔记的所有者,服务器会自动将记录在案的用户的身份设置为笔记的所有者。这两项措施共同作用,使得所有权的设定变得不可被篡改。

7.2 为什么这很重要:防止ID枚举攻击

如果没有get_queryset的过滤功能,你的API可能会允许这样的情况发生:某个用户发送一个GET请求到/api/notes/42/,然后看到了一条属于其他用户的笔记,仅仅是因为他们猜对了这条笔记的ID。

这种攻击被称为ID枚举攻击——攻击者会依次尝试不同的ID(1、2、3、4……),以此来发现并访问其他用户的数据。

由于你使用了有限范围的get_queryset功能,即使用户B发送请求到/api/notes/42/,而笔记42实际上属于用户A,视图集也不会在用户B的过滤结果中找到这条笔记。DRF会返回404错误码——对用户B来说,这条笔记根本就不存在。

步骤8:如何关联URL地址

现在你需要将视图函数与相应的URL路径关联起来,这样API才能知道在每个接口点应该调用哪个视图函数。

8.1 如何创建应用级别的URL地址

创建一个名为notes/urls.py的新文件,并添加以下内容:

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RegisterView, NoteViewSet

router = DefaultRouter()
router.register(r'notes', NoteViewSet, basename='note')

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('', include(router.urls)),
]

DefaultRouter会自动为NoteViewSet生成相应的URL路径。由于你使用的是ModelViewSet,因此该路由器会创建用于列出所有笔记、创建新笔记、检索特定笔记、更新笔记以及删除笔记的接口——而这一切都是通过一次router.register调用完成的。

在这里设置basename='note'是必要的,因为你的视图集并没有在类中直接定义queryset属性(而是使用了get_queryset方法)。DRF会利用baseline参数来生成诸如note-listnote-detail这样的URL路径名称。

8.2 如何验证项目级别的URL地址

请确保你的notes_core/urls.py文件内容如下所示(你在第5步中已经设置了这些内容,但现在我们来确认一下):

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('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/, TokenRefreshView.as_view(), name='token_refresh'),
]

以下是你API的URL结构的完整列表:

接口地址 请求方法 功能描述
api/register/ POST 创建新用户账户
api/token/ POST 获取访问令牌并刷新令牌
api/token/refresh/ POST 使用刷新令牌获取新的访问令牌
api/notes/ GET 列出当前登录用户的所有笔记
api/notes/ POST 创建新笔记
api/notes// GET 检索特定笔记
api/notes// PUT/PATCH 更新特定笔记
api/notes// DELETE 删除特定笔记

启动开发服务器,确保所有功能都能正常运行而不会出现任何错误:

python manage.py runserver

如果服务器在启动过程中没有出现任何问题,那就说明你的代码编写是正确的。

步骤9:如何使用Postman测试API

构建API只是一方面,证明它的功能是否正常则是另一回事。让我们通过Postman来演示整个测试流程,从注册用户开始,一直到验证权限控制机制是否有效。

如果你之前没有使用过Postman,这个工具可以让你向API发送HTTP请求并查看响应结果。你可以从postman.com/downloads下载它。

Postman软件下载页面

或者,你也可以使用命令行中的curl工具,或者其他任何你熟悉的API测试工具。

在继续下一步操作之前,请确保你的开发服务器已经启动运行。

Python服务器正在运行中

9.1 如何注册用户

打开Postman:

打开Postman

创建一个新的请求:

方法 POST
URL http://127.0.0.1:8000/api/register/
请求体选项 选择“raw”类型,然后从下拉菜单中挑选“JSON”格式
请求体内容 { “username”: “priya”, “email”: “priya@example.com“, “password”: “securepassword123” }

Postman中用于注册新用户的界面

点击发送按钮。你应该会收到一个201 Created的响应,其中会包含用户信息(密码不会被显示出来,这要归功于你在UserSerializer类中设置的write_only=True属性)。

注册用户后的响应结果
这些代码与UserSerializer类相关

9.2 如何获取访问令牌和刷新令牌

现在请登录以获取您的 JWT 令牌:

方法 POST
URL http://127.0.0.1:8000/api/token/
请求体内容 {“username” : “priya”, “password” : “securepassword123”}

系统会返回包含访问令牌和刷新令牌的响应。

请复制访问令牌。后续的所有请求都需要使用这个令牌。同时也要保存刷新令牌,因为后面还会用到它。

该图片展示了 API 返回的访问令牌和刷新令牌

JWT 仅经过编码处理,并未被加密。这种编码方式只是为了将数据转换成一种安全、标准化的字符串格式,以便通过互联网进行传输。

任何人都可以通过解码来查看其中的数据。这种编码使用的是 base64url 编码格式。

我们可以使用 Python 的 pyjwt 库来解码 JWT 令牌,或者使用其他在线工具来进行解码。需要注意的是,在使用在线工具时必须谨慎操作,因为 JWT 令牌中可能包含敏感信息。

在这个演示中,我们将使用 jwt.io 这个网站来解码 JWT 令牌。

打开该网站,然后粘贴您刚刚生成的访问令牌:

该图片展示了解码 JWT 令牌后的界面结构

JWT 令牌由三部分组成:头部、有效载荷和签名。

头部信息说明了该 JWT 是如何被签名的。在这个例子中,它是使用 HS256 算法进行签名的。

有效载荷部分包含了实际的数据或声明信息。其中包含一些标准字段,如令牌类型、过期时间(exp)、发行时间(iat),以及自定义字段。

签名部分用于验证 JWT 令牌的完整性。您无法将签名部分解码成有意义的数据。这一机制确保了令牌没有被篡改。

9.3 如何创建便条

现在请使用访问令牌来创建一条便条:

方法 POST
URL http://127.0.0.1:8000/api/notes/
头部设置:

添加一个新的头部字段:
键:Authorization,值:Bearer
请求体内容 {‘title’: ‘我的便条’, ‘body’: ‘这里包含机密信息’}

该图片展示了如何在Postman中添加新的头部信息

请注意,您不需要手动添加“所有者”字段——这一操作会由`perform_create`函数自动完成。您应该会收到一个201 Created响应:

该图片展示了创建笔记后的输出结果

您可以再创建一些笔记,这样我们就会有可供操作的数据了。

9.4 如何列出您的笔记

现在,让我们来获取Priya所有的笔记:

方法 GET
URL http://127.0.0.1:8000/api/notes/
头部信息: 同样使用“Authorization: Bearer”头部字段

您应该会看到所有创建的笔记,这些笔记会按照创建时间顺序进行排序,最新创建的笔记会显示在最前面。

该图片展示了获取笔记列表时的响应结果

9.5 如何验证权限范围

让我们证明,第二个用户无法查看第一个用户的笔记。

首先,为第二个用户注册账户。

http://127.0.0.1:8000/api/register发送一个POST请求,并提供以下数据:

方法 POST
URL http://127.0.0.1:8000/api/register/
请求体信息: 从下拉菜单中选择“raw”类型,并选择“JSON”作为数据格式
请求体内容 { “username”: “sujan”, “email”: “sujan@example.com“, “password”: “anotherpassword123” }

该图片展示了新用户的注册过程

然后,使用Sujan的用户名和密码,向http://127.0.0.1:8000/api/token/发送一个POST请求,以获取Sujan的访问令牌。随后,请复制该访问令牌。

8fe22f1b-f36e-4b35-a478-1b48ea0218c3

现在,使用Sujan的访问令牌,在请求头中添加“Authorization”字段,然后向http://127.0.0.1:8000/api/notes/发送一个GET请求。

由于该用户尚未创建任何笔记,因此返回的结果应该是一个空列表:

该图片展示了使用获取新用户访问令牌的请求所获得的响应结果

更重要的是,Priya的笔记对他来说是完全不可见的。即使Sujan试图通过ID来访问某篇具体的笔记——比如http://127.0.0.1:8000/api/notes/1/——他也会收到404 Not Found的响应,而不会是403 Forbidden

这是有意为之。404 Not Found并不会透露该笔记确实存在,而403 Forbidden反而会向潜在攻击者证实笔记的存在。

403 Forbidden这种响应就如同一扇写着“仅限授权人员进入”的门。你会知道里面肯定有重要的东西;而404 Not Found则就像一堵空墙,你甚至都不会知道那里有房间存在。

该图片展示了403和404响应代码之间的区别

现在你们已经明白了为什么我们使用404响应码而不是403响应码,接下来我们就来实际演示一下这一点。

首先,我会使用Priya的账号凭证和她的访问令牌来访问她的个人笔记:

该图片展示了使用第一个用户(Priya)的账号凭证访问其个人笔记所获得的结果

接下来,我会更换访问令牌,使用Sujan(新用户)的访问令牌来尝试访问笔记:

该图片展示了使用新用户(Sujan)的账号凭证访问笔记所获得的响应结果

可以看到,使用新用户的访问令牌去尝试访问另一个用户的笔记时,确实会得到404 Not Found的响应。

步骤10:如何利用刷新令牌来处理访问令牌过期的问题

访问令牌被设置为有效期较短(在您的配置中为30分钟),这样一旦令牌被盗,造成的损害范围也会受到限制。

b86f4f24-b8b0-45d0-bcee-5e39e2268e21

但是,我们并不希望用户每隔30分钟就重新输入一次账号凭证。这就是刷新令牌存在的意义所在。

当Priya的访问令牌过期后,她发起的API请求将会返回401 Unauthorized错误响应。此时,客户端无需重新登录,而是需要使用刷新令牌来获取新的访问令牌。

方法 POST
URL http://127.0.0.1:8000/api/token/refresh/
请求体选项 从下拉列表中选择“raw”并挑选“JSON”格式。
请求体内容 { refresh: }

该图片展示了使用刷新令牌获取新访问令牌时的响应结果

用这个新的访问令牌替换你之前的旧令牌,这样你就可以再使用30分钟了。刷新令牌本身的有效期为一天,因此用户只需每24小时重新登录一次即可。

在实际应用中,前端客户端会自动处理这类操作。当API请求返回401错误码时,客户端会捕获这个错误,然后使用刷新令牌去获取新的访问令牌,并重新尝试原来的请求——整个过程用户都不会察觉到。

以下是用伪代码表示的这一流程:

  1. 客户端使用访问令牌发送请求

  2. 服务器返回401错误码(表示令牌已过期)

  3. 客户端将刷新令牌发送到/api/token/refresh/

  4. 服务器返回新的访问令牌

  5. 客户端使用新令牌重新尝试原来的请求

  6. 服务器最终返回数据

如果刷新令牌本身已经过期(在你的配置中,其有效期为24小时),那么第4步也会返回401错误码。在这种情况下,用户确实需要使用用户名和密码重新登录。这种设计正是有意为之的:这样即使刷新令牌被窃取,它的有效期限也是有限的。

如何改进这个项目

当前的API功能完备且安全性较高,但仍有很大的改进空间。以下是一些建议方向:

  1. 添加搜索和过滤功能。允许用户通过标题或内容文本来搜索笔记。你可以利用DRF的SearchFilter和django-filter为笔记列表接口添加查询参数,比如?search=meeting

  2. 添加分类或标签功能。创建一个Category模型,并在Note模型中添加外键来关联分类;对于标签,也可以使用多对多关系来实现。这样用户就可以按类别整理笔记并进行筛选了。

  3. 添加分页功能。当用户拥有数百条笔记时,一次性返回所有笔记会导致响应速度变慢。DRF提供了内置的分页类,你可以根据需要将笔记分成10条、20条或其他数量进行显示。

  4. 部署到生产环境。目前这个API是在你的本地机器上运行的。你可以将其部署到PythonAnywhere、Railway或Render等平台上,这样用户就可以从任何地方访问它了。你需要配置一个生产环境的数据库(比如PostgreSQL),设置安全的SECRET_KEY,并确保应用程序通过HTTPS协议提供服务。

  5. 开发前端界面。可以将React、Next.js或Vue.js等前端框架与这个API连接起来。在客户端存储JWT令牌,并实现令牌刷新机制,这样用户就可以保持无缝登录状态。

  6. 添加令牌黑名单功能。SimpleJWT支持令牌黑名单功能,这样当用户退出账户时,相关的刷新令牌就会失效。如果没有这个功能,即使用户已经“退出”账户,刷新令牌仍然会保持有效状态直到过期。

这些改进都是建立在你们已经掌握的知识基础之上的,它们会进一步加深你对Django、Django REST Framework以及API设计的理解。

结论

通过使用Django、Django REST Framework和SimpleJWT,你成功构建了一个功能完备且安全的笔记记录API。在这个过程中,你学习到了一些基本概念,这些概念适用于未来你开发的任何API。

你从自定义用户模型开始入手——这个最初看似简单的决定,实际上为你日后避免繁琐的迁移工作提供了便利。你配置了JWT认证机制,使得你的API能够为移动客户端提供服务;同时,你也实现了与会话cookie无关的前端架构。

你编写了序列化器,通过将密码字段设置为只读、将所有权信息设置为只读来保护敏感数据。最重要的是,你实现了基于用户身份的权限控制机制,确保每个用户的数据与其他用户的 数据完全隔离。

在这里你实践的各种技巧——比如重写`get_queryset`方法以根据当前用户进行数据过滤、重写`perform_create`方法自动分配数据所有权,以及使用`read-only`字段防止数据被篡改——这些都是在处理真实用户数据的生产环境API中也会用到的关键技术。

巩固所学知识的最佳方式就是继续实践。你可以尝试添加搜索和过滤功能,开发一个使用这个API的React前端应用;或者开始一个新的项目,比如任务管理工具、日记应用,甚至是书签API,只要运用相同的JWT认证机制和权限控制策略即可。核心的工作流程是不变的,只有使用的模型和业务逻辑会发生变化。

Comments are closed.