跳到主要內容

初探 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 沒有內建的安全機制。

參考 : https://flask-wtf.readthedocs.io/en/1.2.x/#features


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

參考 : https://wtforms.readthedocs.io/en/3.2.x/



安裝所需套件

pip3 install -U Flask-WTF


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

pip3 install email-validator



一個簡單版的 Flask 專案 範例


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'))



# 啟動 Server

if __name__ == "__main__":

    app.run(debug=True)



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.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` 屬性來實現。




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

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


# 建立 Flask 應用程式

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="帳號不能為空")

    ])

    

    # 電子郵件欄位與格式檢查

    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'))



# 啟動 Server

if __name__ == "__main__":

    app.run(debug=True)



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%; /* 必須讓 html 與 body 撐滿整個螢幕高度,垂直置中才會生效 */
}
.form-signin {
max-width: 330px; /* 限制登入框的最大寬度,否則 m-auto 無法計算左右留白 */
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>



測試




留言

這個網誌中的熱門文章

初探 Vue 呼叫 API 出現 CORS 跨來源資源共享 問題原因

提要:   在 {初探Vue 與 Spring boot 的對話} 專案 ,前端 Vue 應用程式 串接 後端 API 伺服器 ,axios 呼叫 API 時出現以下,”無法取得回應內容 (No 'Access-Control-Allow-Origin' header is present on the requested resource):” 錯誤訊息,根據查找相關資料 ,出現以下原因。 瀏覽器開發工具 錯誤訊息 畫面 錯誤原因: “ Access to XMLHttpRequest at ” from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource” 瀏覽器為了安全考量,實施了同源政策。 當您的前端應用程式 (http://localhost:8080) 嘗試呼叫一個不同來源 (不同協議、不同域名或不同埠號) 的 API 伺服器 (http://localhost:8088) 時,瀏覽器會主動阻止這個請求,除非伺服器明確地允許這個跨來源的存取。 同源政策限制(Same-Origin Policy): 同源政策限制了程式碼和不同網域資源間的互動,同源是指兩份網頁具備相同協定、埠號(如果有指定)以及主機位置 範例: 表列哪些 URL 與 URL http://www.example.com/api/p1 屬於同源: URL                                                   | 結果   | 原因 --------------------------------------------------------------------- http://www.example.com/api/p2     |...

初探 Vue 與 Spring boot 的對話之Frontend (Vue-Frontend)

  Front-end Vue 使用 REST API 建立 Vite 專案 可參考 { Vue 3 初探}  文章 danny@Danny-Yu projects % npm create vite@latest Need to install the following packages: create-vite@8.2.0 Ok to proceed? (y) y > npx > "create-vite" │ ◇   Project name: │   vue-frontend │ ◇   Select a framework: │   Vue │ ◇   Select a variant: │   TypeScript │ ◇   Use rolldown-vite (Experimental)?: │   No │ ◇   Install with npm and start now? │   Yes │ ◇   Scaffolding project in /Users/danny/Desktop/projects/vue-frontend... │ ◇   Installing dependencies with npm... added 47 packages, and audited 48 packages in 27s 6 packages are looking for funding   run `npm fund` for details found 0 vulnerabilities │ ◇   Starting dev server... > vue-frontend@0.0.0 dev > vite   VITE v7.2.4   ready in 411 ms   ➜   Local:   http://localhost:5173/   ➜   Network: use --host to expose   ➜   press h + enter to show...

初探 Spring 中的循環依賴

原因: 當兩個或多個 bean 直接或間接地相互依賴時, 就會出現 Circular Dependency (循環依賴) 如: Bean A -> Bean B -> Bean A import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanA {          @Autowired     private BeanB beanB;     public String sayHi() {         return "Hi! 我是 Class A.";     } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanB {          @Autowired     private BeanA beanA;     public String sayHi() {         return "Hi! 我是 Class B.";     } } 編譯時不會出現問題 danny@Danny-Yu demo % mvn clean install -Dmaven.test.skip=true                           ... ... [INFO] Installing /Users/danny/Desktop/projects/demo/target/dem...