前言
之前 初探 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 屬性來實現。