Flask 框架提供了強大的 Session 模組元件,為 Web 應用實現使用者註冊與登入系統提供了方便的機制。結合 Flask-WTF 表單元件,我們能夠輕鬆地設計出使用者友好且具備美觀介面的註冊和登入頁面,使這一功能能夠直接應用到我們的專案中。本文將深入探討如何透過 Flask 和 Flask-WTF 構建一個完整的使用者註冊與登入系統,以及如何對頁面進行最佳化美化,提高使用者體驗。透過這一系統,使用者能夠方便註冊賬戶、安全登入,並且我們能夠有效管理使用者的會話資訊,為 Web 應用的使用者管理提供一種高效的解決方案。
什麼是Session機制?
Session 是一種在 Web
應用中用於儲存使用者特定資訊的機制。它允許在使用者訪問網站時儲存和檢索資訊,以便在使用者的不同請求之間保持狀態。Session
機制在使用者登入、購物網站、個性化設定等場景中得到廣泛應用,它為使用者提供了更加連貫和個性化的體驗。在 Flask
中,透過 Flask Session
模組可以方便地使用 Session ,實現使用者狀態的維護和管理。
在 Web 開發中,HTTP 協議是無狀態的,即每個請求都是獨立的,伺服器不會記住之前的請求資訊。為了解決這個問題,引入了 Session
機制。基本思想是在使用者訪問網站時,伺服器生成一個唯一的 Session ID
,並將這個 ID 儲存在使用者的瀏覽器中(通常透過 Cookie
)。同時,伺服器端會儲存一個對映,將 Session ID
與使用者的相關資訊關聯起來,這樣在使用者的後續請求中,伺服器就能根據 Session ID
找到相應的使用者資訊,從而實現狀態的保持。
Session 的認證流程通常包括以下步驟:
- 使用者登入: 使用者透過提供使用者名稱和密碼進行登入。在登入驗證成功後,伺服器為該使用者建立一個唯一的 Session ID,並將這個 ID 儲存在使用者瀏覽器的 Cookie 中。
- Session 儲存: 伺服器端將使用者的相關資訊(如使用者 ID、許可權等)與 Session ID 關聯起來,並將這些資訊儲存在伺服器端的 Session 儲存中。Session 儲存可以是記憶體、資料庫或其他持久化儲存方式。
- Session ID 傳遞: 伺服器將生成的 Session ID 傳送給使用者瀏覽器,通常是透過 Set-Cookie 頭部。這個 Cookie 會在使用者的每次請求中被包含在 HTTP 頭中。
- 後續請求: 使用者在後續的請求中會攜帶包含 Session ID 的 Cookie。伺服器透過解析請求中的 Session ID,從 Session 儲存中檢索使用者的資訊,以恢復使用者的狀態。
- 認證檢查: 伺服器在每次請求中檢查 Session ID 的有效性,並驗證使用者的身份。如果 Session ID 無效或過期,使用者可能需要重新登入。
- 使用者登出: 當使用者主動登出或 Session 過期時,伺服器將刪除與 Session ID 關聯的使用者資訊,使用者需要重新登入。
總體而言,Session 的認證流程透過在客戶端和伺服器端之間傳遞唯一的 Session ID
,實現了使用者狀態的持久化和管理。這種機制使得使用者可以在多個請求之間保持登入狀態,提供了一種有效的使用者認證方式。在 Flask 中,開發者可以方便地使用 Flask 提供的 Session 模組來實現這一流程。
Session 認證基礎
預設情況下,直接使用Session
模組即可實現Session
登入會話保持,該方式是將Session
儲存到記憶體中,程式重啟後即釋放,Session的設定一般可以透過使用session["username"]
賦值的方式進行,如需驗證該Session的可靠性,則只需要呼叫session.get
方法即可一得到特定欄位,透過對欄位的判斷即可實現認證機制。
如下是一個Flask後端程式碼,執行後透過訪問http://127.0.0.1:5000
進入到登入這頁面。
from flask import Flask,session,render_template,request,Response,redirect,url_for
from functools import wraps
import os
app = Flask(__name__, static_folder="./template",template_folder="./template")
app.config['SECRET_KEY'] = os.urandom(24)
# 登入認證裝飾器
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if session.get("username") != None and session.get("is_login") ==True:
print("登陸過則繼續執行原函式")
return func(*args, **kwargs)
else:
print("沒有登入則跳轉到登入頁面")
resp = Response()
resp.status_code=200
resp.data = "<script>window.location.href='/login';</script>"
return resp
return wrapper
@app.route("/login",methods=["GET","POST"])
def login():
if request.method == "GET":
html = """
<form action="/login" method="post">
<p>賬號: <input type="text" name="username"></p>
<p>密碼: <input type="password" name="password"></p>
<input type="submit" value="登入">
</form>
"""
return html
if request.method == "POST":
get_dict = request.form.to_dict()
get_username = get_dict['username']
get_password = get_dict['password']
if (get_username == "lyshark" or get_username == "admin") and get_password == "123123":
session["username"] = get_username
session["is_login"] = True
print("登入完成直接跳到主頁")
resp = Response()
resp.status_code=200
resp.data = "<script>window.location.href='/index';</script>"
return resp
else:
return "登陸失敗"
return "未知錯誤"
# 主頁選單
@app.route("/index",methods = ["GET","POST"])
@login_required
def index():
username = session.get("username")
return "使用者 {} 您好,這是主頁面".format(username)
# 第二個選單
@app.route("/get",methods = ["GET","POST"])
@login_required
def get():
username = session.get("username")
return "使用者 {} 您好,這是子頁面".format(username)
@app.route("/logout",methods = ["GET","POST"])
@login_required
def logout():
username = session.get("username")
# 登出操作
session.pop("username")
session.pop("is_login")
session.clear()
return "使用者 {} 已登出".format(username)
if __name__ == '__main__':
app.run()
程式執行後,當使用者訪問http://127.0.0.1:5000
地址則會跳轉到login
登陸頁面,此時如果使用者第一次訪問則會輸出如下所示的登陸資訊;
透過輸入正確的使用者名稱lyshark
和密碼123123
則可以登入成功,此處登入的使用者是lyshark
如下圖。
透過輸入不同的使用者登入會出現不同的頁面提示資訊,如下圖則是admin
的主頁資訊。
當我們手動輸入logout
時則此時會退出登入使用者,後臺也會清除該使用者的Session
,在開發中可以自動跳轉到登出頁面;
Session 使用資料庫
透過結合 Session 與 SQLite 資料庫,我們可以實現一個更完善的使用者註冊、登入以及密碼修改功能。在這個案例中,首先,使用者可以透過登錄檔單輸入使用者名稱、密碼等資訊,這些資訊經過驗證後將被儲存到 SQLite 資料庫中。註冊成功後,使用者可以使用相同的使用者名稱和密碼進行登入。登入成功後,我們使用 Flask 的 Session 機制將使用者資訊儲存在伺服器端,確保使用者在訪問其他頁面時仍然處於登入狀態。
為了增加更多功能,我們還可以實現密碼修改的功能。使用者在登入狀態下,透過密碼修改表單輸入新的密碼,我們將新密碼更新到資料庫中,確保使用者可以安全地更改密碼。這個案例綜合運用了 Flask、SQLite 和 Session 等功能,為 Web 應用提供了一套完整的使用者管理系統。
from flask import Flask,request,render_template,session,Response
import sqlite3,os
from functools import wraps
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
# 建立資料庫
def UserDB():
conn = sqlite3.connect("./database.db")
cursor = conn.cursor()
create = "create table UserDB(" \
"uid INTEGER primary key AUTOINCREMENT not null unique," \
"username char(64) not null unique," \
"password char(64) not null," \
"email char(64) not null" \
")"
cursor.execute(create)
conn.commit()
cursor.close()
conn.close()
# 增刪改查簡單封裝
def RunSqlite(db,table,action,field,value):
connect = sqlite3.connect(db)
cursor = connect.cursor()
# 執行插入動作
if action == "insert":
insert = f"insert into {table}({field}) values({value});"
if insert == None or len(insert) == 0:
return False
try:
cursor.execute(insert)
except Exception:
return False
# 執行更新操作
elif action == "update":
update = f"update {table} set {value} where {field};"
if update == None or len(update) == 0:
return False
try:
cursor.execute(update)
except Exception:
return False
# 執行查詢操作
elif action == "select":
# 查詢條件是否為空
if value == "none":
select = f"select {field} from {table};"
else:
select = f"select {field} from {table} where {value};"
try:
ref = cursor.execute(select)
ref_data = ref.fetchall()
connect.commit()
connect.close()
return ref_data
except Exception:
return False
# 執行刪除操作
elif action == "delete":
delete = f"delete from {table} where {field};"
if delete == None or len(delete) == 0:
return False
try:
cursor.execute(delete)
except Exception:
return False
try:
connect.commit()
connect.close()
return True
except Exception:
return False
# 建立資料庫
@app.route("/create")
def create():
UserDB()
return "create success"
# 登入認證裝飾器
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if session.get("username") != None and session.get("is_login") ==True:
print("登陸過則繼續執行原函式")
return func(*args, **kwargs)
else:
print("沒有登入則跳轉到登入頁面")
resp = Response()
resp.status_code=200
resp.data = "<script>window.location.href='/login';</script>"
return resp
return wrapper
# 使用者註冊頁面
@app.route("/register",methods=["GET","POST"])
def register():
if request.method == "GET":
html = """
<form action="/register" method="post">
<p>賬號: <input type="text" name="username"></p>
<p>密碼: <input type="password" name="password"></p>
<p>郵箱: <input type="text", name="email"></p>
<input type="submit" value="使用者註冊">
</form>
"""
return html
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
email = request.form.get("email")
if RunSqlite("database.db","UserDB","select","username",f"username='{username}'") == []:
insert = RunSqlite("database.db","UserDB","insert","username,password,email",f"'{username}','{password}','{email}'")
if insert == True:
return "建立完成"
else:
return "建立失敗"
else:
return "使用者存在"
return "未知錯誤"
# 使用者登入模組
@app.route("/login",methods=["GET","POST"])
def login():
if request.method == "GET":
html = """
<form action="/login" method="post">
<p>賬號: <input type="text" name="username"></p>
<p>密碼: <input type="password" name="password"></p>
<input type="submit" value="登入">
</form>
"""
return html
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
select = RunSqlite("database.db","UserDB","select","username,password",f"username='{username}'")
if select != []:
# 繼續驗證密碼
if select[0][1] == password:
session["username"] = username
session["is_login"] = True
print("登入完成直接跳到主頁")
resp = Response()
resp.status_code = 200
resp.data = "<script>window.location.href='/index';</script>"
return resp
else:
return "密碼不正確"
else:
return "使用者不存在"
return "未知錯誤"
# 修改密碼
@app.route("/modify",methods=["GET","POST"])
@login_required
def modify():
if request.method == "GET":
html = """
<form action="/modify" method="post">
<p>新密碼: <input type="password" name="new_password"></p>
<input type="submit" value="修改密碼">
</form>
"""
return html
if request.method == "POST":
username = session.get("username")
new_password = request.form.get("new_password")
update = RunSqlite("database.db","UserDB","update",f"username='{username}'",f"password='{new_password}'")
if update == True:
# 登出操作
session.pop("username")
session.pop("is_login")
session.clear()
print("密碼已更新,請重新登入")
resp = Response()
resp.status_code = 200
resp.data = "<script>window.location.href='/login';</script>"
return resp
else:
return "密碼更新失敗"
return "未知錯誤"
# 主頁選單
@app.route("/index",methods = ["GET","POST"])
@login_required
def index():
username = session.get("username")
return "使用者 {} 您好,這是主頁面".format(username)
# 第二個選單
@app.route("/get",methods = ["GET","POST"])
@login_required
def get():
username = session.get("username")
return "使用者 {} 您好,這是子頁面".format(username)
@app.route("/logout",methods = ["GET","POST"])
@login_required
def logout():
username = session.get("username")
# 登出操作
session.pop("username")
session.pop("is_login")
session.clear()
return "使用者 {} 已登出".format(username)
if __name__ == '__main__':
app.run(debug=True)
案例被執行後首先透過呼叫http://127.0.0.1:5000/create
建立database.db
資料庫,接著我們可以透過訪問/register
路徑實現賬號註冊功能,如下我們註冊lyshark
密碼是123123
,輸出效果如下所示;
透過訪問/modify
可實現對使用者密碼的修改,但在修改之前需要先透過/login
頁面登入後進行,否則會預設跳轉到使用者登入頁面中;
使用WTForms登入模板
在如上程式碼基礎上,我們著重增加一個美化登入模板,以提升使用者在註冊登入流程中的整體體驗。透過引入WTF表單元件和Flask-WTF擴充套件,在前端實現了一個更友好的登入頁面。
此登入模板的設計考慮了頁面佈局、顏色搭配、表單樣式等因素,以確保使用者在輸入使用者名稱和密碼時感到輕鬆自然。同時,我們利用Flask-WTF的驗證器功能,對使用者輸入的資料進行有效性檢查,保障了使用者資訊的安全性。
首先,我們需要在template
目錄下,建立register.html
前端檔案,用於使用者註冊,並寫入以下程式碼。
<html>
<head>
<link rel="stylesheet" href="https://www.lyshark.com/javascript/bootstrap/3.3.7/css/bootstrap.min.css">
<link href="https://www.lyshark.com/javascript/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="https://www.lyshark.com/javascript/other/my_login.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-offset-3 col-md-6">
<form action="/register" method="post" class="form-horizontal">
{{ form.csrf_token }}
<span class="heading">用 戶 注 冊</span>
<div class="form-group">
{{ form.username }}
<i class="fa fa-user"></i>
<a href="/login" class="fa fa-question-circle"></a>
</div>
<div class="form-group">
{{ form.email }}
<i class="fa fa-envelope"></i>
</div>
<div class="form-group">
{{ form.password }}
<i class="fa fa-lock"></i>
</div>
<div class="form-group">
{{ form.RepeatPassword }}
<i class="fa fa-unlock-alt"></i>
</div>
{{ form.submit }}
</form>
</div>
</div>
</div>
</body>
</html>
接著,繼續建立login.html
前端檔案,用於登入賬號時使用,並寫入以下程式碼。
<html>
<head>
<link rel="stylesheet" href="https://www.lyshark.com/javascript/bootstrap/3.3.7/css/bootstrap.min.css">
<link href="https://www.lyshark.com/javascript/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="https://www.lyshark.com/javascript/other/my_login.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-offset-3 col-md-6">
<form action="/login" method="post" class="form-horizontal">
{{ form.csrf_token }}
<span class="heading">用 戶 登 錄</span>
<div class="form-group">
{{ form.username }}
<i class="fa fa-user"></i>
</div>
<div class="form-group help">
{{ form.password }}
<i class="fa fa-lock"></i>
<a href="#" class="fa fa-question-circle"></a>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success">登 錄 後 臺</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
後臺程式碼部分,我們需要在原始碼的基礎之上,增加對前端註冊和登入頁面的渲染類,此處使用flask_wtf
元件實現渲染生成,具體程式碼如下。
from flask import Flask,request,render_template,session,Response
from functools import wraps
import sqlite3,os
from flask_wtf import FlaskForm
from wtforms import widgets,validators
from wtforms.validators import DataRequired,Regexp,DataRequired, Length, Email, EqualTo, NumberRange
from wtforms.fields import (StringField, PasswordField, DateField, BooleanField,DateTimeField,TimeField,
SelectField, SelectMultipleField, TextAreaField,FloatField,HiddenField,
RadioField, IntegerField, DecimalField, SubmitField, IntegerRangeField)
# app = Flask(__name__, static_folder="./template",template_folder="./template")
app = Flask(__name__)
app.config["SECRET_KEY"] = "d3d3Lmx5c2hhcmsuY29t"
# -----------------------------------------------------------------------------
# 建立資料庫
def UserDB():
conn = sqlite3.connect("database.db")
cursor = conn.cursor()
create = "create table UserDB(" \
"uid INTEGER primary key AUTOINCREMENT not null unique," \
"username char(64) not null unique," \
"password char(64) not null," \
"email char(64) not null" \
")"
cursor.execute(create)
conn.commit()
cursor.close()
conn.close()
# 增刪改查簡單封裝
def RunSqlite(db,table,action,field,value):
connect = sqlite3.connect(db)
cursor = connect.cursor()
# 執行插入動作
if action == "insert":
insert = f"insert into {table}({field}) values({value});"
if insert == None or len(insert) == 0:
return False
try:
cursor.execute(insert)
except Exception:
return False
# 執行更新操作
elif action == "update":
update = f"update {table} set {value} where {field};"
if update == None or len(update) == 0:
return False
try:
cursor.execute(update)
except Exception:
return False
# 執行查詢操作
elif action == "select":
# 查詢條件是否為空
if value == "none":
select = f"select {field} from {table};"
else:
select = f"select {field} from {table} where {value};"
try:
ref = cursor.execute(select)
ref_data = ref.fetchall()
connect.commit()
connect.close()
return ref_data
except Exception:
return False
# 執行刪除操作
elif action == "delete":
delete = f"delete from {table} where {field};"
if delete == None or len(delete) == 0:
return False
try:
cursor.execute(delete)
except Exception:
return False
try:
connect.commit()
connect.close()
return True
except Exception:
return False
# -----------------------------------------------------------------------------
# 生成使用者登錄檔單
class RegisterForm(FlaskForm):
username = StringField(
validators=[
DataRequired(message='使用者名稱不能為空'),
Length(min=1, max=15, message='使用者名稱長度必須大於%(min)d且小於%(max)d')
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control', "placeholder":"輸入註冊使用者名稱"}
)
email = StringField(
validators=[validators.DataRequired(message='郵箱不能為空'),validators.Email(message="郵箱格式輸入有誤")],
render_kw={'class':'form-control', "placeholder":"輸入Email郵箱"}
)
password = PasswordField(
validators=[
validators.DataRequired(message='密碼不能為空'),
validators.Length(min=5, message='使用者名稱長度必須大於%(min)d'),
validators.Regexp(regex="[0-9a-zA-Z]{5,}",message='密碼不允許使用特殊字元')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control', "placeholder":"輸入使用者密碼"}
)
RepeatPassword = PasswordField(
validators=[
validators.DataRequired(message='密碼不能為空'),
validators.Length(min=5, message='密碼長度必須大於%(min)d'),
validators.Regexp(regex="[0-9a-zA-Z]{5,}",message='密碼不允許使用特殊字元'),
validators.EqualTo("password",message="兩次密碼輸入必須一致")
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control', "placeholder":"再次輸入密碼"}
)
submit = SubmitField(
label="用 戶 注 冊", render_kw={ "class":"btn btn-success" }
)
# 生成使用者登入表單
class LoginForm(FlaskForm):
username = StringField(
validators=[
validators.DataRequired(message=''),
validators.Length(min=4, max=15, message=''),
validators.Regexp(regex="[0-9a-zA-Z]{4,15}", message='')
],
widget=widgets.TextInput(),
render_kw={"class":"form-control", "placeholder":"請輸入使用者名稱或電子郵件"}
)
password = PasswordField(
validators=[
validators.DataRequired(message=''),
validators.Length(min=5, max=15,message=''),
validators.Regexp(regex="[0-9a-zA-Z]{5,15}",message='')
],
widget=widgets.PasswordInput(),
render_kw={"class":"form-control", "placeholder":"請輸入密碼"}
)
# -----------------------------------------------------------------------------
# 建立資料庫
@app.route("/create")
def create():
UserDB()
return "create success"
# 登入認證裝飾器
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if session.get("username") != None and session.get("is_login") ==True:
print("登陸過則繼續執行原函式")
return func(*args, **kwargs)
else:
print("沒有登入則跳轉到登入頁面")
resp = Response()
resp.status_code=200
resp.data = "<script>window.location.href='/login';</script>"
return resp
return wrapper
# 使用者註冊頁面
@app.route("/register",methods=["GET","POST"])
def register():
form = RegisterForm(csrf_enabled = True)
if request.method == "POST":
if form.validate_on_submit():
username = form.username.data
password = form.RepeatPassword.data
email = form.email.data
print("使用者: {} 郵箱: {}".format(username,email))
if RunSqlite("database.db", "UserDB", "select", "username", f"username='{username}'") == []:
insert = RunSqlite("database.db", "UserDB", "insert", "username,password,email",
f"'{username}','{password}','{email}'")
if insert == True:
return "建立完成"
else:
return "建立失敗"
else:
return "使用者存在"
return render_template("register.html", form=form)
# 使用者登入頁面
@app.route("/login",methods=["GET","POST"])
def login():
form = LoginForm(csrf_enabled = True)
if request.method == "POST":
username = form.username.data
password = form.password.data
select = RunSqlite("database.db","UserDB","select","username,password",f"username='{username}'")
if select != []:
# 繼續驗證密碼
if select[0][1] == password:
session["username"] = username
session["is_login"] = True
print("登入完成直接跳到主頁")
resp = Response()
resp.status_code = 200
resp.data = "<script>window.location.href='/index';</script>"
return resp
else:
return "密碼不正確"
else:
return "使用者不存在"
return render_template("login.html", form=form)
# 修改密碼
@app.route("/modify",methods=["GET","POST"])
@login_required
def modify():
if request.method == "GET":
html = """
<form action="/modify" method="post">
<p>新密碼: <input type="password" name="new_password"></p>
<input type="submit" value="修改密碼">
</form>
"""
return html
if request.method == "POST":
username = session.get("username")
new_password = request.form.get("new_password")
update = RunSqlite("database.db","UserDB","update",f"username='{username}'",f"password='{new_password}'")
if update == True:
# 登出操作
session.pop("username")
session.pop("is_login")
session.clear()
print("密碼已更新,請重新登入")
resp = Response()
resp.status_code = 200
resp.data = "<script>window.location.href='/login';</script>"
return resp
else:
return "密碼更新失敗"
return "未知錯誤"
# 主頁選單
@app.route("/index",methods = ["GET","POST"])
@login_required
def index():
username = session.get("username")
return "使用者 {} 您好,這是主頁面".format(username)
# 第二個選單
@app.route("/get",methods = ["GET","POST"])
@login_required
def get():
username = session.get("username")
return "使用者 {} 您好,這是子頁面".format(username)
@app.route("/logout",methods = ["GET","POST"])
@login_required
def logout():
username = session.get("username")
# 登出操作
session.pop("username")
session.pop("is_login")
session.clear()
return "使用者 {} 已登出".format(username)
if __name__ == '__main__':
app.run(debug=True)
目錄結果如下圖所示;
當使用者訪問/register
時,則可以看到透過flask_wtf
渲染後的使用者註冊頁面,如下圖所示;
使用者訪問/login
時,則是使用者登入頁面,如下圖所示;