第 10 章:用户认证

现在我们已经有了一个可用的自定义用户模型,可以添加每个网站都需要的功能:用户注册、登录和注销。Django 提供了用户登录和注销所需的一切,但我们必须创建自己的表单来允许新用户注册。我们还将构建一个包含这三个功能链接的基本首页,这样用户就不必每次都手动输入 URL 了。

模板

默认情况下,Django 模板加载器会在每个应用内部的嵌套结构中查找模板。例如,accounts 应用中的 home.html 模板需要放在 accounts/templates/accounts/home.html 这种路径下。然而,使用项目级别的单一模板目录方法更简洁且扩展性更好,所以我们将采用这种方式。

创建一个新的项目级别 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"],  # 新增
        ...
    }
]

想想登录或退出网站时会发生什么——你会立即被重定向到一个后续页面。我们需要告诉 Django 在每个情况下将用户发送到哪里。LOGIN_REDIRECT_URLLOGOUT_REDIRECT_URL 设置就是用来做这个的。我们将两者都配置为重定向到名为 'home' 的首页。请记住,当我们创建 URL 路由时,可以为每个路由添加一个名称。所以当我们创建首页 URL 时,我们将其命名为 'home'

django_project/settings.py 文件的底部添加这两行代码。

# django_project/settings.py
LOGIN_REDIRECT_URL = "home"  # 新增
LOGOUT_REDIRECT_URL = "home"  # 新增

现在我们在文本编辑器中创建四个新模板:

  • templates/base.html
  • templates/home.html
  • templates/registration/login.html
  • templates/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  # 新增 include
from django.views.generic.base import TemplateView  # 新增

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),  # 新增
    path("accounts/", include("django.contrib.auth.urls")),  # 新增
    path("", TemplateView.as_view(template_name="home.html"), name="home"),  # 新增
]

现在用文本编辑器创建一个名为 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”链接。你将被重定向到我们的注册页面。可以看到年龄字段也包含在内!

创建一个新用户。我的用户名是 testuser,年龄设置为 25。

注册页面

成功提交表单后,你将被重定向到登录页面。使用你的新用户登录,然后再次被重定向到首页,并显示新用户的个性化问候语。既然我们有了新的年龄字段,让我们把它添加到 home.html 模板中。它是用户模型上的一个字段,所以要显示它,只需要使用 { user.age }

<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock title %}
{% block content %}
{% if user.is_authenticated %}
    <!-- 这里是新代码! -->
    Hi {{ user.username }}! You are {{ user.age }} years old.
    <!-- 新代码结束 -->
    <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 %}

保存文件并刷新首页。

testuser 首页

一切按预期工作。

管理后台

在浏览器中访问管理后台 http://127.0.0.1:8000/admin 并登录以查看两个用户账户。

错误的管理后台登录

这是什么?为什么我们登录不了?我们现在登录的是新创建的 testuser 账户,而不是超级用户账户。只有超级用户账户才有权限登录管理后台!所以,请改用你的超级用户账户登录。

登录后,你应该会看到正常的管理后台首页。点击 Users 可以看到两个用户:我们刚刚创建的 testuser 账户和之前的超级用户名(我的用户名是 wsv)。

管理后台中的用户列表

一切正常,但你可能会注意到我们的 testuser 没有”email address”。为什么呢?我们的注册页面没有电子邮件字段,因为它没有包含在 accounts/forms.py 中。这是一个重要的点:仅仅因为用户模型有某个字段,并不意味着它会被包含在自定义注册表单中,除非显式添加。现在让我们来添加它。

目前,在 accounts/forms.py 中的 fields 下,我们使用 Meta.fields,它会显示默认设置(用户名/密码),然后我们显式添加了自定义字段 age。但我们也可以显式设置要显示的字段,让我们将其更新为要求输入用户名/电子邮件/年龄/密码,设置为 ('username', 'email', 'age',)。我们不需要包含密码字段,因为它们是必需的!其他字段可以根据需要任意配置。

# 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",
            "age",
        )  # 新增

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = (
            "username",
            "email",
            "age",
        )  # 新增

注销你的超级用户账户,再次访问 http://127.0.0.1:8000/accounts/signup/——你可以看到额外的”Email address”字段出现了。用一个新用户账户注册。我将我的命名为 testuser2,年龄 18,电子邮件地址为 testuser2@email.com

新的注册页面

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

testuser2 首页问候语

切换回管理后台页面,使用我们的超级用户账户登录,可以看到全部三个用户都显示出来了。

管理后台中的三个用户

Django 的用户认证流程需要一些设置,但你应该开始看到它也为我们提供了令人难以置信的灵活性,可以随心所欲地配置注册和登录过程。

测试

新的注册页面有视图、URL 和模板,所有这些都应该被测试。打开包含上一章 UsersManagersTests 代码的 accounts/tests.py 文件。在其下方添加一个名为 SignupPageTests 的新类,我们将在下面进行审查。

# accounts/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse  # 新增

class UsersManagersTests(TestCase):
    ...

class SignupPageTests(TestCase):  # 新增
    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 状态码。第二个测试检查视图,通过 reverse('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

总结

到目前为止,我们的新闻应用已经拥有了自定义用户模型和可用的注册、登录、注销页面。但你可能已经注意到,我们的网站看起来还可以更好。在下一章中,我们将使用 Bootstrap 添加 CSS 样式,并创建一个专用的 pages 应用。

参考资料