前言
- 之前做了什么
前面的章节里,我们的项目具备了用户注册、登录、登出的功能。
我们使用了 Django 自带的用户功能,在 URL 配置中使用了:
python
path("accounts/", include("django.contrib.auth.urls")),
从而可以快速搭建用户功能并使用。
- 现在要做什么
- 首先是:如何给我们的用户新增自定义字段
- 然后是:另外一些功能,比如如何修改用户信息
- 最终我们需要完成的用户账户相关功能有:
- 注册(待修改)
- 登录(待修改)
- 登出(待修改)
- 修改用户信息(待添加)
- 修改密码(待添加)
- 忘记密码时通过邮件重置(待添加)
- 修改密码(待添加)
1. 用户新增字段
1.0 方案 & 选择
- 方案1:"用户配置文件"方法
- 通过在现有的用户模型上创建一个指向包含额外信息字段的单独模型的 OneToOneField 来扩展现有的用户模型。
- 其核心理念是将身份验证保留给用户本身,而不与非身份验证相关的用户信息捆绑在一起。
- 方案2:第二种方法是创建自定义用户模型(这是官方 Django 文档中推荐的)。
- 我们可以扩展 AbstractUser 来创建一个自定义用户模型
- 其行为与默认的 User 模型完全相同,但将来可以进行自定义。
- 方案3:第三种更高级的方法是使用 AbstractBaseUser 创建一个自定义用户模型
- 它提供完全的控制权限。
- 这种方法只适用于高级 Django 用户。
本章将使用 方案2,即继承 AbstractUser 来自定义用户,取代我们之前的用户模型。
并且全面回顾:注册、登录、登出 功能。
1.1 数据库删除 & 重建
由于之前并没有使用 AbstractUser 方案,因此在新方案会改动模型的情况下,需要清理数据库。
删除之前的数据库并重建是最简单的方案。
清理数据库中的表是可选方案。
这里我们选择删除数据库并重建。
- 终端进入数据库
bash
sudo -u postgres psql
- 断开所有连接
bash
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'your_database_name';
- 删除数据库
bash
DROP DATABASE 数据库名;
- 重新创建数据库
bash
CREATE DATABASE 数据库名 OWNER 用户名;
-
注:
- 我们使用了和之前相同的"数据库名"、"用户名"
- 其他数据库操作可以参考
- 《【Web应用开发笔记】★ Django 后端命令、代码总结 ★》
- 9.2 节
-
清理 migrations
- 应用名/migrations/ 文件夹下
- 除了 init.py,其他所有 .py、.pyc 都可以删掉
1.2 如何新增字段
在模型开发时(models.py)中:
- 继承 AbstractUser
- 添加成员
在后面的 2.4.1 节有具体案例。
2. 用户账户功能开发
2.1 前置操作
- 新建 app
bash
python manage.py startapp accounts
2.2 URL 配置
- learn_django/urls.py
- 需要使用 Django 内置的页面("django.contrib.auth.urls")
- 此外还需要自己创建"注册"、"修改用户信息"等页面("accounts.urls")
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path("accounts/", include("accounts.urls")),
]
- accounts/urls.py
python
# accounts/urls.py
from django.urls import path
from .views import SignUpView
urlpatterns = [
path("signup/", SignUpView.as_view(), name="signup"),
path("profile/", UserUpdateView.as_view(), name="user_update"),
]
- 注1:内置页面
| URL 路径 | 视图类 | 用途 | URL 名称 |
|---|---|---|---|
/accounts/login/ |
LoginView |
用户登录页面 | login |
/accounts/logout/ |
LogoutView |
用户登出 | logout |
/accounts/password_change/ |
PasswordChangeView |
修改密码(需登录) | password_change |
/accounts/password_change/done/ |
PasswordChangeDoneView |
密码修改成功提示 | password_change_done |
/accounts/password_reset/ |
PasswordResetView |
忘记密码/重置密码申请 | password_reset |
/accounts/password_reset/done/ |
PasswordResetDoneView |
重置邮件发送成功提示 | password_reset_done |
/accounts/reset/<uidb64>/<token>/ |
PasswordResetConfirmView |
通过邮件链接设置新密码 | password_reset_confirm |
/accounts/reset/done/ |
PasswordResetCompleteView |
密码重置完成提示 | password_reset_complete |
- 注2:待开发者实现的页面
- Django 核心团队认为
注册流程因项目需求差异很大(如是否需要邮箱验证、审批流程、条款同意等),因此将注册留给开发者自行实现。 - 同理,用户信息修改页面也需要自定义。
- Django 核心团队认为
- 注3:
- SignUpView、UserUpdateView 是待实现的视图
- SignUpView:注册页的视图
- UserUpdateView:用户信息修改页的视图
- SignUpView、UserUpdateView 是待实现的视图
2.3 视图
- accounts/views.py
- SignUpView:注册页的视图
- UserUpdateView:用户信息修改页的视图
python
# accounts/views.py
from django.urls import reverse_lazy
from django.views.generic import CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import CustomUser
from .forms import CustomUserCreationForm, CustomUserChangeForm
class SignUpView(CreateView):
form_class = CustomUserCreationForm
success_url = reverse_lazy("login")
template_name = "registration/signup.html"
class UserUpdateView(LoginRequiredMixin, UpdateView):
model = CustomUser
form_class = CustomUserChangeForm
template_name = "registration/user_update.html"
success_url = reverse_lazy("user_update")
def get_object(self, queryset=None):
return self.request.user
- 注1
- success_url 是成功后跳转到哪个页面
- reverse_lazy 是延后获取"login"页面的 url
- 类成员不能用 reverse,而是用 reverse_lazy
- reverse_lazy 是延后获取"login"页面的 url
- signup.html 是待实现的 html 页面
- success_url 是成功后跳转到哪个页面
- 注2:关于UserUpdateView
- LoginRequiredMixin:登录校验
- 作用:强制要求用户必须登录后,才能访问这个视图对应的页面。
- 如果用户没登录,会自动跳转到 Django 配置的登录页面(默认是 /accounts/login/);
- 如果用户已登录,才允许进入修改资料页面。
- 作用:强制要求用户必须登录后,才能访问这个视图对应的页面。
- get_object
- 作用是指定要修改的「具体数据行」
- 默认行为:从 URL 中提取主键(比如 pk 或 id),然后查询数据库
- 场景是「用户修改自己的资料」,不是「管理员修改他人资料」,不需要从 URL 传 pk
- 如果不重写,有人可以通过 URL 拼接其他用户的 pk(比如 /user/update/2/),修改别人的资料;
- 重写后
- 无论 URL 是什么,都只能修改 self.request.user(当前登录用户),彻底杜绝越权修改。
- LoginRequiredMixin:登录校验
- 注3
- CustomUser 是待实现的用户模型
- CustomUserCreationForm、CustomUserChangeForm 是待实现的表单
- signup.html、user_update.html 是待实现的 html 模版
2.4 模型
2.4.1 用户模型 & 表单
2.4.1.1 用户模型(新增字段)
- accounts/models.py
- age 是我们新增的字段
python
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
age = models.PositiveIntegerField(null=True, blank=True)
- 注1:
| 参数 | 含义 |
|---|---|
null=True |
数据库层面:该字段在数据库中可以为 NULL |
blank=True |
表单验证层面:用户提交表单时可以不填 |
- 注2:用户表默认带有的字段如下
| 字段名 | 类型 | 说明 | 必填 |
|---|---|---|---|
id |
AutoField |
主键,自动创建 | 是 |
password |
CharField(128) |
加密后的密码(哈希值) | 是 |
last_login |
DateTimeField |
上次登录时间 | 否(可为空) |
is_superuser |
BooleanField |
超级管理员(所有权限) | 是(默认False) |
username |
CharField(150) |
用户名(唯一标识) | 是 |
first_name |
CharField(150) |
名 | 否(可为空) |
last_name |
CharField(150) |
姓 | 否(可为空) |
email |
EmailField(254) |
邮箱地址 | 否(可为空) |
is_staff |
BooleanField |
可登录后台管理 | 是(默认False) |
is_active |
BooleanField |
账号是否激活 | 是(默认True) |
date_joined |
DateTimeField |
账号创建时间 | 是(默认当前时间) |
2.4.1.2 用户表单(新建、修改)
- accounts/forms.py
- 用于自定义的表单(注册、修改)
python
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):
password = None
class Meta:
model = CustomUser
fields = (
"username",
"email",
"age",
)
- 注:
- 上面表单中的 age 是我们新增的字段
- 并且我们希望可以在表单里修改
- 有两张表单
- 一个用于新建用户(即:注册)
- 一个用于修改用户信息
- 关于 新建用户信息的表单
- 默认会带有 password 需要填写
- fields 里面填写除了password 外需要填写的内容
- 或者用 exclude = ("xxx",) 来指定不填写的字段
- 关于 修改用户信息的表单
- 由于修改 password 是"高危"操作
- 因此常用的做法是单独创建修改密码的页面
- 这样我们修改用户信息的表单
- 仅负责一些"基础信息"
- 而不处理修改密码
- 需要覆盖类成员才能使得表单不包含密码修改
- 正确方法:password = None
- 错误方法:在 Meta 中使用 exclude = ("password",) 是无效的
- 原因:UserChangeForm 是 Django 内置的表单类,它主动添加了 password 字段(用于显示密码提示文本),并且这个字段的优先级高于 Meta 中的 exclude 配置。所以即使你写了 exclude = ("password",),这个字段依然会被渲染出来。
- 由于修改 password 是"高危"操作
- 上面表单中的 age 是我们新增的字段
2.4.2 关联模型
-
我们当前的项目是"博客网站"
-
其中在 blog APP 中的 Post 模型
- 有 作者 这个字段
- 这个字段是外键,对应用户表的用户名
-
我们需要告诉 作者字段 使用自定义的用户模型
-
learn_django/settings.py
python
AUTH_USER_MODEL = "accounts.CustomUser"
- blog/models.py
python
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # <===
on_delete=models.CASCADE,
related_name='posts',
verbose_name="作者"
)
2.4.3 管理员
- accounts/admin.py
- 管理员页面模型注册
python
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = [
"email",
"username",
"age",
"is_staff",
]
fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("age",)}),)
add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("age",)}),)
admin.site.register(CustomUser, CustomUserAdmin)
- 由于使用了自定义表单,因此在管理员页面需要定义 CustomUserAdmin
- 用来指定 CustomUser 对应的表单(add_form、form)
- 并且指定了需要显示的属性(list_display)
- 以及在管理员页面中,进入具体用户页面时,可以操作的字段(fieldsets、add_fieldsets)
2.4.4 更新数据库
bash
# 刷新数据库
python manage.py makemigrations
python manage.py migrate
# 创建管理员
python manage.py createsuperuser
- 注
- 由于我们修改了不止一个 APP 的模型
- 因此 makemigrations 不指定 APP 了,直接对所有 APP 生效
2.5 模版
2.5.1 公用页眉:登入|注册 or 登出
- templates/base.html
- 用 {% if user.is_authenticated %} 来应对不同的登录状态
- 登录情况下支持的操作包括
- 登出
- 新建博客、编辑博客、删除博客
- 修改用户信息
- 浏览
- 未登录情况下支持的操作包括
- 登录
- 仅浏览
- 登录情况下支持的操作包括
- 值得注意的是,在较高版本的 Django 中,登出需要采用 POST 请求
- Django 官方为了防止 CSRF 攻击,废弃了 GET 方式的 logout。
- 此时使用 GET 方式的 logout,页面会报 405。
- 相关模版内容如下:
- 用 {% if user.is_authenticated %} 来应对不同的登录状态
html
<!-- templates/base.html -->
{% load static %}
<html>
<head>
<title>Django blog</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400"
rel="stylesheet">
<link href="{% static 'css/base.css' %}" rel="stylesheet">
</head>
<body>
<div>
<header>
<div class="nav-left">
<h1><a href="{% url 'home' %}">Django blog</a></h1>
</div>
{% if user.is_authenticated %}
<div class="nav-right">
<a href="{% url 'post_new' %}">+ New Blog Post</a> |
<a href="{% url 'user_update' %}">Edit Profile</a>
</div>
{% endif %}
</header>
{% 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 %}
{% block content %}
{% endblock content %}
</div>
</body>
</html>
- learn_django/settings.py
- 这里需要设置登录、登出成功后,跳转到哪个页面
python
# login
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
2.5.2 注册页
- registration/signup.html
- 用于注册
html
<!-- templates/registration/signup.html -->
{% extends "base.html" %}
{% block content %}
<h2>Sign Up</h2>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<button type="submit">Sign Up</button>
</form>
{% endblock content %}
2.5.3 登录页
- templates/registration/login.html
- 用于登录
html
<!-- templates/registration/login.html -->
{% extends "base.html" %}
{% block content %}
<h2>Log In</h2>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<button type="submit">Log In</button>
</form>
{% endblock content %}
2.5.4 用户信息修改页
- templates/registration/user_update.html
- 用于修改用户信息
- 提供了一个修改密码的连接
html
{% extends "base.html" %}
{% block title %}Edit Profile{% endblock title %}
{% block content %}
<h1>Edit Profile</h1>
<form method="POST">{% csrf_token %}
{{ form.as_p }}
<button type="submit">Update</button>
</form>
<a href="{% url 'password_change' %}">Change Password</a>
{% endblock content %}
2.5.5 密码修改页
- templates/registration/password_change_form.html
- 密码修改(表单)页
html
<!-- templates/registration/password_change_form.html -->
{% extends "base.html" %}
{% block title %}Password Change{% endblock title %}
{% block content %}
<h1>Password change</h1>
<p>Please enter your old password, for security's sake, and then enter
your new password twice so we can verify you typed it in correctly.</p>
<form method="POST">{% csrf_token %}
{{ form.as_p }}
<input class="btn btn-success" type="submit" value="Change my password">
</form>
<a href="{% url 'password_reset' %}">Forgot Your Password?</a>
<br><br>
<a href="{% url 'user_update' %}">Back to Edit Profile</a>
{% endblock content %}
- 注:
- 这里还关联了一个 password_reset 页面
- 用于在忘记密码时发送邮件来重置密码
- 这里还关联了一个 password_reset 页面
- templates/registration/password_change_done.html
- 密码修改成功提示页
html
<!-- templates/registration/password_change_done.html -->
{% extends "base.html" %}
{% block title %}Password Change Successful{% endblock title %}
{% block content %}
<h1>Password change successful</h1>
<p>Your password was changed.</p>
<a href="{% url 'user_update' %}">Back to Edit Profile</a>
{% endblock content %}
2.5.6 密码重置
- settings.py
- 首先我们需要新增一些配置来启用"邮件"功能
python
# email
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
- 页面1:templates/registration/password_reset_form.html
- 重置密码时,发送邮件页
html
<!-- templates/registration/password_reset_form.html -->
{% extends "base.html" %}
{% block title %}Change Your Password{% endblock title %}
{% block content %}
<h1>Change Your Password</h1>
<p>Enter your email address below, and we'll email instructions
for setting a new one.</p>
<form method="POST">{% csrf_token %}
{{ form.as_p }}
<input class="btn btn-success" type="submit" value="Send me instructions!">
</form>
<a href="{% url 'password_change' %}">Back to Change Password</a>
{% endblock content %}
- 页面2:templates/registration/password_reset_done.html
- 当在前一个页面进行发送邮件后,跳转到这个页面
html
<!-- templates/registration/password_reset_done.html -->
{% extends "base.html" %}
{% block title %}Email Sent{% endblock title %}
{% block content %}
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password.
You should receive the email shortly!</p>
{% endblock content %}
-
注:
-
目前发送邮件仅会在后台显示,而非真正发送邮件
-
后台内容类似于
textContent-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: Password reset on 127.0.0.1:8000 From: webmaster@localhost To: xxx@xx.com Date: Mon, 09 Mar 2026 10:41:30 -0000 Message-ID: <177305289005.16399.2746470069724596085@zkding-X570S-AORUS-MASTER> You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8000. Please go to the following page and choose a new password: http://127.0.0.1:8000/accounts/reset/Mg/xxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/ In case you've forgotten, you are: XXX Thanks for using our site! The 127.0.0.1:8000 team -
上面有一个链接(http://127.0.0.1:8000/accounts/reset/Mg/xxx),浏览该链接就进入了重置密码的页面
-
-
页面3:templates/registration/password_reset_confirm.html
- 点击"邮箱"里的链接后,跳转到这个页面,用于设置新密码
html
<!-- templates/registration/password_reset_confirm.html -->
{% extends "base.html" %}
{% block title %}Enter new password{% endblock title %}
{% block content %}
<h1>Set a new password!</h1>
<form method="POST">{% csrf_token %}
{{ form.as_p }}
<input class="btn btn-success" type="submit" value="Change my password">
</form>
{% endblock content %}
- 页面4:templates/registration/password_reset_complete.html
- 设置新密码完成后,跳转到这个页面
- 放置了一个"登录"页的链接
- 设置新密码完成后,跳转到这个页面
html
<!-- templates/registration/password_reset_complete.html -->
{% extends "base.html" %}
{% block title %}Password reset complete{% endblock title %}
{% block content %}
<h1>Reset Your Password</h1>
<p>Your new password has been set.</p>
<p>You can log in now on the
<a href="{% url 'login' %}">Log In page</a>.</p>
{% endblock content %}
2.5.x 其他相关页面
- templates/post_detail.html
- 在浏览具体的博客页时
- 需要判断该博客是否属于
当前用户 - 如果是的话,提供"编辑"、"删除"博客的功能
- 需要判断该博客是否属于
- 相关模版内容如下:
- 在浏览具体的博客页时
html
<!-- 只有作者才能看到编辑和删除链接 -->
{% if user.is_authenticated and user == post.author %}
<a href="{% url 'post_edit' post.pk %}">+ Edit Blog Post</a>
<p><a href="{% url 'post_delete' post.pk %}">+ Delete Blog Post</a></p>
{% endif %}
3. 总结
- 本文基本完成了"用户账户"相关的功能:
- 注册
- 登录
- 登出
- 修改用户信息
- 修改密码
- 忘记密码时通过邮件重置
- 修改密码
- 还缺失的功能
- 真正发送邮件到邮箱
- 目前只是在后台可以进行验证,还不是真正发邮件
- 用户注册
- 也应该用邮箱进行验证
- 或者采用其他有"唯一"性的方式
- 防止被恶意注册大量账号
- 真正发送邮件到邮箱