Data verification: Build a secure line of defense for Flask user interaction
📂 Stage: Stage 2 - Interaction and Data (Core)
🔗 Related chapters: Flask-WTF 插件 · 密码安全加密
The data submitted by users is the first-hand material received by web applications from the outside world. Null pointer crash, XSS injection, SQL injection vulnerability, business logic error are often due to the lack of strict data verification. Instead of checking later, it is better to establish the rules at the entrance.
Today we will use the Flask-WTF extension to see how to quickly build general rules through built-in validators, then cover business-specific needs through custom validators, and finally use HTML5 native and AJAX to make the verification experience more friendly.
The validation logic of WTForms is written in each fieldvalidatorsIn the list, execute in order. Once a validator throwsValidationError, the subsequent validator will no longer run, and the error message will be added to the corresponding field.errorsOn properties.
1.1 High-frequency built-in validator cheat sheet
2. Commonly used built-in validator combinations in practice
2.1 Basic form (general content management)
Below is an example of an article submission form that covers a combination of validations for text, URL alias, age, and blog link.
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField, TextAreaField
from wtforms.validators import (
DataRequired, Email, Length, EqualTo,
Regexp, URL, NumberRange, Optional
)
class ArticleSubmissionForm(FlaskForm):
# 标题:必填、5-100 字符、仅允许中文/英文/数字/下划线/空格
title = StringField(
"投稿标题",
validators=[
DataRequired(message="标题可不能空着哦😯"),
Length(
min=5, max=100,
message="标题太长太短都不行,请控制在 5-100 个字符之间"
),
Regexp(
r"^[\u4e00-\u9fa5a-zA-Z0-9_ ]+$",
message="标题只能包含中文、英文字母、数字、下划线和空格"
),
],
)
# URL 别名:必填、5-50 字符、仅允许小写字母/数字/连字符(SEO 友好)
url_slug = StringField(
"URL 别名(用于生成链接)",
validators=[
DataRequired(),
Length(max=50, message="URL 别名太长啦,不利于分享"),
Regexp(
r"^[a-z0-9-]+$",
message="只能用小写字母、数字和连字符哦"
),
],
)
# 作者年龄:可选、18-100 岁
author_age = IntegerField(
"您的年龄(选填)",
validators=[
Optional(),
NumberRange(
min=18, max=100,
message="如果填写年龄,请确保是 18-100 岁之间的有效数字"
),
],
)
# 个人博客:可选、仅允许 http/https 开头
personal_blog = StringField(
"个人博客链接(选填)",
validators=[
Optional(),
URL(message="请输入有效的博客链接,记得加上 http:// 或 https:// 前缀"),
],
)
For rules that the built-in validator cannot handle, just useRegexpCome on:
# 中国手机号验证
phone = StringField(
"手机号",
validators=[
Regexp(
r"^1[3-9]\d{9}$",
message="请输入 11 位有效的中国手机号"
),
],
)
# 合规用户名(字母开头、字母数字下划线、3-20 位)
username = StringField(
"用户名",
validators=[
Regexp(
r"^[a-zA-Z]\w{2,19}$",
message="用户名需以字母开头,3-20 个字符,仅允许字母、数字和下划线"
),
],
)
3. Custom validator: covering business-specific requirements
When the built-in validators and regular expressions are not enough (for example, if you need to check the database or check sensitive words), it is time to use a custom validator.
It is suitable to use the logic of **only for the current form. The method name must comply withvalidate_字段名Naming rules:
# 假设已定义 User 模型和 db 实例
# from app.models import User
# from app import db
class SignUpForm(FlaskForm):
username = StringField("用户名", validators=[
DataRequired(), Length(3, 20),
])
bio = TextAreaField("个人简介", validators=[
Length(max=200, message="简介最多 200 个字符"),
])
# 同时检查敏感词和唯一性
def validate_username(self, field):
sensitive_words = ["admin", "root", "system", "test"]
if any(word in field.data.lower() for word in sensitive_words):
raise ValidationError("该用户名太敏感啦,换一个试试😉")
if User.query.filter_by(username=field.data).first():
raise ValidationError("这个用户名已经被别人注册了哦")
# 简介里禁止出现外部链接
def validate_bio(self, field):
if field.data and ("http://" in field.data.lower() or "https://" in field.data.lower()):
raise ValidationError("为了社区安全,简介中暂时禁止添加链接")
3.2 Independent custom validator (universal reuse)
Suitable for ** multiple forms that will use ** logic, put it separatelyapp/validators.pyinside:
# app/validators.py
import re
from wtforms.validators import ValidationError
# 通用敏感词检查
def no_common_sensitive_words(form, field):
"""检查文本是否包含通用敏感词(示例列表)"""
sensitive_words = ["诈骗", "赌博", "色情"]
if any(word in field.data for word in sensitive_words):
raise ValidationError("文本中包含违规内容,请修改后再提交")
# 通用外部链接禁止
def no_external_links(form, field):
"""检查文本是否包含以 http/https 开头的外部链接"""
url_pattern = r"https?://[^\s]+"
if re.search(url_pattern, field.data, re.IGNORECASE):
raise ValidationError("文本中暂时禁止添加外部链接")
Then import it directly into any form:
from app.validators import no_common_sensitive_words, no_external_links
class CommentForm(FlaskForm):
content = TextAreaField("评论内容", validators=[
DataRequired(), Length(1, 500),
no_common_sensitive_words, no_external_links,
])
4. Optimize verification experience: client-assisted verification
⚠️ Core Security Principles: Client-side verification is just a "dessert" to improve the experience. What truly protects data security is server-side verification (WTForms). Attackers can easily disable JavaScript or send forged requests, so backend defense is a must.
4.1 HTML5 native verification (zero code based experience)
You can use fields generated by WTForms or manually add HTML5 attributes in Jinja2 templates:
<form method="POST" action="{{ url_for('auth.signup') }}">
{{ form.hidden_tag() }} <!-- Flask-WTF 必须:CSRF 保护 -->
<div>
{{ form.email.label }}
<!-- 手动添加 HTML5 原生属性 -->
<input
type="email"
name="{{ form.email.name }}"
id="{{ form.email.id }}"
required
minlength="5"
maxlength="100"
placeholder="your@email.com"
{% if form.email.data %}value="{{ form.email.data }}"{% endif %}
>
{% if form.email.errors %}
<span style="color: red;">{{ form.email.errors[0] }}</span>
{% endif %}
</div>
<button type="submit">注册</button>
</form>
4.2 AJAX real-time verification (advanced experience)
Allow users to see feedback during the input process, reducing the frustration of reporting errors after submission.
Backend API routing:
# app/routes/api.py
from flask import Blueprint, jsonify
from app.models import User
api_bp = Blueprint("api", __name__, url_prefix="/api")
@api_bp.route("/check/username/<username>")
def check_username(username):
exists = User.query.filter_by(username=username).first() is not None
return jsonify({"available": not exists, "username": username})
@api_bp.route("/check/email/<email>")
def check_email(email):
import re
if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
return jsonify({"valid": False, "available": False})
exists = User.query.filter_by(email=email).first() is not None
return jsonify({"valid": True, "available": not exists})
Front-end JavaScript (real-time verification of username):
document.getElementById("username").addEventListener("blur", async function() {
const username = this.value.trim();
if (!username) return; // 为空时不发送请求
const response = await fetch(`/api/check/username/${username}`);
const data = await response.json();
const errorEl = document.getElementById("username-error");
if (!data.available) {
errorEl.textContent = "这个用户名已经被注册啦";
errorEl.style.color = "red";
errorEl.style.display = "block";
} else {
errorEl.textContent = "这个用户名可以用哦🎉";
errorEl.style.color = "green";
errorEl.style.display = "block";
}
});
5. Full text summary
- Verifier running rules:
- Execute in list order and stop if any verification fails.
Optional()It should be placed at the front of the validator list, otherwise a null value will also trigger subsequent validation.
- use
EmailConfirm before validatoremail-validatorThe library is installed.
- Combined routines for typical scenarios:
- Text class:
DataRequired + Length+ optionalRegexp。
- Number Class:
DataRequired / Optional + NumberRange。
- Confirmation Class:
EqualToMatch the corresponding field's own rules.
- URL/Email Class:
Optional / DataRequired + URL / Email。
- Safety first principle:
- Server-side validation (WTForms) is the bottom line, must be written and cannot be omitted.
- Client-side validation (HTML5/AJAX) is just the icing on the cake and in no way a replacement for back-end validation.
- For sensitive operations such as registration, login, and payment, database uniqueness checks and additional business rules must be added.
🔗 Extended reading