如果你已经构建了一个Django API,但不知道如何添加认证机制,以确保每个用户只能访问自己的数据,那么你来对地方了。
大多数Django教程都会介绍基于会话的认证方式。当前端和后端运行在同一个服务器上时,这种认证方式确实很有效。但一旦将它们分开——比如让一个运行在Netlify上的React应用与运行在PythonAnywhere上的Django API进行交互——就会遇到问题,因为会话机制在这种情况下就无法正常工作了。
Cookie在不同域名之间无法顺利传输,因此登录系统也会突然失效。
这时,JSON Web Tokens(JWT)就派上用场了。JWT提供了一种无需使用Cookie的状态无关型认证方式,它可以在不同的域名、设备和平台上无缝运行。服务器根本不需要记住任何用户信息,只需验证token的签名即可确定请求者是谁。
不过,认证只是解决问题的一半。一旦确定了用户的身份,接下来还需要控制他们能够查看哪些数据。这时,权限控制就显得非常重要了。
权限控制的目的是确保每个用户只能访问自己的数据。例如,用户A绝对不能读取、编辑或删除用户B的数据(在我们的例子中就是笔记),即使他们猜对了用户的ID也是如此。
在本教程中,你将构建一个个人笔记记录API,让用户能够注册账号、使用JWT令牌登录,并存储只有自己才能访问的笔记。
在整个开发过程中,你还将实现一个自定义的用户模型,配置SimpleJWT以实现基于token的认证机制,同时编写一些权限控制相关的代码,确保每个用户的数据只能通过自己的凭证来访问。
我们将涵盖的内容:
本教程涵盖以下内容:
-
如何设置自定义用户模型(以及为何应始终这样做)
-
如何配置SimpleJWT以实现访问令牌和刷新令牌的身份验证
-
如何编写能够保护敏感字段的序列化器
-
如何限定API接口的范围,使用户只能看到自己的数据
-
如何使用Postman测试整个流程
让我们开始吧
先决条件
在开始之前,请确保您已经掌握了以下内容:
-
Django基础:您应该了解Django项目及应用程序的运作原理,包括模型、视图、URL以及数据迁移。
-
Django REST Framework基础:您需要熟悉序列化器、视图集或API接口,以及DRF如何处理请求和响应。
-
基本的命令行使用方法:在本教程中,您需要在终端中执行各种命令。
您需要安装以下工具:
-
Python 3.8或更高版本
-
pip(Python的包管理器)
-
像Visual Studio Code这样的代码编辑器
-
Postman(或任何API测试工具),用于测试您的接口端点。您将使用它向API发送请求。
什么是JWT?为什么要在会话认证之外使用它?
在编写任何代码之前,了解JWT能解决什么问题,以及为什么Django内置的会话认证机制并不总是足够用的,这一点非常重要。
会话认证的工作原理
Django提供了基于会话的身份验证系统。其工作原理大致如下:
-
用户将用户名和密码发送给服务器。
-
服务器验证这些凭证,并创建一个会话记录,该记录存储在数据库中,用于表明“该用户已登录”。
-
服务器会将会话ID以cookie的形式返回给浏览器。浏览器会自动保存这个cookie。
-
在后续的所有请求中,浏览器都会将这个cookie发送回服务器。服务器会在数据库中查找该会话ID,然后确认“这是用户A,允许其访问资源”。

当前端和后端位于同一域名下时,这种认证机制可以完美地发挥作用。浏览器会自动处理cookie,而Django也会在后台悄无声息地管理会话信息。
然而,这种方法也存在一些局限性。
-
跨域问题: 如果你的 React 前端程序位于 app.example.com,而 Django API 位于 api.example.com,那么 cookie 的使用就会变得复杂。浏览器对于哪些域名可以发送和接收 cookie 有着严格的规定。
你可以通过设置 CORS(跨源资源共享)头部信息以及特殊的 cookie 配置来解决这个问题,但这样做会增加系统的复杂性,而且这些解决方案也可能存在漏洞。
-
可扩展性问题: 每个处于活动状态的会话都会被存储在服务器的数据库中。如果同时有 10,000 名用户登录系统,那么服务器就需要在每次请求时查询这 10,000 条会话记录。随着应用程序规模的扩大,这种查询操作就会成为性能瓶颈。
-
移动设备问题: 移动应用的处理 cookie 的方式与浏览器不同。如果你开发的 API 需要同时为网页应用和移动应用提供服务,那么会话 cookie 就会带来额外的麻烦。
JWT认证的工作原理
JWT采用了一种完全不同的方式。它没有将会话数据存储在服务器上,而是直接将认证信息嵌入到令牌本身中。
其工作流程如下:
-
用户向服务器提交他们的用户名和密码。
-
服务器验证这些凭证,并生成一个JWT——这是一个包含用户ID以及令牌有效期等信息的加密字符串。
-
服务器将这个令牌发送回客户端。客户端会将其存储起来(通常是在内存或本地存储中)。
-
在后续的每次请求中,客户端都会在请求头中包含这个令牌。服务器会读取该令牌,验证其签名,然后确认“这是用户A,允许其通过”。
需要注意一个关键点:服务器从不存储任何会话信息。
它不会在数据库中查找会话记录,而是直接读取令牌,检查其加密签名以确保没有人篡改过它,然后提取用户信息。正因为如此,JWT被称为无状态认证机制——服务器不会保存关于哪些用户已登录的状态信息。
这种方式解决了跨域问题,因为令牌是通过请求头发送的,而不是以cookie的形式传输。无论请求来自哪个域名,请求头的处理方式都是一样的。
这也解决了可扩展性问题,由于服务器不需要存储会话信息,验证令牌只需要进行快速的加密检查,而无需查询数据库。
对于移动设备来说,这也是一个很好的解决方案,因为任何能够发送HTTP请求头的客户端都可以使用JWT。移动应用、桌面应用,甚至是其他服务器,它们都可以采用同样的认证方式。

步骤1:如何设置项目并安装依赖项
1.1 如何创建项目
打开终端,导航到你想要存放项目的目录,然后运行以下命令:
mkdir notes-project
cd notes-project

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 list
你应该能看到这三个包以及它们的依赖项都被列了出来。

1.3 如何创建项目及应用
运行以下命令来创建Django项目:
django-admin startproject notes_core .
命令末尾的点非常重要,它告诉Django在当前目录中创建项目文件,而不是创建一个额外的嵌套文件夹。
现在让我们输入以下命令来创建应用:
python manage.py startapp notes

1.4 如何注册应用程序及Django Rest Framework
打开notes_core/settings.py文件,在INSTALLED_APPS列表中添加rest_framework和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
输入这条命令时,请确保虚拟环境已经激活:

之后,在shell中导入CustomUser模型:
from notes.models import CustomUser
接着输入以下代码:
[fields.name for field in CustomUser._meta.get_fields()]
上述代码会列出CustomUser类中包含的所有字段。
2.2 如何让Django使用你自定义的用户模型
现在来到关键部分。打开notes_core/settings.py文件,添加这一行代码:
AUTH_USER_MODEL = 'notes.CustomUser'
这个设置告诉Django在处理所有与认证相关的功能时,使用你自定义的CustomUser模型,而不是内置的模型。
关于应该将这段代码添加到文件中的哪个位置,并没有严格的规定,但最佳实践是将其放在文件的末尾附近。

你可以通过调用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)
你应该会看到所使用的模型名称:

如果你没有在settings.py文件中添加AUTH_USER_MODEL,那么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表示迁移编号。
执行这条命令后,你应该会得到如下输出结果:

我们还需要创建一个超级用户,这样以后你就可以登录到管理员面板进行调试了:
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} 创建)"

让我们逐一了解这些字段的含义:
-
owner = models.ForeignKey(settings.auth_user_MODEL, ...):这种设置建立了每条笔记与用户之间的关系。ForeignKey字段告诉Django,每条笔记只能属于一个用户,但一个用户可以拥有多条笔记。请注意,我们使用了
settings.auth_userMODEL,而不是直接导入CustomUser。这种做法是推荐的做法,因为它能使代码更具灵活性。如果将来您修改了设置中指定的用户模型,这个外键会自动进行相应的调整。on_delete=models.CASCADE表示:如果某个用户被删除,那么他所有的笔记也会被同时删除。related_name='notes'使得可以通过user.notes.all()来获取某用户的所有笔记。 -
title = models.CharField(max_length=200):这个字段用于存储任务标题,长度最多为200个字符。 -
body = models.TextField():这个字段用于保存笔记的实际内容。TextField没有字符长度限制,因此用户可以随意输入内容。 -
created_at = models.DateTimeField(auto_now_add=True):这个字段会自动记录任务创建的时间和日期。您完全不需要手动设置这一值。__str__()方法用于为每条笔记生成一个便于阅读的表示形式。在管理员界面或调试过程中,看到的不会是“Note object (1)”这样的信息,而是像“会议笔记(由Solina创建)”这样的文字。
3.2 如何申请进行数据迁移
运行以下命令来创建`Notes`表:
python manage.py makemigrations
python manage.py migrate

与之前一样,我们可以看到Django用于创建`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)

在开发过程中,这种方式非常有用,因为它能让你快速检查数据是否被正确保存。
步骤4:如何创建序列化器
在DRF中,序列化器就像是连接数据库与前端应用程序的桥梁。
Django模型会将数据存储为Python对象。但当你需要将这些数据发送到前端应用(比如React或移动应用)时,直接发送Python对象是不行的。你需要使用一种所有人都能理解的格式进行传输,而这种格式通常就是JSON。
序列化器主要承担三项任务:
-
序列化: 将复杂的Python对象转换为Python字典(这种字典可以很容易地被转换成JSON格式)。
-
反序列化: 将从用户端传来的JSON数据重新转换回复杂的Python对象。
-
验证: 在将数据保存到数据库之前,检查这些数据是否合法有效。

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
让我们来详细分析这个序列化器。
-
UserSerializer用于处理用户注册功能。 -
User = get_user_model()用于获取你正在使用的用户模型,并将其存储在变量User中。在我们的例子中,我们使用的是CustomUser模型。 -
class UserSerializer(serializers.ModelSerializer)::这里创建了UserSerializer类,它继承自ModelSerializer。ModelSerializer是一种简化方式,它会根据模型类中的字段自动生成相应的序列化器类。当我们使用
ModelSerializer时,DRF会自动完成以下操作:1. 根据模型类生成所需的字段;这样你就无需手动编写这些代码了。
2. 自动添加模型中定义的字段验证规则。
3> 实现create()和update()方法。ModelSerializer会知道应该使用哪个模型,以及如何更新或创建该模型。如果你需要自定义这些行为,也可以重写create()和update()方法。在上面的代码中,你确实重写了create()方法。 -
password = serializers.CharField(write_only=True):这一行代码非常重要。write_only=True表示在注册过程中可以输入密码,但该密码永远不会出现在任何API响应中。如果没有这个设置,每次返回用户信息时,API都会把密码原封不动地返回出来。因此,用户虽然可以创建账户,但他们的密码永远不会被公开显示。
-
class Meta:在Meta类中,你可以指定序列化器应该使用哪个模型。在这个例子中,使用的模型是User,同时也会指定需要处理的字段。 -
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']
现在我们来详细解释这段代码:
-
owner = serializersReadOnlyField(source='owner.username'):这是代码中最重要的一行。这一行使得owner字段变为只读字段。这意味着,API会显示笔记的所有者(即他们的用户名),但没有人能够通过 API 来更改笔记的所有者。如果没有这种保护机制,恶意用户就可以发送一个包含
"owner": 5的 POST 请求,从而将某人的笔记分配给另一个用户的账户;更糟糕的是,他们还可以通过修改所有权来篡改他人的笔记。source='owner.username'这一行告诉 DRF 应该显示所有者的用户名而不是他们的数字 ID,这样 API 返回的数据就会更加易于阅读。 -
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']

步骤 5:如何配置 SimpleJWT
现在我们来设置认证系统。在这个步骤中,你需要告诉 DRF 使用 JWT 进行认证,而不是使用会话机制。这个步骤非常重要,因为如果不进行这样的配置,DRF 将默认使用基于会话的认证方式。
SimpleJWT为DRF提供了完整的JWT实现方案,因此您无需从头开始开发令牌生成、签名或验证的相关功能。
访问令牌是客户端在每次发送API请求时都会附带的信息。这种令牌被设计成具有较短的有效期。可以把它想象成办公楼里的访客通行证:它能让您进入大楼,但会在一天结束时就失效。如果有人窃取了这种令牌,造成的危害也会有限,因为该令牌很快就会失去效力。
刷新令牌的有效期较长,它的唯一用途就是在当前访问令牌过期时获取新的访问令牌。客户端会安全地存储刷新令牌,并且只会在向特定的服务器端点发送请求时使用它。可以把刷新令牌想象成员工的身份证:您每天早上都会用它来换取新的访客通行证,但并不会在每一道门前都出示它。
这种分离机制是出于安全考虑。如果有效期较短的访问令牌被窃取(由于它会在每次请求时都被发送出去,因此这种情况更有可能发生),攻击者在令牌失效之前只有很短的时间来利用它。而刷新令牌由于发送频率较低,被截获的风险也就更低。
让我们来看看访问令牌和刷新令牌是如何协同工作的。
-
用户登录后,服务器会同时提供访问令牌和刷新令牌。
-
用户使用访问令牌来发送请求。
-
访问令牌失效。
-
应用程序向服务器发送刷新令牌。
-
服务器验证该刷新令牌后,会重新颁发一个新的访问令牌。
-
用户无需再次登录,就可以继续使用服务。

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),
}

让我们来详细解释每一部分的作用。
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参数。这段代码就是用来将当前登录的用户设置为笔记的所有者的。
还记得你在序列化器中是如何将“所有者”字段设为只读的吗?这就是这项安全措施的另一部分内容。

用户无法通过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-list和note-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下载它。

或者,你也可以使用命令行中的curl工具,或者其他任何你熟悉的API测试工具。
在继续下一步操作之前,请确保你的开发服务器已经启动运行。

9.1 如何注册用户
打开Postman:

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

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


9.2 如何获取访问令牌和刷新令牌
现在请登录以获取您的 JWT 令牌:
| 方法 | POST |
|---|---|
| URL | http://127.0.0.1:8000/api/token/ |
| 请求体内容 | {“username” : “priya”, “password” : “securepassword123”} |
系统会返回包含访问令牌和刷新令牌的响应。
请复制访问令牌。后续的所有请求都需要使用这个令牌。同时也要保存刷新令牌,因为后面还会用到它。

JWT 仅经过编码处理,并未被加密。这种编码方式只是为了将数据转换成一种安全、标准化的字符串格式,以便通过互联网进行传输。
任何人都可以通过解码来查看其中的数据。这种编码使用的是 base64url 编码格式。
我们可以使用 Python 的 pyjwt 库来解码 JWT 令牌,或者使用其他在线工具来进行解码。需要注意的是,在使用在线工具时必须谨慎操作,因为 JWT 令牌中可能包含敏感信息。
在这个演示中,我们将使用 jwt.io 这个网站来解码 JWT 令牌。
打开该网站,然后粘贴您刚刚生成的访问令牌:

JWT 令牌由三部分组成:头部、有效载荷和签名。
头部信息说明了该 JWT 是如何被签名的。在这个例子中,它是使用 HS256 算法进行签名的。
有效载荷部分包含了实际的数据或声明信息。其中包含一些标准字段,如令牌类型、过期时间(exp)、发行时间(iat),以及自定义字段。
签名部分用于验证 JWT 令牌的完整性。您无法将签名部分解码成有意义的数据。这一机制确保了令牌没有被篡改。
9.3 如何创建便条
现在请使用访问令牌来创建一条便条:
| 方法 | POST |
|---|---|
| URL | http://127.0.0.1:8000/api/notes/ |
| 头部设置: | 添加一个新的头部字段: |
| 键:Authorization,值:Bearer | |
| 请求体内容 | {‘title’: ‘我的便条’, ‘body’: ‘这里包含机密信息’} |

请注意,您不需要手动添加“所有者”字段——这一操作会由`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的访问令牌。随后,请复制该访问令牌。

现在,使用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则就像一堵空墙,你甚至都不会知道那里有房间存在。

现在你们已经明白了为什么我们使用404响应码而不是403响应码,接下来我们就来实际演示一下这一点。
首先,我会使用Priya的账号凭证和她的访问令牌来访问她的个人笔记:

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

可以看到,使用新用户的访问令牌去尝试访问另一个用户的笔记时,确实会得到404 Not Found的响应。
步骤10:如何利用刷新令牌来处理访问令牌过期的问题
访问令牌被设置为有效期较短(在您的配置中为30分钟),这样一旦令牌被盗,造成的损害范围也会受到限制。

但是,我们并不希望用户每隔30分钟就重新输入一次账号凭证。这就是刷新令牌存在的意义所在。
当Priya的访问令牌过期后,她发起的API请求将会返回401 Unauthorized错误响应。此时,客户端无需重新登录,而是需要使用刷新令牌来获取新的访问令牌。
| 方法 | POST |
|---|---|
| URL | http://127.0.0.1:8000/api/token/refresh/ |
| 请求体选项 | 从下拉列表中选择“raw”并挑选“JSON”格式。 |
| 请求体内容 | { refresh: |

用这个新的访问令牌替换你之前的旧令牌,这样你就可以再使用30分钟了。刷新令牌本身的有效期为一天,因此用户只需每24小时重新登录一次即可。
在实际应用中,前端客户端会自动处理这类操作。当API请求返回401错误码时,客户端会捕获这个错误,然后使用刷新令牌去获取新的访问令牌,并重新尝试原来的请求——整个过程用户都不会察觉到。
以下是用伪代码表示的这一流程:
-
客户端使用访问令牌发送请求
-
服务器返回401错误码(表示令牌已过期)
-
客户端将刷新令牌发送到/api/token/refresh/
-
服务器返回新的访问令牌
-
客户端使用新令牌重新尝试原来的请求
-
服务器最终返回数据
如果刷新令牌本身已经过期(在你的配置中,其有效期为24小时),那么第4步也会返回401错误码。在这种情况下,用户确实需要使用用户名和密码重新登录。这种设计正是有意为之的:这样即使刷新令牌被窃取,它的有效期限也是有限的。
如何改进这个项目
当前的API功能完备且安全性较高,但仍有很大的改进空间。以下是一些建议方向:
-
添加搜索和过滤功能。允许用户通过标题或内容文本来搜索笔记。你可以利用DRF的SearchFilter和django-filter为笔记列表接口添加查询参数,比如
?search=meeting。 -
添加分类或标签功能。创建一个
Category模型,并在Note模型中添加外键来关联分类;对于标签,也可以使用多对多关系来实现。这样用户就可以按类别整理笔记并进行筛选了。 -
添加分页功能。当用户拥有数百条笔记时,一次性返回所有笔记会导致响应速度变慢。DRF提供了内置的分页类,你可以根据需要将笔记分成10条、20条或其他数量进行显示。
-
部署到生产环境。目前这个API是在你的本地机器上运行的。你可以将其部署到PythonAnywhere、Railway或Render等平台上,这样用户就可以从任何地方访问它了。你需要配置一个生产环境的数据库(比如PostgreSQL),设置安全的SECRET_KEY,并确保应用程序通过HTTPS协议提供服务。
-
开发前端界面。可以将React、Next.js或Vue.js等前端框架与这个API连接起来。在客户端存储JWT令牌,并实现令牌刷新机制,这样用户就可以保持无缝登录状态。
-
添加令牌黑名单功能。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认证机制和权限控制策略即可。核心的工作流程是不变的,只有使用的模型和业务逻辑会发生变化。




