第 10 章:用户身份验证¶
现在我们已经有了一个可用的自定义用户模型,接下来可以为网站添加每个网站都需要的功能:用户注册、登录和注销。Django 为我们提供了用户登录和注销所需的一切,但我们必须创建自己的表单来允许新用户注册。我们还将构建一个基本的主页,其中包含指向这三个功能的链接,这样用户就不必每次都手动输入 URL。
模板¶
默认情况下,Django 模板加载器会在每个应用的嵌套结构中查找模板。例如,accounts 应用中的 home.html 模板需要放在 accounts/templates/accounts/home.html 这样的结构中。然而,使用单个项目级别的 templates 目录的方法更简洁,也更易于扩展,所以我们将采用这种方式。
创建一个新的项目级别 templates 目录,并在其中创建一个 registration 目录,Django 将在此查找与登录和注册相关的模板。
(.venv) $ mkdir templates
(.venv) $ mkdir templates/registration
我们需要通过在 django_project/settings.py 中更新 "DIRS" 配置来告诉 Django 这个新目录。
# django_project/settings.py
TEMPLATES = [
{
...
"DIRS": [BASE_DIR / "templates"], # new
...
}
]
如果你想想登录或注销网站时会发生什么,你会立即被重定向到后续页面。我们需要告诉 Django 在每种情况下将用户发送到哪里。LOGIN_REDIRECT_URL 和 LOGOUT_REDIRECT_URL 设置就是用来做这个的。我们将两者都配置为重定向到我们的主页,URL 名称为 'home'。记住,当我们创建 URL 路由时,可以为每个路由添加一个名称。所以当我们创建主页 URL 时,我们将它命名为 'home'。
在 django_project/settings.py 文件底部添加这两行。
# django_project/settings.py
LOGIN_REDIRECT_URL = "home" # new
LOGOUT_REDIRECT_URL = "home" # new
现在我们可以在文本编辑器中创建四个新模板:
templates/base.htmltemplates/home.htmltemplates/registration/login.htmltemplates/registration/signup.html
以下是每个文件需要使用的 HTML 代码。base.html 将被项目中的每个其他模板继承。使用像 {% block content %} 这样的块,我们之后可以在其他模板中仅在这个位置覆盖内容。
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}Newspaper App{% endblock title %}</title>
</head>
<body>
<main>
{% block content %}
{% endblock content %}
</main>
</body>
</html>
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock title %}
{% block content %}
{% if user.is_authenticated %}
<p>Hi {{ user.username }}!</p>
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit">Log Out</button>
</form>
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a> |
<a href="{% url 'signup' %}">Sign Up</a>
{% endif %}
{% endblock content %}
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% block title %}Log In{% endblock title %}
{% block content %}
<h2>Log In</h2>
<form method="post">{% csrf_token %}
{{ form }}
<button type="submit">Log In</button>
</form>
{% endblock content %}
<!-- templates/registration/signup.html -->
{% extends "base.html" %}
{% block title %}Sign Up{% endblock title %}
{% block content %}
<h2>Sign Up</h2>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<button type="submit">Sign Up</button>
</form>
{% endblock content %}
我们的模板现在都准备好了。相关的 URL 和视图还需要完成。
URL¶
让我们从 URL 路由开始。在我们的 django_project/urls.py 文件中,我们希望 home.html 模板作为主页显示,但我们还不想创建一个专门的 pages 应用。我们可以使用快捷方式,导入 TemplateView 并在 URL 模式中直接设置 template_name。
接下来,我们要"include"accounts 应用和内置的 auth 应用。原因是内置的 auth 应用已经提供了登录和注销的视图和 URL。但要注册,我们必须创建自己的视图和 URL。为了确保我们的 URL 路由一致,我们将它们都放在 accounts/ 下,这样最终的 URL 将是 /accounts/login、/accounts/logout 和 /accounts/signup。
# django_project/urls.py
from django.contrib import admin
from django.urls import path, include # new
from django.views.generic.base import TemplateView # new
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("accounts.urls")), # new
path("accounts/", include("django.contrib.auth.urls")), # new
path("", TemplateView.as_view(template_name="home.html"),
name="home"), # new
]
现在用文本编辑器创建一个名为 accounts/urls.py 的文件,并用以下代码更新它:
# accounts/urls.py
from django.urls import path
from .views import SignUpView
urlpatterns = [
path("signup/", SignUpView.as_view(), name="signup"),
]
最后一步是 views.py 文件,其中包含注册表单的逻辑。我们在这里使用 Django 的泛型 CreateView,并告诉它使用我们的 CustomUserCreationForm,在用户成功注册后重定向到登录页面,并且我们的模板命名为 signup.html。
# accounts/views.py
from django.urls import reverse_lazy
from django.views.generic import CreateView
from .forms import CustomUserCreationForm
class SignUpView(CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy("login")
template_name = "registration/signup.html"
好了,终于完成了!让我们来测试一下。使用 python manage.py runserver 启动服务器,然后访问主页。

我们在上一章中已经登录了管理员,所以你应该在这里看到一个个性化的问候。点击"Log Out"链接。

现在我们在已注销的主页上。点击 Log In 链接并使用你的超级用户凭据。成功登录后,你将被重定向到主页,并看到同样的个性化问候。它起作用了!
现在,使用"Log Out"链接返回已注销的主页,这次点击"Sign Up"链接。你将被重定向到我们的注册页面。注意 age 字段也包含在内!

创建一个新用户。我的用户名为 testuser,我设置年龄为 25。
成功提交表单后,你将被重定向到登录页面。使用你的新用户登录,你将再次被重定向到主页,并看到新用户的个性化问候。但由于我们有了新的 age 字段,让我们把它添加到 home.html 模板中。它是用户模型上的一个字段,所以要显示它,我们只需要使用 {{ user.age }}。
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock title %}
{% block content %}
{% if user.is_authenticated %}
<!-- new code here! -->
Hi {{ user.username }}! You are {{ user.age }} years old.
<!-- end of new code -->
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit">Log Out</button>
</form>
{% else %}
<p>You are not logged in</p>
<a href="{% url 'login' %}">Log In</a> |
<a href="{% url 'signup' %}">Sign Up</a>
{% endif %}
{% endblock content %}
保存文件并刷新主页。

一切都按预期运行。
管理员¶
在浏览器中导航到 http://127.0.0.1:8000/admin 的管理员页面,登录查看两个用户账户。

这是什么?为什么我们登录不了?我们已经用新的 testuser 账户登录了,而不是超级用户账户。只有超级用户账户才有权限登录管理员!所以,改用你的超级用户账户登录。
完成后,你应该看到正常的管理员主页。点击 Users 查看我们的两个用户:我们刚刚创建的 testuser 账户和你之前的超级用户名(我的是 wsv)。

一切正常,但你可能会注意到 testuser 没有"email address"。这是为什么呢?我们的注册页面没有 email 字段,因为它没有包含在 accounts/forms.py 中。这是一个重要的点:仅仅因为用户模型有一个字段,并不意味着它会被包含在我们的自定义注册表单中,除非明确添加。让我们现在来做。
目前,在 accounts/forms.py 的 fields 下,我们使用的是 Meta.fields,它显示 username/password 的默认设置,然后我们还明确添加了自定义字段 age。但我们也可以明确设置我们希望显示哪些字段,所以让我们更新它,通过设置为 ('username', 'email', 'age',) 来要求输入 username/email/age/password。我们不需要包含 password 字段,因为它们是必填的!其他字段可以按照我们的选择进行配置。
# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = (
"username",
"email", # new
"age",
)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = (
"username",
"email", # new
"age",
)
退出你的超级用户账户,再次尝试 http://127.0.0.1:8000/accounts/signup/——你可以看到额外的"Email address"字段已经出现了。使用一个新的用户账户注册。我将我的命名为 testuser2,年龄 18 岁,电子邮件地址为 testuser2@email.com。

点击"Sign Up"按钮,继续登录。你会在主页上看到个性化的问候。

切换回管理员页面,使用我们的超级用户账户登录,可以看到所有三个用户都显示在那里。

Django 的用户身份验证流程需要一些设置。不过,你应该已经开始看到,它也为我们提供了难以置信的灵活性,可以按照我们想要的方式精确配置注册和登录过程。
测试¶
新的注册页面有一个视图、URL 和模板,都应该进行测试。打开 accounts/tests.py 文件,其中包含上一章中 UsersManagersTests 的代码。在其下方添加一个名为 SignupPageTests 的新类,我们将在下面进行回顾。
# accounts/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse # new
class UsersManagersTests(TestCase):
...
class SignupPageTests(TestCase): # new
def test_url_exists_at_correct_location_signupview(self):
response = self.client.get("/accounts/signup/")
self.assertEqual(response.status_code, 200)
def test_signup_view_name(self):
response = self.client.get(reverse("signup"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "registration/signup.html")
def test_signup_form(self):
response = self.client.post(
reverse("signup"),
{
"username": "testuser",
"email": "testuser@email.com",
"password1": "testpass123",
"password2": "testpass123",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(get_user_model().objects.all().count(), 1)
self.assertEqual(get_user_model().objects.all()[0].username, "testuser")
self.assertEqual(
get_user_model().objects.all()[0].email, "testuser@email.com"
)
在顶部,我们导入了 reverse 来验证 URL 和视图是否正常工作。然后我们创建了一个名为 SignupPageTests 的新测试类。第一个测试检查我们的注册页面是否在正确的 URL 上并返回 200 状态码。第二个测试检查视图,反向解析 signup(即 URL 名称),然后确认 200 状态码以及确认正在使用我们的 signup.html 模板。
第三个测试通过发送 post 请求来检查我们的表单。表单提交后,我们确认预期的 302 重定向,并确认测试数据库中现在有一个匹配用户名和电子邮件地址的用户。我们不检查密码,因为 Django 默认会自动加密密码。这就是为什么如果你查看用户的管理员视图,你可以更改密码但看不到当前密码的原因。
使用 python manage.py test 运行测试,检查所有测试是否按预期通过。
(.venv) $ python manage.py test
Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 5 tests in 0.183s
OK
Destroying test database for alias 'default'...
Git¶
在进入下一章之前,让我们用 Git 记录我们的工作并将其存储在 GitHub 上。
(.venv) $ git status
(.venv) $ git add -A
(.venv) $ git commit -m "user authentication"
(.venv) $ git push origin main
结论¶
到目前为止,我们的 Newspaper 应用拥有一个自定义用户模型以及可用的注册、登录和注销页面。但你可能已经注意到我们的网站可以看起来更好。在下一章中,我们将使用 Bootstrap 添加 CSS 样式,并创建一个专门的 pages 应用。