Django REST Framework 教程 4:为 API 添加认证(Authentication)与权限(Permissions)控制
【免费下载链接】django-rest-frameworkWeb APIs for Django. 🎸项目地址: https://gitcode.com/gh_mirrors/dj/django-rest-framework
本篇是 Django REST Framework 官方教程系列的第 4 部分(前序为 Tutorial 1: Serialization、Tutorial 2: Requests and Responses、Tutorial 3: Class-based Views)。在完成了基于类视图的代码片段 API 之后,本节将系统讲解如何为 Web API 引入用户体系与细粒度访问控制:让每个代码片段归属其创建者、仅允许已认证用户创建资源、仅允许资源所有者修改或删除,同时保证未认证请求仍可只读访问。学完本篇,你将掌握 DRF 的视图级权限、对象级权限、自定义权限类、Browsable API 登录集成,以及基于 HTTP Basic/Session 的程序化认证调用。
本节要解决的四个问题
当前我们的 API 对任何人都开放编辑和删除权限,这显然不够安全。教程为本节设定了明确的目标:
- 代码片段(snippet)始终与一个创建者(creator)关联;
- 只有已认证用户才能创建片段;
- 只有片段的创建者本人才能更新或删除它;
- 未认证请求拥有完整的只读访问权。
这四个目标分别对应了"模型关联、视图权限、对象权限、匿名只读"四层工作,下面逐一实现。
第一步:扩展模型,让每个片段都归属一个用户
添加 owner 与 highlighted 字段
在Snippet模型中增加两个字段:一个用于记录创建该片段的用户,另一个用于存储代码的语法高亮 HTML 表示。在snippets/models.py中添加:
owner = models.ForeignKey( "auth.User", related_name="snippets", on_delete=models.CASCADE ) highlighted = models.TextField()要点说明:
owner使用字符串引用"auth.User",可以避免在模型模块顶部导入User时产生的循环依赖问题;related_name="snippets"非常关键:它定义了从User反向查询其拥有片段集合的名字(即user.snippets),稍后编写UserSerializer时会用到;on_delete=models.CASCADE表示用户被删除时其名下片段一并删除。
用 Pygments 自动生成高亮 HTML
为了让 API 后续能直接展示带语法高亮的代码,我们需要在模型保存时用pygments代码高亮库填充highlighted字段。先补充导入:
from pygments.lexers import get_lexer_by_name from pygments.formatters.html import HtmlFormatter from pygments import highlight然后在模型类中添加.save()方法:
def save(self, *args, **kwargs): """ Use the `pygments` library to create a highlighted HTML representation of the code snippet. """ lexer = get_lexer_by_name(self.language) linenos = "table" if self.linenos else False options = {"title": self.title} if self.title else {} formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options) self.highlighted = highlight(self.code, lexer, formatter) super().save(*args, **kwargs)这段逻辑的输入全部来自Snippet模型既有的字段:language决定词法分析器(lexer),linenos决定是否输出行号("table"表示用表格形式呈现行号),title与style则传给HtmlFormatter控制标题和配色主题。
更新数据库
模型结构变了,需要同步数据库。正式项目中应当编写迁移文件,但为了教程简洁,这里直接删除数据库重新初始化:
rm -f db.sqlite3 rm -r snippets/migrations python manage.py makemigrations snippets python manage.py migrate提示:在实际项目中请勿删除迁移目录,应使用
python manage.py makemigrations snippets正常生成迁移。教程这样做只是为了快速重置演示数据。
为了方便后续测试,再创建几个不同的用户:
python manage.py createsuperuser第二步:为 User 模型添加 API 端点
编写 UserSerializer
有了用户数据,就需要把用户也暴露到 API 中。在snippets/serializers.py中添加:
from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): snippets = serializers.PrimaryKeyRelatedField( many=True, queryset=Snippet.objects.all() ) class Meta: model = User fields = ["id", "username", "snippets"]这里有一个容易踩坑的知识点:snippets是User模型上的反向关系(由Snippet.owner的related_name定义),因此ModelSerializer默认不会自动把它包含进来,必须显式声明字段。PrimaryKeyRelatedField(many=True)表示序列化时输出一组主键(即用户所拥有片段的 id 列表);其实现位于 rest_framework/relations.py,当传入的主键不存在时会抛出Invalid pk ... - object does not exist之类的校验错误。
编写只读的用户视图
在snippets/views.py中添加两个只读的泛型类视图——用户信息只需读取,不需要增删改,因此选用ListAPIView和RetrieveAPIView:
from django.contrib.auth.models import User class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer别忘了同时导入UserSerializer:
from snippets.serializers import UserSerializer这两个视图类分别对应 rest_framework/generics.py 中的ListAPIView与RetrieveAPIView:它们各自只绑定了一个 HTTP 方法(get),并分别组合了ListModelMixin与RetrieveModelMixin,因此天然就是只读的。
注册用户端点
最后在snippets/urls.py的urlpatterns中添加入口:
path("users/", views.UserList.as_view()), path("users/<int:pk>/", views.UserDetail.as_view()),第三步:通过 perform_create 把创建者与片段关联
现在的数据流存在一个缺口:客户端提交的 JSON 里不会、也不应该包含"创建者是谁"——用户身份是请求(request)的固有属性,而不是序列化数据的一部分。DRF 提供的解决方案是重写视图的.perform_create()方法,它允许我们介入实例保存过程,注入请求或 URL 中隐含的信息。
在SnippetList视图类中添加:
def perform_create(self, serializer): serializer.save(owner=self.request.user)当CreateModelMixin.create()完成数据校验后,会调用perform_create(serializer),这里把self.request.user作为额外的owner关键字参数传给serializer.save()。于是序列化器的create()方法收到的是"校验后的数据 + owner 字段",最终生成的Snippet实例就与当前登录用户正确关联。
这一模式在后续教程中还会反复出现:任何"请求隐含信息"(如当前用户、URL 参数、客户端 IP)都可以通过重写perform_create/perform_update注入。
第四步:更新序列化器,用 ReadOnlyField 暴露 owner
现在片段已经与用户关联,还需要让 API 响应体现出这一点。在SnippetSerializer中新增字段:
owner = serializers.ReadOnlyField(source="owner.username")注意:同时要在内部
Meta类的fields列表中加入'owner',。
这个字段背后有几个值得深挖的设计:
source参数决定该字段从实例的哪个属性取值,可以指向被序列化实例上的任意属性,也支持上面这种点号记法——DRF 会像 Django 模板语言那样逐级遍历属性(instance.owner.username)。- 无类型的
ReadOnlyField:与CharField、BooleanField等类型化字段不同,ReadOnlyField不关心类型,它"永远只读"——只参与序列化输出,反序列化(写入)时会被忽略,绝不会用于更新模型实例。其源码实现位于 rest_framework/fields.py:__init__中强制设置kwargs['read_only'] = True,to_representation直接原样返回取值。 - 本示例完全等价于
owner = serializers.CharField(read_only=True),但用ReadOnlyField语义更直白。
第五步:视图级权限——IsAuthenticatedOrReadOnly
片段归属用户后,就要开始收紧写权限:只有已认证用户能创建、更新、删除片段。REST framework 内置了一系列可直接使用的权限类,本节选用的是IsAuthenticatedOrReadOnly:已认证请求获得读写权限,未认证请求只能读。
先在snippets/views.py中导入:
from rest_framework import permissions然后给SnippetList和SnippetDetail两个视图类都加上:
permission_classes = [permissions.IsAuthenticatedOrReadOnly]从源码看,IsAuthenticatedOrReadOnly的实现位于 rest_framework/permissions.py:
def has_permission(self, request, view): return bool( request.method in SAFE_METHODS or request.user and request.user.is_authenticated )其中SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')定义在 rest_framework/permissions.py,即所有安全方法无条件放行,其余方法要求用户已认证。permission_classes列表由视图基类在请求进入时逐个实例化并调用其has_permission();而对象级检查(has_object_permission)则在GenericAPIView.get_object()内部通过self.check_object_permissions(self.request, obj)触发,见 rest_framework/generics.py。
顺带认识其他内置权限类
同一文件 rest_framework/permissions.py 中还提供了:
| 权限类 | 行为 |
|---|---|
AllowAny | 允许所有访问(显式声明意图,等价于空列表) |
IsAuthenticated | 仅允许已认证用户 |
IsAdminUser | 仅允许is_staff为真的管理员 |
IsAuthenticatedOrReadOnly | 已认证可读写,匿名仅可读(本节所用) |
DjangoModelPermissions | 结合 Django 的add/change/delete模型权限,通过perms_map把 HTTP 方法映射为权限码(POST → app.add_model、PUT/PATCH → app.change_model、DELETE → app.delete_model) |
DjangoModelPermissionsOrAnonReadOnly | 同上,但匿名用户可只读访问 |
DjangoObjectPermissions | 对象级权限,需要 Django Guardian 之类的后端支持 |
此外BasePermission的元类(rest_framework/permissions.py)还通过OperationHolderMixin实现了&(AND)、|(OR)、~(NOT)运算符,支持把多个权限类组合成表达式,例如permission_classes = [IsOwnerOrReadOnly & IsAuthenticated]。内置权限类行为均有对应的测试覆盖,可参阅 tests/test_permissions.py。
第六步:为 Browsable API 添加登录入口
应用IsAuthenticatedOrReadOnly后,如果你在浏览器中打开 Browsable API,会发现已经无法创建新的代码片段了——要恢复创建能力,必须先以某个用户身份登录。
在项目级的tutorial/urls.py顶部补充导入:
from django.urls import path, include并在文件末尾追加一个包含 DRF 登录/登出视图的 URL 模式:
urlpatterns += [ path("api-auth/", include("rest_framework.urls")), ]其中'api-auth/'前缀可以换成任何你喜欢的路径。这组视图来自 rest_framework/urls.py:它注册了两个 Django 内置认证视图——login/(使用rest_framework/login.html模板渲染)和logout/。文档头部注释明确提醒:使用 Browsable API 的登录功能时,认证设置里必须包含SessionAuthentication。
完成后刷新浏览器页面,右上角会出现 "Login" 链接。用之前createsuperuser创建的用户登录后即可重新创建片段。创建若干片段后访问/users/端点,可以看到每个用户的snippets字段中列出了与其关联的片段 id 列表——这正是第二步中PrimaryKeyRelatedField的输出效果。
第七步:对象级权限——自定义 IsOwnerOrReadOnly
目前权限粒度还不够细:我们希望所有片段对所有人可见,但只有创建者本人才能更新或删除某个片段。这属于对象级权限,需要自定义权限类。
在 snippets 应用中新建snippets/permissions.py:
from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object to edit it. """ def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. if request.method in permissions.SAFE_METHODS: return True # Write permissions are only allowed to the owner of the snippet. return obj.owner == request.user工作原理解读:
- 继承
BasePermission后只需实现has_object_permission(self, request, view, obj),其中obj是当前被操作的具体模型实例; - 先判断
request.method in permissions.SAFE_METHODS(GET/HEAD/OPTIONS),安全方法一律放行,实现"任何人可读"; - 写操作(PUT/PATCH/DELETE)则比较
obj.owner与request.user,只有两者相等才返回True。注意这里直接使用了第一步在模型上建立的owner外键,这也是为何必须先完成"片段关联用户"这一步。
然后把该权限应用到片段实例端点,修改SnippetDetail的permission_classes:
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]并导入:
from snippets.permissions import IsOwnerOrReadOnlypermission_classes列表中的权限按顺序执行,两个类需要同时通过(逻辑与)。再次打开浏览器你会发现:只有在以片段创建者身份登录时,实例端点页面上才会出现 'DELETE' 和 'PUT' 操作按钮——Browsable API 会根据对象级权限自动隐藏无权限的操作。
第八步:用 HTTP 客户端验证认证流程
权限生效后,所有修改操作都必须携带认证凭证。本教程没有显式配置 authentication classes,因此使用的是 DRF 默认认证方案:SessionAuthentication和BasicAuthentication。
- 浏览器交互:通过 Browsable API 登录后,浏览器会话(Session)自动为后续请求提供认证;
- 程序化调用:需要在每次请求中显式携带认证凭据。
默认认证方案的实现可参见 rest_framework/authentication.py:BasicAuthentication从请求的Authorization头解析 base64 编码的username:password(rest_framework/authentication.py),SessionAuthentication则借助 Django 会话框架(rest_framework/authentication.py 起)。
使用 HTTPie 未认证地创建片段,会得到明确的错误响应:
http POST http://127.0.0.1:8000/snippets/ code="print(123)" { "detail": "Authentication credentials were not provided." }通过-a(或--auth)参数附带用户名与密码后即可成功:
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)" { "id": 1, "owner": "admin", "title": "foo", "code": "print(789)", "linenos": false, "language": "python", "style": "friendly" }注意响应中的"owner": "admin"——这正是第四步ReadOnlyField(source="owner.username")的输出。整套认证与权限机制在仓库中都有对应测试验证,例如 tests/test_authentication.py 与 tests/test_permissions.py。
小结
至此,我们的 Web API 已经具备了一套相当精细的权限体系:
- 每个片段都通过
owner外键与其创建者关联,并在保存时自动生成语法高亮 HTML; - 用户与片段都有各自的 API 端点,用户端点只读并展示其名下片段 id;
- 视图级使用
IsAuthenticatedOrReadOnly保证匿名只读、登录可写; - 对象级使用自定义
IsOwnerOrReadOnly保证只有创建者能修改或删除自己的片段; - Browsable API 集成了登录/登出入口,程序化客户端则通过 HTTP Basic 认证访问。
在 第 5 部分 中,我们将为高亮片段创建 HTML 端点,并引入超链接(Hyperlinking)来串联 API 内部的关系。更系统的认证方案(Token、JWT、自定义认证类等)可参考 Authentication 指南,更多内置与自定义权限的用法见 Permissions 指南。
【免费下载链接】django-rest-frameworkWeb APIs for Django. 🎸项目地址: https://gitcode.com/gh_mirrors/dj/django-rest-framework
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考