注册页只加一个「确认密码」,端点里一行都不用动。 模板改成遍历字段渲染,之后加字段页面自动跟着长。 少带 CSRF 令牌时,页面一条错误都不显示,数据也没写进去——这是最难查的一种。
这一篇对应教材 6.1 与 6.3:修改注册页面的端点、修改注册页面的模板。 第 3 章的注册功能只是「能用」,这一篇把它改成「像样」。
一、加一个字段,端点要不要改
confirm = PasswordField("确认密码",
validators=[DataRequired(),
EqualTo("password", "两次输入不一致")])
EqualTo 的第一个参数是「跟哪个字段比」,第二个是比不过时的提示。 端点里一行都没加。校验规则写进表单类,端点只看 validate_on_submit 的结论。 这是 WTForms 最省事的地方:加字段、加规则,端点不用动。 要是自己写 if 判断,每加一条规则就要改一次端点。
教材 6.1 节改端点时动的其实是模板那一侧——因为原来写死了几个字段,多一个就要多写一段。
二、模板改成遍历字段
<form method="post">
{{ form.hidden_tag() }}
{% for f in form if f.type != 'CSRFTokenField' %}
<p>{{ f.label }} {{ f() }}
{% for e in f.errors %}<b>{{ e }}</b>{% endfor %}</p>
{% endfor %}
</form>
三行替代了原来一长串手写的字段段落。 实测注册页渲染出 6 个 input:csrf_token、name、email、password、confirm、submit。 登录页同样一张模板,渲出 4 个。 所以这两页可以共用一张模板,差别只在传进去的 title。这也是「加字段不用改模板」的原因。
那个 if 是为了跳过 CSRF 字段——它由 hidden_tag 单独渲染,再在循环里渲一次就重复了。
三、错误显示在字段后面
实测空表单提交,页面上出现四条 This field is required.,每个字段后面一条。 两次密码填不一样时,出现的是「两次输入不一致」——这句是中文,因为它来自 EqualTo 的第二个参数。 其余那些英文都是 WTForms 内置的。想统一成中文,得给每条内置提示配翻译,或者自己写 messages。
教材没展开这一点,但实际项目第一天就会碰到。
四、少带令牌会怎样
实测不带 csrf_token 直接 POST,返回码是 200,不是 400。 但库里行数没变——0 还是 0。请求被拒了,只是拒得不声不响。 原因是那个错误挂在 csrf_token 这个字段上,而模板只渲染有名字的字段,谁也没把这条错误显示出来。
要让缺令牌直接变成 400,得装 CSRFProtect(app)。实测装上之后同一个请求返回 400。
差别在拦的位置:CSRFProtect 在进入视图之前就拦了;
FlaskForm 的字段级校验要等视图跑起来、表单开始校验。
实际项目里装的人更多,因为「静默失败」比「明着报错」难查得多。
五、测试客户端怎么过 CSRF
html = client.get("/auth/register").get_data(as_text=True)
tok = re.search(r'name="csrf_token"[^>]*value="([^"]+)"', html)
client.post("/auth/register",
data={"csrf_token": tok.group(1), "name": "李四"})
浏览器不会做这件事——它是自动的:hidden_tag 渲出一个隐藏 input,提交时跟着表单一起发走。 测试客户端不解析 HTML,所以得自己从页面里把令牌抠出来,再当参数传回去。 实测带上令牌、两次密码一致,返回 302,库里多一行。
六、动手 Demo:注册页改版:多一个确认密码
这个 Demo 在注册页上加一个确认密码字段,然后把它会走的每条分支都试一遍。 第五节是个陷阱:不带令牌时返回 200,看起来一切正常,其实什么也没写进去。
class RegisterForm(FlaskForm):
name = StringField("姓名", validators=[DataRequired(), Length(1, 20)])
email = StringField("邮箱", validators=[DataRequired(), Email()])
password = PasswordField("密码", validators=[DataRequired(),
Length(8, 60)])
confirm = PasswordField("确认密码",
validators=[DataRequired(),
EqualTo("password",
"两次输入不一致")])
submit = SubmitField("注册")
实跑输出:
--------------------------------------------------------------------
注册页改版:多一个确认密码
--------------------------------------------------------------------
① 端点只多了一件事
--------------------------------------------------------------------
[代码原样]
| confirm = PasswordField("确认密码",
| validators=[DataRequired(),
| EqualTo("password", "两次输入不一致")])
EqualTo 的第一个参数是「跟哪个字段比」,第二个是比不过时的提示。
端点里一行都没加。校验规则写进表单类,端点只看 validate_on_submit 的结论。
! 这是 WTForms 最省事的地方:加一个字段、加一条规则,端点不用动。
要是自己写 if 判断,每加一条规则都要改一次端点。
② 模板把四个字段各渲一遍
--------------------------------------------------------------------
input name=csrf_token type=hidden
input name=name type=text
input name=email type=text
input name=password type=password
input name=confirm type=password
input name=submit type=submit
! 那段 for 循环遍历 form,跳过 CSRF 字段,其余各渲一个 label 加一个 input。
字段加在表单类里,页面自动跟着多一行——模板不用改。
③ 空提交:错误显示在字段后面
--------------------------------------------------------------------
POST 空表单 -> 200
页面上的错误 -> ['This field is required.', 'This field is required.', 'This field is required.', 'This field is required.']
④ 两次密码不一致
--------------------------------------------------------------------
两次密码不一样 -> 200
页面上的错误 -> ['两次输入不一致']
库里行数 -> 0
! 这条提示是自己写的那句中文字,因为它来自 EqualTo 的第二个参数。
其余那些英文是 WTForms 内置的——想统一成中文,得给每条内置提示配翻译。
⑤ CSRF:不先取令牌会怎样
--------------------------------------------------------------------
不带 csrf_token 直接 POST -> 200(不是 400)
库里行数 0 -> 0,说明确实没写进去
! 页面上一条错误都没有,但数据也没写进去。
这就是最难查的一种:那个错误挂在 csrf_token 字段上,而模板只渲染有名字的字段,谁也没显示它。
同一个请求,装上 CSRFProtect 之后 -> 400
! 差别在拦的位置:CSRFProtect 在进入视图之前就把请求拦了,所以是 400;
FlaskForm 的字段级校验要等视图跑起来、表单开始校验,所以只是一个校验错误。
! 教材没有装它,只用 FlaskForm 自带的那一层。
两种都能防住,实际项目里装的人更多——因为「静默失败」比「明着报错」难查得多。
! 浏览器里这事是自动做的:{{ form.hidden_tag() }} 渲染出一个隐藏 input,名字叫 csrf_token,
提交时跟着表单一起发走。测试客户端不会自动填,得自己从页面里抠。
⑥ 带上令牌就通了
--------------------------------------------------------------------
带令牌且两次密码一致 -> 302,Location=/
库里行数 -> 1
第二节那六个 input 里,csrf_token 是 hidden,其余按表单类里的顺序排。 第四节那条中文提示是唯一一条中文——它是我们自己写在 EqualTo 里的。 第五节那两个 0,和「返回码 200」放在一起看,才是完整的事实。
30 秒实验
- 把 EqualTo 的第一个参数改成 "email",提交一次,看会发生什么。
- 把模板里遍历字段的那三行删掉,改回手写四个字段,再给表单类加一个字段,看页面变不变。
- 想一想:为什么 CSRF 的错误不显示在页面上?
常见误区
- 以为返回 200 就是成功:CSRF 失败也是 200,数据一个字没进库。
- 模板里手写每个字段:加字段要改三处(表单类、端点、模板)。
- 把 CSRF 字段也放进循环:页面上会出现两个同名隐藏域。
- 只靠前端校验两次密码:绕过前端就是空的确认密码也能提交。
自测三问(点开对答案)
1. 加一个字段,端点要改吗? —— 不用,规则写在表单类里,端点只看 validate_on_submit 的结论。 2. 为什么模板可以遍历字段渲染? —— 表单类里每个字段都是可迭代对象的一部分,模板按顺序渲就行。 3. 少带 CSRF 令牌时返回什么? —— 只用 FlaskForm 时是 200 加一条看不见的错误;装了 CSRFProtect 才是 400。注册页改完了,下一篇把标头抽出来,顺手把登录页也对齐。
- ☐ (本篇完,下一篇 6.2 通用标头与登录页)