允许用户拥有自己的数据
用户应能在学习日志中输入私人数据,因此我们将创建一个系统来确定哪些数据属于哪个用户。然后,我们将限制某些页面的访问权限,以便用户只能使用自己的数据。
我们将修改 "主题 "模型,使每个主题都属于某个特定用户。这样也可以处理条目,因为每个条目都属于一个特定的主题。我们将首先限制某些页面的访问权限。
使用 @login_required 限制访问
通过 @login_required 装饰器,Django 可以轻松限制对某些页面的访问。回顾第 11 章,装饰器是放在函数定义之前的指令,用于修改函数的行为。让我们来看一个例子。
限制访问主题页面
每个主题将由一个用户拥有,因此只有注册用户才能申请主题页面。将以下代码添加到 learning_logs/views.py:
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
--snip--
@login_required
def topics(request):
"""Show all topics."""
--snip--
我们首先导入 login_required() 函数。通过在 login_required 前加上 @ 符号,我们将 login_required() 作为装饰器应用到 topics() 视图函数中。因此,Python 知道在运行 topics() 中的代码之前运行 login_required() 中的代码。
login_required() 中的代码会检查用户是否已登录,只有当用户已登录时,Django 才会运行 topics() 中的代码。如果用户没有登录,则会重定向到登录页面。
为了让重定向生效,我们需要修改 settings.py 以便 Django 知道在哪里可以找到登录页面。在 settings.py 的末尾添加以下内容:
--snip--
# My settings.
LOGIN_REDIRECT_URL = 'learning_logs:index'
LOGOUT_REDIRECT_URL = 'learning_logs:index'
LOGIN_URL = 'accounts:login'
现在,当未认证用户请求受 @login_required 装饰器保护的页面时,Django 会将用户发送到 settings.py 中 LOGIN_URL 所定义的 URL。您可以通过注销任何用户账户并访问主页来测试此设置。单击 "主题 "链接,该链接会将您重定向到登录页面。然后登录任何账户,在主页上再次点击主题链接。您应该可以访问主题页面。
限制整个学习日志的访问权限
Django 可以轻松限制页面访问,但您必须决定保护哪些页面。最好先考虑哪些页面需要不受限制,然后再限制项目中的所有其他页面。你可以很容易地纠正过度限制的访问,而且这样做比不限制敏感页面要危险得多。
在学习日志中,我们将保持主页和注册页面不受限制。我们将限制其他页面的访问。
下面是 learning_logs/views.py,除了 index() 之外,每个视图都应用了 @login_required 装饰器:
--snip--
@login_required
def topics(request):
--snip--
@login_required
def topic(request, topic_id):
--snip--
@login_required
def new_topic(request):
--snip--
@login_required
def new_entry(request, topic_id):
--snip--
@login_required
def edit_entry(request, entry_id):
--snip--
请尝试在退出登录的情况下访问这些页面;您应该会被重定向到登录页面。您也将无法点击页面链接,如 new_topic
。但如果输入 URL http://localhost:8000/new_topic/ ,就会重定向到登录页面。您应该限制访问任何可公开访问且与用户私人数据有关的 URL。
将数据连接到某些用户
接下来,我们需要将数据连接到提交数据的用户。我们只需将层次结构中最高的数据与用户连接起来,较低层次的数据就会随之而来。在学习日志中,主题是应用程序中最高级别的数据,所有条目都与主题相连。只要每个主题都属于一个特定的用户,我们就可以追踪数据库中每个条目的所有权。
我们将通过添加与用户的外键关系来修改主题模型。然后,我们将迁移数据库。最后,我们将修改部分视图,使其只显示与当前登录用户相关的数据。
修改主题模型
对 models.py 的修改只有两行:
from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
"""A topic the user is learning about."""
Text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
"""Return a string representing the topic."""
Return self.text
class Entry(models.Model):
--snip--
我们从 django.contrib.auth 导入用户模型。然后,我们在 Topic 中添加一个所有者字段,这样就与用户模型建立了外键关系。如果用户被删除,与该用户相关的所有主题也将被删除。
识别现有用户
当我们迁移数据库时,Django 将修改数据库,以便存储每个主题和用户之间的连接。要进行迁移,Django 需要知道将哪个用户与每个现有主题关联起来。最简单的方法是先将所有现有主题分配给一个用户—例如超级用户。但首先,我们需要知道该用户的 ID。
让我们看看目前创建的所有用户的 ID。启动一个 Django shell 会话并发出以下命令:
(ll_env)learning_log$ python manage.py shell
❶ >>> from django.contrib.auth.models import User
❷ >>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>
❸ >>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>
我们首先将用户模型导入 shell 会话❶。然后,我们查看迄今为止创建的所有用户❷。输出结果显示我的项目版本中有三个用户:ll_admin
、eric
和 willie
。
接下来,我们在用户列表中循环,打印每个用户的用户名和 ID ❸。当 Django 询问哪个用户与现有主题相关联时,我们将使用其中一个 ID 值。
迁移数据库
现在我们知道了 ID,就可以迁移数据库了。当我们这样做时,Python 会要求我们将 Topic 模型暂时连接到特定的所有者,或者在 models.py 文件中添加一个默认值来告诉它该怎么做。选择选项 1:
❶ (ll_env)learning_log$ python manage.py makemigrations learni
ng_logs
❷ It is impossible to add a non-nullable field 'owner' to topi
c without
specifying a default. This is because...
❸ Please select a fix:
1) Provide a one-off default now (will be set on all existin
g rows with a
null value for this column)
2) Quit and manually define a default value in models.py.
❹ Select an option: 1
❺ Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are availabl
e...
Type 'exit' to exit this prompt
❻ >>> 1
Migrations for 'learning_logs':
learning_logs/migrations/0003_topic_owner.py
- Add field owner to topic
(ll_env)learning_log$
我们首先发布 makemigrations
命令❶。在输出中,Django 指出我们正试图在一个未指定默认值的现有模型(主题)中添加一个必填(不可为空)字段❷。Django 给了我们两个选择:我们可以立即提供一个默认值,或者我们可以退出并在 models.py 中添加一个默认值❸。这里我选择了第一个选项❹。然后,Django 会要求我们输入默认值❺。
为了将所有现有主题与最初的管理员用户 ll_admin 关联,我输入了用户 ID 1 ❻。您可以使用您创建的任何用户的 ID,它不一定是超级用户。Django 会使用此值迁移数据库,并生成迁移文件 0003_topic_owner.py,将字段所有者添加到 Topic 模型中。
现在我们可以执行迁移了。在活动虚拟环境中输入以下内容:
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, learning_l
ogs, sessions
Running migrations:
❶ Applying learning_logs.0003_topic_owner... OK
(ll_env)learning_log$
Django 应用了新的迁移,结果是 OK ❶。
我们可以在 shell 会话中验证迁移是否按预期进行,如下所示:
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
>>>
我们从 learning_logs.models 中导入 Topic,然后循环浏览所有已有的主题,打印每个主题及其所属用户。如果运行此代码时出现错误,请尝试退出 shell 并启动一个新的 shell)。
您可以简单地重置数据库而不是迁移,但这样做会丢失所有现有数据。学习如何在迁移数据库的同时保持用户数据的完整性是一种很好的做法。如果你确实想从一个全新的数据库开始,请执行 python manage.py flush 命令重建数据库结构。你必须创建一个新的超级用户,而且所有数据都将丢失。 |
限制适当用户的主题访问
目前,如果您已登录,无论您以哪个用户身份登录,都能看到所有主题。我们将改变这种情况,只向用户显示属于他们的主题。
对 views.py 中的 topics() 函数做如下修改:
--snip--
@login_required
def topics(request):
"""Show all topics."""
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.xhtml', context)
--snip--
当用户登录时,请求对象会设置 request.user 属性,其中包含该用户的相关信息。查询 Topic.objects.filter(owner=request.user)告诉 Django 从数据库中只检索 owner 属性与当前用户匹配的 Topic 对象。由于我们并不改变主题的显示方式,所以根本不需要更改主题页面的模板。
要查看此方法是否有效,请以连接了所有现有主题的用户身份登录,然后转到主题页面。你应该能看到所有主题。现在退出并以另一个用户身份重新登录。你应该会看到 "尚未添加任何主题 "的信息。
保护用户的主题
我们还没有限制对主题页面的访问,因此任何注册用户都可以尝试一系列 URL(如 http://localhost:8000/topics/1/ ),并检索碰巧匹配的主题页面。
自己试试看。以拥有所有主题的用户身份登录后,复制某个主题的 URL 或记下 URL 中的 ID,然后退出并以另一个用户身份重新登录。输入该主题的 URL。即使你是以不同的用户身份登录,你也应该可以读取条目。
现在我们将通过在 topic() 视图函数中检索请求的条目之前执行检查来解决这个问题:
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.http import Http404 # 1
--snip--
@login_required
def topic(request, topic_id):
"""Show a single topic and all its entries."""
topic = Topic.objects.get(id=topic_id)
# Make sure the topic belongs to the current user.
if topic.owner != request.user: # 2
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.xhtml', context)
--snip--
404 响应是一种标准错误响应,当请求的资源在服务器上不存在时会返回。在这里,我们导入了 Http404 异常❶,如果用户请求了他们不应该访问的主题,我们就会引发该异常。收到主题请求后,我们会先确定主题的用户是否与当前登录的用户一致,然后再渲染页面。如果请求的主题所有者与当前用户不一致,我们就会引发 Http404 异常❷,Django 会返回一个 404 错误页面。
现在,如果您尝试查看其他用户的主题条目,您将看到来自 Django 的 "未找到页面 "消息。在第 20 章中,我们将对项目进行配置,使用户看到的是正确的错误页面,而不是调试页面。
保护 edit_entry 页面
edit_entry 页面的 URL 形式为 http://localhost:8000/edit_entry/entry_id/ ,其中 entry_id 是一个数字。让我们来保护这个页面,使任何人都无法使用该 URL 访问他人的条目:
--snip--
@login_required
def edit_entry(request, entry_id):
"""Edit an existing entry."""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user:
raise Http404
if request.method != 'POST':
--snip--
我们检索条目和与该条目相关的主题。然后,我们会检查主题的所有者是否与当前登录的用户匹配;如果不匹配,我们就会引发 Http404 异常。
将新主题与当前用户关联
目前,添加新主题的页面已损坏,因为它无法将新主题与任何特定用户关联。如果您尝试添加新主题,您会看到信息 IntegrityError 以及 NOT NULL 约束失败:learning_logs_topic.owner_id。Django 表示,如果没有为主题的所有者字段指定值,就无法创建新主题。
我们可以直接解决这个问题,因为我们可以通过请求对象访问当前用户。添加以下代码,将新主题与当前用户关联:
--snip--
@login_required
def new_topic(request):
--snip--
else:
# POST data submitted; process data.
form = TopicForm(data=request.POST)
if form.is_valid():
new_topic = form.save(commit=False) # 1
new_topic.owner = request.user # 2
new_topic.save() # 3
return redirect('learning_logs:topics')
# Display a blank or invalid form.
context = {'form': form}
return render(request, 'learning_logs/new_topic.xhtml', context)
--snip--
当我们第一次调用 form.save() 时,我们传递了 commit=False 参数,因为我们需要在将新主题保存到数据库❶ 之前修改它。然后,我们将新主题的所有者属性设置为当前用户❷。最后,我们在刚刚定义的主题实例 ❸ 上调用 save()。现在,该主题已包含所有所需数据,并将成功保存。
您可以为任意多的用户添加任意多的新主题。每个用户只能访问自己的数据,无论是查看数据、输入新数据还是修改旧数据。
总结
在本章中,你将学习如何通过表单让用户添加新主题和条目以及编辑现有条目。然后,您还学习了如何实现用户账户。您为现有用户提供了登录和注销的功能,并使用 Django 的默认 UserCreationForm 让用户创建新账户。
在建立了简单的用户身份验证和注册系统后,您使用 @login_required 装饰器限制了登录用户对某些页面的访问。然后通过外键关系为特定用户分配数据。您还学习了在迁移过程中需要指定一些默认数据时如何迁移数据库。
最后,您学会了如何通过修改视图函数确保用户只能看到属于自己的数据。你使用 filter() 方法检索了适当的数据,并将所请求数据的所有者与当前登录的用户进行了比较。
哪些数据应该提供,哪些数据应该保护,可能并不总是一目了然,但这种技能会在实践中逐渐形成。我们在本章中为保护用户数据安全所做的决定也说明了为什么在创建项目时与他人合作是个好主意:让他人查看你的项目会让你更容易发现易受攻击的地方。
现在,你已经在本地计算机上运行了一个功能完备的项目。在最后一章中,你将为学习日志设计样式,使其在视觉上更具吸引力,并将项目部署到服务器上,这样任何可以访问互联网的人都可以注册并建立账户。