数据验证:确保用户输入的邮箱、密码符合安全规范

📂 所属阶段:第二阶段 — 交互与数据(核心篇)
🔗 相关章节:Flask-WTF 插件 · 密码安全加密


1. WTForms 验证器概述

1.1 验证器一览

验证器说明
DataRequired字段不能为空
Email邮箱格式验证
Length长度限制
Range数字范围限制
EqualTo两字段相等(如确认密码)
Regexp正则表达式
URLURL 格式验证
NumberRange数字范围(配合 IntegerField)
AnyOf枚举值
NoneOf排除值
InputRequired原始数据存在(非空字符串)
Optional允许为空

2. 内置验证器详解

2.1 常用验证组合

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 ArticleForm(FlaskForm):
    title = StringField("标题", validators=[
        DataRequired(message="标题不能为空"),
        Length(min=5, max=100, message="标题长度需在 5-100 个字符之间"),
        # 自定义正则:只允许中文、英文、数字
        Regexp(r"^[\u4e00-\u9fa5a-zA-Z0-9]+$", message="标题只能包含中文、英文字母和数字"),
    ])

    url_slug = StringField("URL别名", validators=[
        DataRequired(),
        Length(max=50),
        # 小写字母、数字、连字符
        Regexp(r"^[a-z0-9-]+$", message="URL别名只能包含小写字母、数字和连字符"),
    ])

    price = IntegerField("价格(元)", validators=[
        DataRequired(message="价格不能为空"),
        NumberRange(min=0, max=999999, message="价格必须在 0-999999 之间"),
    ])

    website = StringField("个人网站", validators=[
        Optional(),
        URL(message="请输入有效的 URL,以 http:// 或 https:// 开头"),
    ])

    content = TextAreaField("正文", validators=[
        DataRequired(message="正文不能为空"),
        Length(min=10, message="正文至少 10 个字符"),
    ])

2.2 正则表达式验证

# 手机号验证(中国)
phone = StringField("手机号", validators=[
    Regexp(
        r"^1[3-9]\d{9}$",  # 13x-9x 开头的 11 位手机号
        message="请输入有效的手机号"
    )
])

# 用户名验证(字母开头,字母数字下划线,3-20位)
username = StringField("用户名", validators=[
    Regexp(
        r"^[a-zA-Z]\w{2,19}$",
        message="用户名需以字母开头,3-20个字符,只能包含字母、数字和下划线"
    )
])

# 身份证号验证
id_card = StringField("身份证号", validators=[
    Regexp(
        r"^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$",
        message="请输入有效的身份证号"
    )
])

3. 自定义验证器

3.1 表单类中的自定义验证

class ProfileForm(FlaskForm):
    username = StringField("用户名", validators=[
        DataRequired(), Length(3, 20)
    ])
    bio = StringField("个人简介", validators=[
        Length(max=200, message="简介最多 200 个字符")
    ])

    # 字段级验证(针对特定字段)
    def validate_username(self, field):
        # 检查用户名是否包含敏感词
        if any(word in field.data for word in ["admin", "root", "system"]):
            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():
            raise ValidationError("简介中禁止包含链接")

3.2 独立的自定义验证器

# app/validators.py
from wtforms.validators import ValidationError

def contains_no_links(form, field):
    """验证字段中不包含链接"""
    import re
    url_pattern = r"https?://[^\s]+"
    if re.search(url_pattern, field.data, re.IGNORECASE):
        raise ValidationError("禁止包含 URL 链接")

def starts_with_uppercase(form, field):
    """验证字段以大写字母开头"""
    if not field.data[0].isupper():
        raise ValidationError("必须以大写字母开头")

# 在表单中使用
class PostForm(FlaskForm):
    title = StringField("标题", validators=[
        DataRequired(), contains_no_links, starts_with_uppercase
    ])

3.3 异步验证(AJAX)

# app/routes/api.py
@api_bp.route("/check/username/<username>")
def check_username(username):
    """检查用户名是否可用(供前端 AJAX 调用)"""
    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})
    exists = User.query.filter_by(email=email).first() is not None
    return jsonify({"valid": True, "available": not exists})

前端实时验证:

// 前端实时验证
document.getElementById("username").addEventListener("blur", async function() {
    const response = await fetch(`/api/check/username/${this.value}`);
    const data = await response.json();
    const errorEl = document.getElementById("username-error");
    if (!data.available) {
        errorEl.textContent = "用户名已被注册";
        errorEl.style.display = "block";
    } else {
        errorEl.style.display = "none";
    }
});

4. 客户端验证 + 服务器端验证

4.1 HTML5 原生验证

<input type="email"
       name="email"
       required
       minlength="5"
       maxlength="100"
       pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
       placeholder="your@email.com">

4.2 完整验证体系

双重验证 = 客户端(用户体验)+ 服务器端(安全保障)

1. 客户端验证:
   → HTML5 原生属性(required, minlength)
   → JavaScript 实时反馈(无需等待服务器)

2. 服务器端验证(必须):
   → WTForms 验证器(防止绕过客户端直接请求)
   → 数据库唯一性检查
   → 业务逻辑验证

5. 小结

# 验证器速查

# 必填
DataRequired(message="...")

# 邮箱
Email(message="...")

# 长度
Length(min=3, max=50, message="...")

# 正则
Regexp(r"^\w+$", message="...")

# 相等
EqualTo("password", message="两次密码不一致")

# 范围
NumberRange(min=0, max=100, message="...")

# URL
URL(message="...")

# 可选(允许空)
Optional()

# 自定义
def validate_field(self, field):
    if bad_condition:
        raise ValidationError("错误信息")

💡 记住:客户端验证只是提升用户体验,服务器端验证才是真正的安全防线。永远不要只依赖客户端验证。


🔗 扩展阅读