第 14 章:权限与授权
我们当前的 Newspaper 网站存在几个问题。首先,我们希望报纸能够实现财务上的可持续发展。如果有更多时间,我们可以添加一个专门的支付应用来对访问进行收费。但至少,我们希望添加关于权限和授权的规则,例如要求用户登录后才能查看文章。作为一个成熟的 Web 框架,Django 拥有内置的授权功能,我们可以用它来限制对文章列表页面的访问,并添加额外的限制,使得只有文章的作者才能编辑或删除它。
改进 CreateView
目前,新文章的作者可以设置为任何现有用户。相反,它应该自动设置为当前登录的用户。我们可以通过修改 Django 的 CreateView 来实现这一点:移除 author 字段,改为通过 form_valid 方法自动设置。
# articles/views.py
class ArticleCreateView(CreateView):
model = Article
template_name = "article_new.html"
fields = ("title", "body") # new
def form_valid(self, form): # new
form.instance.author = self.request.user
return super().form_valid(form)我是怎么知道可以这样修改 CreateView 的呢?答案是查看了源代码,并使用了一个非常棒的资源——Classy Class-Based Views,它能详细分解 Django 中每个通用类视图的工作原理。通用类视图非常强大,但当你想自定义它们时,就必须深入理解底层的工作原理。这是类视图相对于函数视图的一个缺点:更多细节被隐藏了,必须理解继承链。你使用和自定义内置视图越多,就越能熟练地进行这样的自定义操作。通常,通过覆写像 form_valid 这样的特定方法,就能达到你想要的效果,而无需从头重写所有内容。
现在重新加载浏览器,尝试点击顶部导航栏中的”+ New”链接。它将被重定向到更新后的创建页面,其中 author 字段已经消失了。

如果你创建一篇新文章并进入管理后台,会发现作者已自动设置为当前登录的用户。
授权
当前项目存在多个与缺乏授权相关的问题。我们希望限制仅登录用户才能访问,以便将来可以对读者收费。但除此之外,任何知道正确 URL 的未登录用户都可以访问网站的任何部分。
试想一下,如果一个未登录的用户试图创建新文章会怎样。试试看:点击导航栏右上角的用户名,然后从下拉选项中选择”退出登录”。导航栏中的”+ New”链接消失了,但如果你直接访问 http://127.0.0.1:8000/articles/new/ 呢?
页面仍然可以访问。

现在尝试创建一个包含标题和正文的新文章,然后点击”保存”按钮。

出错了!这是因为我们的模型期望有一个 author 字段,且该字段链接到当前登录的用户。但由于我们没有登录,所以没有 author,提交就失败了。该怎么办呢?
Mixins(混入类)
我们希望设置一些授权规则,让只有登录用户才能访问特定的 URL。为此,我们可以使用 mixin(混入类)——一种特殊的多重继承方式,Django 用它来避免重复代码并允许自定义。例如,内置的通用 ListView 需要返回一个模板,DetailView 也一样,几乎所有视图都是如此。与其在每个通用视图中重复相同的代码,Django 将这一功能提取到一个名为 TemplateResponseMixin 的 mixin 中。ListView 和 DetailView 都使用这个 mixin 来渲染相应的模板。
如果你阅读 Django 的源代码(可以在 Github 上免费获取),你会看到 mixin 随处可见。为了限制只有登录用户才能访问视图,Django 提供了 LoginRequiredMixin,我们可以直接使用。它功能强大且极其简洁。
在 articles/views.py 文件中,导入 LoginRequiredMixin 并将其添加到 ArticleCreateView 中。确保 mixin 位于 CreateView 的左侧,这样它会被优先读取。我们希望 CreateView 知道我们打算限制访问。
就这样!完成了。
# articles/views.py
from django.contrib.auth.mixins import LoginRequiredMixin # new
from django.views.generic import ListView, DetailView
...
class ArticleCreateView(LoginRequiredMixin, CreateView): # new
...回到首页 http://127.0.0.1:8000/ 以避免重新提交表单。再次访问 http://127.0.0.1:8000/articles/new/ 进入新文章的 URL 路由。

发生了什么?Django 自动将用户重定向到了登录页面!仔细看,URL 是 http://127.0.0.1:8000/accounts/login/?next=/articles/new/,这表明我们试图访问 articles/new/,但被重定向到了登录页面。
LoginRequiredMixin
限制视图访问需要在所有现有视图的开头添加 LoginRequiredMixin。让我们更新其余的 articles 视图,因为我们不希望用户在没有登录的情况下创建、读取、更新或删除任何文章。
完整的 views.py 文件现在应该如下所示:
# articles/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Article
class ArticleListView(LoginRequiredMixin, ListView): # new
model = Article
template_name = "article_list.html"
class ArticleDetailView(LoginRequiredMixin, DetailView): # new
model = Article
template_name = "article_detail.html"
class ArticleUpdateView(LoginRequiredMixin, UpdateView): # new
model = Article
fields = (
"title",
"body",
)
template_name = "article_edit.html"
class ArticleDeleteView(LoginRequiredMixin, DeleteView): # new
model = Article
template_name = "article_delete.html"
success_url = reverse_lazy("article_list")
class ArticleCreateView(LoginRequiredMixin, CreateView):
model = Article
template_name = "article_new.html"
fields = ("title", "body",)
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)在网站中操作一下,确认重定向到登录页面的功能按预期工作。如果你需要回忆正确的 URL,可以先登录,记下每个路由(创建、编辑、删除和文章列表)的 URL。
UpdateView 和 DeleteView
我们正在稳步推进,但编辑和删除视图仍然存在问题。任何登录用户都可以修改任何文章,但我们希望限制只有文章的作者才有这个权限。我们可以为每个视图添加权限逻辑,但更优雅的方式是创建一个专门的 mixin——一个包含我们想要在 Django 代码中复用的特定功能的类。更棒的是,Django 内置了一个正好用于此目的的 mixin:UserPassesTestMixin!
要使用 UserPassesTestMixin,首先在 articles/views.py 文件的顶部导入它,然后将其添加到需要此限制的更新和删除视图中。test_func 方法由 UserPassesTestMixin 用于我们的逻辑;我们需要覆写它。在这种情况下,我们将变量 obj 设置为视图使用 get_object() 返回的当前对象。然后我们判断:如果当前对象的作者与当前网页用户(即试图进行修改的已登录用户)匹配,则允许操作。如果结果为 false,则会自动抛出错误。代码如下:
# articles/views.py
from django.contrib.auth.mixins import (
LoginRequiredMixin,
UserPassesTestMixin # new
)
from django.views.generic import ListView, DetailView
from django.views.generic.edit import UpdateView, DeleteView, CreateView
from django.urls import reverse_lazy
from .models import Article
...
class ArticleUpdateView(
LoginRequiredMixin, UserPassesTestMixin, UpdateView): # new
model = Article
fields = (
"title",
"body",
)
template_name = "article_edit.html"
def test_func(self): # new
obj = self.get_object()
return obj.author == self.request.user
class ArticleDeleteView(
LoginRequiredMixin, UserPassesTestMixin, DeleteView): # new
model = Article
template_name = "article_delete.html"
success_url = reverse_lazy("article_list")
def test_func(self): # new
obj = self.get_object()
return obj.author == self.request.user在使用 class-based views 配合 mixin 时,顺序至关重要。LoginRequiredMixin 放在第一位,以确保强制登录;然后添加 UserPassesTestMixin 作为额外的功能层;最后是 UpdateView 或 DeleteView。只有按照这个顺序,代码才能正常工作。
用你的 testuser 账户登录,进入文章列表页面。如果代码正常运行,你应该无法编辑或删除由超级用户账户撰写的任何文章;相反,你会看到一个”权限被拒绝”的 403 错误页面。

但是,如果你用 testuser 创建一篇新文章,就可以编辑和删除它。而如果你改用超级用户账户登录,则可以编辑和删除由该作者撰写的文章。
模板逻辑
虽然我们已经成功限制了每篇文章的”编辑”和”删除”页面访问权限,但这些链接仍然显示在文章列表页面和单篇文章页面上。更好的做法是不向没有权限的用户显示它们。换句话说,我们希望限制这些链接只对文章作者可见。
我们可以通过在文章列表和文章详情模板文件中使用内置的 if 过滤器添加简单的逻辑,让只有文章作者才能看到编辑和删除链接。
<!-- templates/article_list.html -->
...
<div class="card-footer text-center text-muted">
<!-- new code here -->
{% if article.author.pk == request.user.pk %}
<a href="{% url 'article_edit' article.pk %}">Edit</a>
<a href="{% url 'article_delete' article.pk %}">Delete</a>
{% endif %} <!-- new code here -->
</div>
...<!-- templates/article_detail.html -->
{% extends "base.html" %}
{% block content %}
<div class="article-entry">
<h2>{{ object.title }}</h2>
<p>by {{ object.author }} | {{ object.date }}</p>
<p>{{ object.body }}</p>
</div>
<div>
<!-- new code here -->
{% if article.author.pk == request.user.pk %}
<p><a href="{% url 'article_edit' article.pk %}">Edit</a>
<a href="{% url 'article_delete' article.pk %}">Delete</a>
</p>
{% endif %} <!-- new code here -->
<p>Back to <a href="{% url 'article_list' %}">All Articles</a>.</p>
</div>
{% endblock content %}为了确认我们新的编辑/删除逻辑按预期工作,请确保你以 testuser 登录,并使用顶部导航栏中的”+ New”按钮创建一篇新文章。如果刷新所有文章页面,只有由 testuser 撰写的文章才应该显示编辑和删除链接。

点击文章名称进入详情页面。编辑和删除链接是可见的。

但是,如果你进入由超级用户创建的文章详情页面,这些链接就消失了。

Git
本章结束了,快速用 Git 保存一下。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "permissions and authorizations"
(.venv) $ git push origin main小结
我们的报纸应用差不多完成了。到这一步,我们还可以做更多改进,比如只为合适的用户显示编辑和删除链接(这涉及自定义模板标签),但总体来说,应用的状态已经不错了。文章配置正确,设置了权限和授权,并且拥有可用的用户认证流程。最后还需要添加的功能是允许其他登录用户发表评论——我们将在下一章中介绍。