注册端点就三件事:查邮箱重不重、把密码哈希、写库并建立会话。 库里那一列叫 password_hash,只有哈希没有密码,丢了也找不回来。 给 CRUD 页面挂上 login_required,整套后台才算真正关上。
这一篇对应教材 3.3.4 到 3.3.7:注册的端点、注册的模板、改成必须登入、确认运作。 上一篇把工具装好了,这一篇用它写出第一个完整功能。
一、注册表单多一个密码字段
class RegisterForm(FlaskForm):
name = StringField("姓名", validators=[DataRequired(), Length(1, 20)])
email = StringField("邮箱", validators=[DataRequired(), Email()])
password = PasswordField("密码",
validators=[DataRequired(), Length(8, 60)])
submit = SubmitField("注册")
PasswordField 和 StringField 的差别只在渲染出来的 input type:一个 password、一个 text。 别指望它保护什么。type=password 只是不显示字符,表单内容在传输时一样是明文, 真正的保护是 HTTPS。 长度限制写在校验器里,不写在前端的 maxlength。前端那一个随手就能绕过。
二、端点三件事
if form.validate_on_submit():
if _find(form.email.data):
flash("这个邮箱已经注册过了", "warning")
return render_template("auth/register.html", form=form)
u = User(name=form.name.data, email=form.email.data)
u.set_password(form.password.data)
db.session.add(u)
db.session.commit()
login_user(u)
return redirect(url_for("main.index"))
顺序是有讲究的:先查重、再建对象、再哈希、再写库、最后建立会话。
set_password 那一行是关键。
少了它,password_hash 是空字符串,而表上那一列是 nullable=False,插入直接报错。
报出来的错是 NOT NULL constraint failed,不会提醒你「忘了哈希」。
三、库里存的不是密码
实测注册成功后那一行:password_hash 以 scrypt:32768:8:1 开头,表里的列是 id、name、email、 password_hash——没有 password。 明文密码在哈希里搜不到,因为哈希是不可逆的运算。 这个设计的直接后果:密码丢了谁也找不回来,只能重置。所以注册页最好写一句「密码丢了只能重置」, 省掉后面的扯皮。
四、重复注册与短密码
实测同一个邮箱再注册一次,返回码 200(不是 302),页面上出现「这个邮箱已经注册过了」, 库里行数还是 1。 返回 200 意味着没跳转——留在原页面让用户改,这是对的。 密码填 abc,页面上出现的是 Field must be between 8 and 60 characters long.
这些英文提示是 WTForms 自带的。要中文得配 locale 或自己写 messages,教材没展开, 实际项目第一天就要处理。
五、给 CRUD 挂上登录门
教材 3.3.6 把 main 蓝图的两个页面都加上 @login_required。
加完之后,未登入访问 /users 会 302 到 /auth/login?next=%2Fusers。
注意 next 里带的是原始地址,所以登入后会回到 /users,
而不是回到首页——这一条是上一篇讲的那个机制在起作用。
首页也挂上之后,未登入的人打开网站第一眼看到的就是登入页。 要不要这样取决于产品,但功能站一般都这么干。
六、注册完为什么不让他再登一次
端点最后那句 login_user(u) 直接把会话建起来,用户注册完就是登录状态。
实测注册之后紧接着 GET /users 返回 200,说明真的进得去。
把那一行删掉,同一个请求就变成 302——这一行就是「自动登入」的全部代码。
有些产品故意不自动登入,让用户走一遍登入流程,顺便确认密码记住了。 两种都行,取决于你想让用户少点几下还是多点几下。
七、动手 Demo:注册功能:查重、哈希、自动登入
这个 Demo 走完注册的每条分支:成功、重复邮箱、密码太短,每一支都看返回码和库里的行数。 最后一节验证「注册完就已经是登录状态」这件事。
@bp.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
if _find(form.email.data):
flash("这个邮箱已经注册过了", "warning")
return render_template("auth/register.html", form=form)
u = User(name=form.name.data, email=form.email.data)
u.set_password(form.password.data)
db.session.add(u)
db.session.commit()
login_user(u)
return redirect(url_for("main.index"))
return render_template("auth/register.html", form=form)
实跑输出:
--------------------------------------------------------------------
注册功能:查重、哈希、自动登入
--------------------------------------------------------------------
① 注册成功:302 而不是 200
--------------------------------------------------------------------
POST /auth/register -> 302,Location=/
库里行数 -> 1
! 和新增用户一样走重定向。注册成功之后浏览器跟过去的是 GET /,刷新一百次也不会多出第二个账号。
② 库里存的是哈希,不是密码
--------------------------------------------------------------------
password_hash 前 34 个字符 -> scrypt:32768:8:1$neNCbFoN2Rr2CMvl$
明文密码在哈希里吗 -> False
user 表的列 -> ['id', 'name', 'email', 'password_hash']
! 表里没有 password 这一列,只有 password_hash。这个设计意味着:密码丢了谁也找不回来,只能重置。
③ 同一个邮箱再注册一次
--------------------------------------------------------------------
再注册 -> 200,页面里有「已经注册过了」吗 -> True
库里行数 -> 1(没变)
! 查重是「先查一次再插入」,两个人同时提交仍有窗口。
真正保险的是 email 上那个 unique 约束,加 IntegrityError 兜底。本篇两个都做了。
④ 密码太短
--------------------------------------------------------------------
密码填 abc -> 200
页面上的错误 -> ['Field must be between 8 and 60 characters long.']
! Length(8, 60) 在这里才起作用。长度限制必须写在校验器里,不能只靠前端那个 maxlength。
⑤ 注册完就已经是登录状态
--------------------------------------------------------------------
紧接着 GET /users -> 200
(要是没登入,这一行会是 302 跳到 /auth/login)
! 端点里那句 login_user(u) 就是全部:注册完直接建立会话,用户不用再登一次。
把那一行删掉,上面这个 200 立刻变成 302。
第一节那个 302 是注册成功的标志;200 反而说明被拦下来了。 第二节打印了库里的列名——没有 password 这一列,只有 password_hash。 第四节那两句英文提示是 WTForms 给的,不是我们写的。
30 秒实验
- 把
login_user(u)注释掉,再注册一个新账号,看紧接着访问 /users 变成几。 - 把 email 字段的校验器里的 Email() 去掉,注册一个不存在的域名,看能不能过。
- 想一想:为什么查重之后还要在 email 列上加 unique?
常见误区
- 忘了 set_password:password_hash 是空值,插入时撞 NOT NULL 约束。
- 只查重不加唯一索引:两个人同时提交,两个都能过。
- 密码长度只靠前端 maxlength:绕过前端就是空密码也能注册。
- 返回 302 当成失败:注册成功才是 302,失败留在原页是 200。
自测三问(点开对答案)
1. 注册端点里三件事的顺序是什么? —— 先查重,再哈希,再写库并建立会话。 2. 注册成功为什么返回 302? —— 重定向到首页,避免刷新时重复提交表单。 3. 为什么密码列叫 password_hash? —— 存的是不可逆哈希,提醒你这里没有明文可查。注册通了,下一篇写登入:校验、限流思路、记住我与 next 回跳。
- ☐ (本篇完,下一篇 3.4 登录功能:校验与状态保持)