- 用戶登錄功能是 Web 系統一個基本功能,是為用戶提供更好服務的基礎,在 Flask 框架中怎么做用戶登錄功能呢?今天學習一下 Flask 的用戶登錄組件
Flask-Login - Python 之所以如此強大和流行,除了本身易於學習和功能豐富之外,最重要的是因為各種類庫和組件,可以說沒有 Python 做不了的事情,只有不知道的組件。
- 之所以選擇
Flask-Login,是因為它基於Session,適合做有 UI 交互的用戶登錄,用我們學習了的 Flask 表單做演示,更容易理清用戶登錄的流程 -
用戶登錄說明
Flask-Login和其他 Flask 組件並沒有太大區別,有必要開始之前了解下用戶登錄的步驟:-
- 登錄:用戶提供登錄憑證(如用戶名和密碼)提交給服務器
- 建立會話:服務器驗證用戶提供的憑證,如果通過驗證,則建立會話(
Session),並返回給用戶一個會話號(Session id) - 驗證:用戶在后續的交互中提供會話號,服務器將根據會話號(
Session id)確定用戶是否有效 - 登出:當用戶不再與服務器交互時,注銷與服務器建立的會話
-
依據以上步驟,我們設計一個應用場景,作為實現:
- 提供一個主頁,需要登錄才能訪問
- 如果沒有登錄,跳轉到登錄頁面,登錄成功再跳回
- 登錄成功后,可以點擊登出退出登錄
- 在登錄頁面提供注冊連接,點擊后跳轉到注冊頁面
- 注冊完成后,跳轉到登錄頁面
-
初始化
- 先實例化
login_manager對象,然后用它來初始化應用: -
from flask import Flask from flask_login import LoginManager # ... app = Flask(__name__) # 創建 Flask 應用 app.secret_key = 'abc' # 設置表單交互密鑰 login_manager = LoginManager() # 實例化登錄管理對象 login_manager.init_app(app) # 初始化應用 login_manager.login_view = 'login' # 設置用戶登錄視圖函數 endpoint
-
- 表單交互時,所以要設置
secret_key,以防跨域攻擊( CSRF ) - 登錄管理對象
login_manager的login_view屬性,指定登錄頁面的視圖函數 (登錄頁面的endpoint),即驗證失敗時要跳轉的頁面,這里設置為登錄頁
- 表單交互時,所以要設置
-
用戶模塊
-
用戶數據
- 要做用戶驗證,需要維護用戶記錄,為了方便演示,使用一個全局列表
USERS來記錄用戶信息,並且初始化了兩個用戶信息: -
from werkzeug.security import generate_password_hash # ... USERS = [ { "id": 1, "name": 'lily', "password": generate_password_hash('123') }, { "id": 2, "name": 'tom', "password": generate_password_hash('123') } ]
-
用戶信息只包含最基本的信息:
name為登錄用戶名password為登錄密碼,切忌:無論如何不要在系統中存放用戶密碼的明文,幸運的是模塊werkzeug.security提供了generate_password_hash方法,使用 sha256 加密算法將字符串變為密文id為用戶識別碼,相當於主鍵
基於用戶信息,定義兩方法,用來創建(
create_user)和獲取(get_user)用戶信息: -
from werkzeug.security import generate_password_hash import uuid # ... def create_user(user_name, password): """創建一個用戶""" user = { "name": user_name, "password": generate_password_hash(password), "id": uuid.uuid4() } USERS.append(user) def get_user(user_name): """根據用戶名獲得用戶記錄""" for user in USERS: if user.get("name") == user_name: return user return None
-
create_user接受用戶名和密碼,創建用戶記錄,對密碼明文進行加密,並添加用戶ID(使用uuid模板的uuid4方法生成一個全球唯一碼),存儲到USERS列表中get_user接受用戶名,從USERS列表中查找用戶記錄,沒有返回空
-
用戶類
- 下面創建一個用戶類,類維護用戶的登錄狀態,是生成
Session的基礎,Flask-Login提供了用戶基類UserMixin,方便定義自己的用戶類,我們定義一個User: -
from flask_login import UserMixin # 引入用戶基類 from werkzeug.security import check_password_hash # ... class User(UserMixin): """用戶類""" def __init__(self, user): self.username = user.get("name") self.password_hash = user.get("password") self.id = user.get("id") def verify_password(self, password): """密碼驗證""" if self.password_hash is None: return False return check_password_hash(self.password_hash, password) def get_id(self): """獲取用戶ID""" return self.id @staticmethod def get(user_id): """根據用戶ID獲取用戶實體,為 login_user 方法提供支持""" if not user_id: return None for user in USERS: if user.get('id') == user_id: return User(user) return None
-
- 實例化方法接受一個用戶記錄,即
USERS列表中的一個元素,用來初始化成員變量 get_id方法返回用戶實例的ID,這是必須實現的,不然Flask-Login將無法判斷用戶是否被驗證get是個靜態方法,即可以通過類之間調用,是為了在獲取驗證后的用戶實例時用的,必須接受參數ID,返回ID所以對應的用戶實例verify_password方法接受一個明文密碼,與用戶實例中的密碼做校驗,將被用在用戶驗證的判斷邏輯中
- 實例化方法接受一個用戶記錄,即
-
加載登錄用戶
- 有了用戶類,並且實現了
get方法,就可以實現login_manager的user_loader回調函數了,user_loader的作用是根據Session信息加載登錄用戶,它根據用戶ID,返回一個用戶實例: -
@login_manager.user_loader # 定義獲取登錄用戶的方法 def load_user(user_id): return User.get(user_id)
-
登錄頁面
頁面包括后台和展現(可以理解成前台)兩部分
-
from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, EqualTo # ... class LoginForm(FlaskForm): """登錄表單類""" username = StringField('用戶名', validators=[DataRequired()]) password = PasswordField('密碼', validators=[DataRequired()])
-
- 定義用戶名和密碼兩個字段,分別是字符類型字段和密碼類型字段,密碼類型字段會在頁面上顯示為密碼形式,以提高安全性
- 為兩個字段設置必填規則
然后定義一個用戶登錄的視圖函數
login: -
from flask import render_template, redirect, url_for, request from flask_login import login_user # ... @app.route('/login/', methods=('GET', 'POST')) # 登錄 def login(): form = LoginForm() emsg = None if form.validate_on_submit(): user_name = form.username.data password = form.password.data user_info = get_user(user_name) # 從用戶數據中查找用戶記錄 if user_info is None: emsg = "用戶名或密碼密碼有誤" else: user = User(user_info) # 創建用戶實體 if user.verify_password(password): # 校驗密碼 login_user(user) # 創建用戶 Session return redirect(request.args.get('next') or url_for('index')) else: emsg = "用戶名或密碼密碼有誤" return render_template('login.html', form=form, emsg=emsg)
-
分析下視圖函數的邏輯:
- 視圖函數同時支持
GET和POST方法 form.validate_on_submit()可以判斷用戶是否完整的提交了表單,只對POST有效,所以可以用來判斷請求方式- 如果是
POST請求,獲取提交數據,通過get_user方法查找是否存在該用戶 - 如果用戶存在,則創建用戶實體,並校驗登錄密碼
- 校驗通過后,調用
login_user方法創建用戶Session,然后跳轉到請求參數中next所指定的地址或者首頁 (不用擔心如何設置next,還記得上面設置的login_manager.login_view = 'login'嗎?對,未登錄訪問時,會跳轉到login,並且帶上next查詢參數) - 非
POST請求,或者未經過驗證,會顯示login.html模板渲染后的結果
- 視圖函數同時支持
-
前台
在
templates模板下創建登錄頁面的模板login.html: -
{% macro render_field(field) %} <!-- 定義字段宏 --> <dt>{{ field.label }}: <dd>{{ field(**kwargs)|safe }} {% if field.errors %} <ul class=errors> {% for error in field.errors %} <li>{{ error }}</li> {% endfor %} </ul> {% endif %} </dd> {% endmacro %} <!-- 登錄表單 --> <form method="POST"> {{ form.csrf_token }} {{ render_field(form.username) }} {{ render_field(form.password) }} {% if emsg %} <!-- 如果有錯誤信息 則顯示 --> <h3> {{ emsg }}</h3> {% endif %} <input type="submit" value="登錄"> </form> -
render_field是 Jinja2 模板引擎的宏,接受表單字段將其渲染成 Html 代碼,並格式化錯誤信息emsg錯誤信息單獨做了處理,如果存在會顯示出來form中並沒有action屬性,默認為當前路徑
-
需要驗證的頁面
為了方便演示,將首頁作為需要驗證的頁面,通過驗證將看到登錄者歡迎信息,頁面上還有個登出鏈接
首頁視圖函數
index: -
from flask import render_template, url_for from flask_login import current_user, login_required # ... @app.route('/') # 首頁 @login_required # 需要登錄才能訪問 def index(): return render_template('index.html', username=current_user.username)
-
- 注解
@login_required會做用戶登錄檢測,如果沒有登錄要方法此視圖函數,就被跳轉到login接入點(endpoint) current_user是當前登錄者,是User的實例,是Flask-Login提供全局變量( 類似於全局變量g)username是模板中的變量,可以將當前登錄者的用戶名傳入index.html模板
首頁模板
index.html: - 注解
-
<h1>歡迎 {{ username }}!</h1> <a href='{{ url_for('logout')}}'>登出</a> - 登出視圖函數
logout: -
from flask import redirect, url_for from flask_login import logout_user # ... @app.route('/logout') # 登出 @login_required def logout(): logout_user() return redirect(url_for('login'))
-
- 只有登錄了才有必要登出,所以加上注解
@login_required logout_user方法和login_user相反,由於注銷用戶的Session- 登出視圖不需要模板,直接跳轉到登錄頁,實際項目中可以增加一個登出頁,展示些有趣的東西
- 只有登錄了才有必要登出,所以加上注解
-
小試牛刀
終於可以試試了,加上啟動代碼:
-
if __name__ == '__main__': app.run(debug=True)
- 啟動項目,如果一切正常將看到類似的反饋:
-
python app.py * Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Restarting with stat * Debugger is active! * Debugger PIN: 176-611-251 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
- 訪問 localhost:5000,將看到登錄頁,主要瀏覽器地址上的
next查詢參數: - 填寫正確的用戶名和密碼,點擊登錄,將進入首頁:
-
用戶注冊
上面的演示了,已存在用戶登錄的情況,不存在用戶需要完成注冊才能登錄。
注冊功能和登錄很類似,頁面上多了密碼確認字段,並且需要驗證兩次輸入的密碼是否一致,后台邏輯是:如果用戶不存在,且通過檢驗,將用戶數據保存到
USERS列表中,跳轉到login頁面。關於具體實現這里不做詳細講解了,本節代碼示例中有實現,可以參考。
-
Flask-Login 其他特性
上面的實例中使用了一些
Flask-Login的基本特性,Flask-Login還提供了一些其他重要特性記住我
記住我,並不是用戶登出之后,再次登錄時自動填寫用戶名和密碼(這是瀏覽器的功能),而是在用戶意外退出后(比如關閉瀏覽器)不用再次登錄。
如果用戶本地的
cookie失效了,Flask-Login會自動將用戶Session放入cookie中。開啟方法是將
login_user方法的命名參數remember設置為True,此功能默認是關閉的Session 防護
Session信息一般存放在cookie中,但是cookie不夠安全,容易被竊取其中Session信息,偽造用戶登錄系統,幸運的是Flask-Login提供了Session防護機制,提供有basic和strong兩種保護等級,通過login_manager.session_protection來開關和設置等級,默認等級為basic,如果設置為None將關閉Session防護機制。在保護機制開啟的情況下,每次請求會根據用戶的特征(一般指有用戶IP、瀏覽器類型生成的哈希碼)與
Session中的對比,如果無法匹配則要求用戶重新登錄,在強模式下(strong)一旦匹配失敗會刪除登錄者Session,以消除攻擊者重構cookie的可能Request Loader
有時候因為一些原因不想或者無法使用
cookie,可以將Session記錄在其他地方,比如Header中或者請求參數中,那么構造用戶Session時就需要將user_loader替換為request_loader,request_loader將request作為參數,這樣就可以從請求的任何數據中獲取Session信息了
