初探 Flask-WTF

前言

之前 初探 Flask ,我們學習如何讓 HTML 表單與 Flask 伺服器協同工作,並取得使用者在表單中輸入的資料。今天,我們將在此基礎上進行改進,使用名為 Flask-WTF 的 Flask 擴充功能來建立表單。

Flask-WTF 是 Flask 框架整合 WTForms 的擴充套件。在傳統的 Flask 開發中,處理表單需要手動寫 HTML、在後端用 request.form.get() 一個個撈資料、手動寫樣式進行資料驗證(驗證長度、格式、是否必填等)。Flask-WTF 它比簡單的 HTML 表單有許多優勢。

  • 用類別(Class)來定義表單欄位。你在 Python 裡定義好,前端就能自動渲染出對應的 HTML。
  • 驗證表單 - 確保使用者在所有必填欄位中都以正確的格式輸入資料。例如,檢查使用者輸入的電子郵件地址是否包含"@"符號和末尾的"."。所有這些都無需編寫您自己的驗證程式碼。
  • 內建 CSRF 防護 - CSRF 代表跨站請求偽造,這是一種可以針對網站表單發起的攻擊,它會迫使您的使用者執行非預期操作(例如,向陌生人轉帳),這是原生的 HTML Form 沒有內建的安全機制。

參考 : Flask-WTF --- Flask-WTF Documentation (1.2.x)

WTForms 是一個靈活的 Python Web 開發表單驗證和渲染庫。它可以與任何 Web 框架和模板引擎一起搭配使用。支援資料驗證、CSRF 保護、多語系 (I18N) 等功能。

參考 : WTForms --- WTForms Documentation (3.2.x)

安裝所需套件

pip3 install -U Flask-WTF

使用 Email() 驗證器需要額外依賴 Python 的 email-validator 套件

pip3 install email-validator

一個簡單版的 Flask 專案 範例

Python
python 复制代码
from dbm import error

import requests

from flask import Flask, request, render_template, redirect, url_for, session
from datetime import datetime
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, EmailField
from wtforms.validators import DataRequired, Length, Email

app = Flask(__name__)

# 安全設定:Flask-WTF 產生 CSRF Token 需要一組金鑰
app.config['SECRET_KEY'] = 'your-super-secret-key'

# 定義登入表單類別(Form Object),繼承自 FlaskForm
class LoginForm(FlaskForm):
    username = StringField('帳號:', validators=[
        DataRequired(message="帳號不能為空")
    ])
    password = PasswordField('密碼:', validators=[
        DataRequired(message="密碼不能為空"),
        Length(min=5, message="密碼長度至少需要 5 個字元")
    ])
    submit = SubmitField('登入')


def valid_login(username, password):
    # 這裡模擬資料庫查詢。實務上會從資料庫撈出使用者,並用 werkzeug.security 驗證雜湊密碼
    if username == "admin" and password == "12345":
        return True
    return False


def log_the_user_in(username):
    # 將使用者名稱存入 session 字典中
    session['logged_in_user'] = username
    # 登入成功後,重新導向(Redirect)到首頁或儀表板
    return redirect(url_for('dashboard'))


@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None  
    # 表單物件
    form = LoginForm()
    
    # 用 validate_on_submit() 「判斷 POST」與「欄位合法驗證」
    if form.validate_on_submit():
        # 直接從 form.data 撈取經過驗證的安全資料
        username = form.username.data
        password = form.password.data
        if valid_login(username, password):
            return log_the_user_in(username)
        else:
            error = '使用者名稱/密碼無效'
    # 將 form 物件傳給前端樣板進行渲染
    return render_template('login.html', form=form, error=error)

@app.route('/dashboard')
def dashboard():
    # 檢查使用者是否已經登入
    if 'logged_in_user' in session:
        return f"<h1>歡迎來到後台!</h1><p>目前登入使用者:{session['logged_in_user']}</p>"
    return redirect(url_for('login'))


if __name__ == "__main__":
    app.run(debug=True)
html
html 复制代码
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <title>Flask 登入範例</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
        html, body {
            height: 100%;
        }
        .form-signin {
            max-width: 330px;
            padding: 15px;
        }
    </style>
</head>
<body class="d-flex align-items-center justify-content-center py-4 bg-body-tertiary">
    <main class="form-signin m-auto">
        <form method="post" class="card p-4 shadow-sm"> <h2 class="h3 mb-3 fw-normal text-center">系統登入</h2>       
            {{ form.csrf_token }}
            {% if error %}
                <div class="alert alert-danger py-2" role="alert">
                    {{ error }}
                </div>
            {% endif %}
            <div class="mb-3">
                {{ form.username.label(class="form-label") }}
                {{ form.username(class="form-control", placeholder="請輸入帳號") }}
                {% for err in form.username.errors %}
                    <div class="text-danger small mt-1">{{ err }}</div>
                {% endfor %}
            </div>
            <div class="mb-3">
                {{ form.password.label(class="form-label") }}
                {{ form.password(class="form-control", placeholder="請輸入密碼") }}
                {% for err in form.password.errors %}
                    <div class="text-danger small mt-1">{{ err }}</div>
                {% endfor %}
            </div> 
            <button type="submit" class="btn btn-primary w-100 py-2 mt-2">登入</button>
        </form>
    </main>
</body>
</html>
測試

登入頁面 加上 輸入欄位 ** **電子郵件地址,檢查使用者輸入的電子郵件地址

python

python 复制代码
from dbm import error

import requests

from flask import Flask, request, render_template, redirect, url_for, session
from datetime import datetime
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, EmailField
from wtforms.validators import DataRequired, Length, Email

app = Flask(__name__)

# 安全設定:Flask-WTF 產生 CSRF Token 需要一組金鑰
app.config['SECRET_KEY'] = 'your-super-secret-key'

class LoginForm(FlaskForm):
    username = StringField('帳號:', validators=[
        DataRequired(message="帳號不能為空")
    ])
    
    # 電子郵件欄位與格式檢查
    email = EmailField('電子郵件地址:', validators=[
        DataRequired(message="電子郵件不能為空"),
        Email(message="請輸入正確的電子郵件格式(例如:user@example.com)")
    ])
    
    password = PasswordField('密碼:', validators=[
        DataRequired(message="密碼不能為空"),
        Length(min=5, message="密碼長度至少需要 5 個字元")
    ])
    submit = SubmitField('登入')


def valid_login(username, password):
    # 這裡模擬資料庫查詢。實務上會從資料庫撈出使用者,並用 werkzeug.security 驗證雜湊密碼
    if username == "admin" and password == "12345":
        return True
    return False


def log_the_user_in(username):
    # 將使用者名稱存入 session 字典中
    session['logged_in_user'] = username
    # 登入成功後,重新導向(Redirect)到首頁或儀表板
    return redirect(url_for('dashboard'))


@app.route('/login', methods=['POST', 'GET'])
def login():
    error = None  
    # 表單物件
    form = LoginForm()
    
    # 用 validate_on_submit() 「判斷 POST」與「欄位合法驗證」
    if form.validate_on_submit():
        # 直接從 form.data 撈取經過驗證的安全資料
        username = form.username.data
        password = form.password.data
        if valid_login(username, password):
            return log_the_user_in(username)
        else:
            error = '使用者名稱/密碼無效'
    # 將 form 物件傳給前端樣板進行渲染
    return render_template('login.html', form=form, error=error)

@app.route('/dashboard')
def dashboard():
    # 檢查使用者是否已經登入
    if 'logged_in_user' in session:
        return f"<h1>歡迎來到後台!</h1><p>目前登入使用者:{session['logged_in_user']}</p>"
    return redirect(url_for('login'))


if __name__ == "__main__":
    app.run(debug=True)

html

html 复制代码
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <title>Flask 登入範例</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <style>
        html, body {
            height: 100%;
        }
        .form-signin {
            max-width: 330px;
            padding: 15px;
        }
    </style>
</head>
<body class="d-flex align-items-center justify-content-center py-4 bg-body-tertiary">
    
    <main class="form-signin m-auto">
        <form method="post" class="card p-4 shadow-sm" novalidate> <h2 class="h3 mb-3 fw-normal text-center">系統登入</h2>       
            {{ form.csrf_token }}
            {% if error %}
                <div class="alert alert-danger py-2" role="alert">
                    {{ error }}
                </div>
            {% endif %}
            <div class="mb-3">
                {{ form.username.label(class="form-label") }}
                {{ form.username(class="form-control", placeholder="請輸入帳號") }}
                {% for err in form.username.errors %}
                    <div class="text-danger small mt-1">{{ err }}</div>
                {% endfor %}
            </div>
            <div class="mb-3">
                {{ form.email.label(class="form-label") }}
                {{ form.email(class="form-control", placeholder="name@example.com") }}
                {% for err in form.email.errors %}
                    <div class="text-danger small mt-1">{{ err }}</div>
                {% endfor %}
            </div>
            <div class="mb-3">
                {{ form.password.label(class="form-label") }}
                {{ form.password(class="form-control", placeholder="請輸入密碼") }}
                {% for err in form.password.errors %}
                    <div class="text-danger small mt-1">{{ err }}</div>
                {% endfor %}
            </div> 
            <button type="submit" class="btn btn-primary w-100 py-2 mt-2">登入</button>
        </form>
    </main>

</body>
</html>

測試

測試表單,您會發現驗證提示跟預期有所不同,這種行為並非來自我們的驗證器,實際上,這是瀏覽器內建的驗證機制,並且因瀏覽器而異。不同瀏覽器中,您會看到不同的情況。

為了確保所有使用者都能看到欄位驗證,我們需要關閉瀏覽器的驗證功能,這可以透過表單元素上的 novalidate 屬性來實現。

相关推荐
AskHarries1 小时前
假门测试怎么玩
后端
AskHarries1 小时前
1300万 Tokens 被刷光:一次 OpenAI API Key 泄露事故复盘
后端
装不满的克莱因瓶1 小时前
基于 sklearn 工具和鸢尾花数据集,进行逻辑回归实战
人工智能·python·机器学习·ai·逻辑回归·sklearn
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月5日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
garmin Chen1 小时前
Prompt工程入门:让AI按你的要求工作(2)--Prompt 高阶优化与结构化设计
java·人工智能·python·ai·prompt
AC赳赳老秦1 小时前
用 OpenClaw 整理团队技术分享:自动提取 PPT 内容、生成文字稿、同步到知识库
开发语言·python·自动化·powerpoint·wpf·deepseek·openclaw
编程大师哥1 小时前
推导式和生成器表达式有什么区别?
python
稳如磐石.1 小时前
北京工业计算机
大数据·人工智能·python·物联网