第 14 章:权限与授权¶
我们当前的 Newspaper 网站存在几个问题。首先,我们希望报纸在财务上是可持续的。如果有更多时间,我们可以添加一个专门的支付应用来收取访问费用。但至少,我们希望添加关于权限和授权的规则,例如要求用户登录后才能查看文章。作为一个成熟的 Web 框架,Django 具有内置的授权功能,我们可以用它来限制对文章列表页面的访问,并添加额外的限制,使只有文章的作者才能编辑或删除它。
改进 CreateView¶
目前,新文章的作者可以设置为任何现有用户。相反,它应该自动设置为当前登录的用户。我们可以通过修改 Django 的 CreateView 来实现这一点,移除作者字段,改为通过 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"链接。它将重定向到更新后的创建页面,其中作者不再是表单字段。

如果你创建了一篇新文章并进入 admin 后台,你会看到它自动设置为当前登录的用户。
授权¶
当前项目缺乏授权机制,这带来了多个问题。我们希望将访问限制为仅限用户,以便有朝一日可以向读者收取访问报纸的费用。但
除此之外,任何知道正确 URL 的未登录用户都可以访问网站的任何部分。
考虑一下如果未登录用户尝试创建新文章会发生什么。要试一试,请点击导航栏右上角的用户名,然后从下拉选项中选择"Log Out"。"+ New"链接从导航栏中消失了,但如果你直接访问 http://127.0.0.1:8000/articles/new/ 会怎样呢?
页面仍然存在。

现在,尝试用标题和正文创建一篇新文章,然后点击"Save"按钮。

出错了!这是因为我们的模型期望有一个与当前登录用户关联的作者字段。但由于我们没有登录,没有作者信息,所以提交失败了。该怎么办呢?
Mixin¶
我们希望设置一些授权规则,使只有登录用户才能访问特定的 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。让我们更新其余的文章视图,因为我们不希望未登录的用户能够创建、阅读、更新或删除文章。
完整的 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
在使用 mixin 和基于类的视图时,顺序至关重要。LoginRequiredMixin 放在首位以强制登录,然后添加 UserPassesTestMixin 作为额外的功能层,最后是 UpdateView 或 DeleteView。只有按照这个顺序,代码才能正常工作。
使用你的 testuser 账户登录,然后进入文章列表页面。如果代码正常工作,你应该无法编辑或删除由超级用户账户撰写的任何帖子;相反,你将看到一个 Permission Denied 403 错误页面。

但是,如果你用 testuser 创建一篇新文章,你将能够编辑和删除它。如果你改用超级用户账户登录,你可以编辑和删除由该作者撰写的帖子。
模板逻辑¶
虽然我们已成功限制了对每篇文章"编辑"和"删除"页面的访问,但这些链接仍然显示在文章列表页面和单个文章页面上。最好不要向无法访问它们的用户显示这些链接。换句话说,我们希望只对文章的作者显示这些链接。
我们可以在 article_list 和 article_detail 模板文件中添加简单的逻辑,使用内置的 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 撰写的文章才应该显示编辑和删除链接。

点击文章名称导航到其详情页面。编辑和删除链接可见。

但是,如果你导航到由 superuser 创建的文章详情页面,链接就消失了。

Git¶
在本章结束时,使用 Git 快速保存一下。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "permissions and authorizations"
(.venv) $ git push origin main
结论¶
我们的 Newspaper 应用差不多完成了。在此阶段我们可以采取进一步的措施,例如只对相应用户显示编辑和删除链接,这将涉及自定义模板标签,但总体而言,应用已经成型。我们的文章配置正确,设置了权限和授权,并拥有可用的用户认证流程。最后一个需要的功能是让已登录的用户可以留下评论,我们将在下一章中介绍。