Flask 使用者登入 Flask-Login

語言: CN / TW / HK

使用者登入功能是 Web 系統一個基本功能,是為使用者提供更好服務的基礎,在 Flask 框架中怎麼做使用者登入功能呢?今天我們學習一下 Flask 的使用者登入元件 Flask-Login

Python 之所以如此強大和流行,除了本身易於學習和功能豐富之外,最重要的是因為各種類庫和元件,可以說沒有 Python 做不了的事情,只有不知道的元件。

但是同一個問題領域中的元件或類庫名稱、功能可能近似,版本多而混亂,會給使用者造成了困擾,比如之前講述的 Flask-BootstrapBootstrap-Flask ,以及今天要講述的使用者登入,由於方式多樣,功能相似,所以出現了很多類似的框架,比如 Flask-LoginFlask-AuthFlask-Security 等等

之所以選擇 Flask-Login,是因為它基於 Session,適合做有 UI 互動的使用者登入,用我們學習了的 Flask 表單做演示,更容易理清使用者登入的流程

使用者登入說明

Flask-Login 和其他 Flask 元件並沒有太大區別,有必要開始之前瞭解下使用者登入的步驟:

  • 1 登入:使用者提供登入憑證(如使用者名稱和密碼)提交給伺服器
  • 2 建立會話:伺服器驗證使用者提供的憑證,如果通過驗證,則建立會話( Session ),並返回給使用者一個會話號( Session id
  • 3 驗證:使用者在後續的互動中提供會話號,伺服器將根據會話號( Session id )確定使用者是否有效
  • 4 登出:當用戶不再與伺服器互動時,登出與伺服器建立的會話

依據以上步驟,我們設計一個應用場景,作為實現:

  • 提供一個主頁,需要登入才能訪問
  • 如果沒有登入,跳轉到登入頁面,登入成功再跳回
  • 登入成功後,可以點選登出退出登入
  • 在登入頁面提供註冊連線,點選後跳轉到註冊頁面
  • 註冊完成後,跳轉到登入頁面

安裝

使用 pip 安裝 Flask-Login 元件:

shell pip install flask-login

如果一切正常,可以將 Flask-Login 模組引入:

```python

from flask-login import LoginManager

```

本次實踐中,會用到 Flask Form 相關功能,請確保已經安裝了 Flask-WTF 元件,詳見 Web 開發 Form

初始化

先例項化 login_manager 物件,然後用它來初始化應用:

```python 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_managerlogin_view 屬性,指定登入頁面的檢視函式 (登入頁面的 endpoint),即驗證失敗時要跳轉的頁面,這裡設定為登入頁

使用者模組

使用者資料

要做使用者驗證,需要維護使用者記錄,為了方便演示,使用一個全域性列表 USERS 來記錄使用者資訊,並且初始化了兩個使用者資訊:

```python 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 )使用者資訊:

```python 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

```python 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_manageruser_loader 回撥函數了,user_loader 的作用是根據 Session 資訊載入登入使用者,它根據使用者 ID,返回一個使用者例項:

python @login_manager.user_loader # 定義獲取登入使用者的方法 def load_user(user_id): return User.get(user_id)

登入頁面

頁面包括後臺和展現(可以理解成前臺)兩部分

後臺

根據前面介紹的 Form 相關知識 (參見 Web 開發 Form ),需要定義一個 Form 類,用來設定頁面的元素和規則:

```python from wtforms import StringField, PasswordField from wtforms.validators import DataRequired, EqualTo

...

class LoginForm(FlaskForm): """登入表單類""" username = StringField('使用者名稱', validators=[DataRequired()]) password = PasswordField('密碼', validators=[DataRequired()]) ```

  • 定義使用者名稱和密碼兩個欄位,分別是字元型別欄位和密碼型別欄位,密碼型別欄位會在頁面上顯示為密碼形式,以提高安全性
  • 為兩個欄位設定必填規則

然後定義一個使用者登入的檢視函式 login:

```python 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) ```

分析下檢視函式的邏輯:

  • 檢視函式同時支援 GETPOST 方法
  • 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: {% raw %}

```html {% macro render_field(field) %}

{{ field.label }}:
{{ field(**kwargs)|safe }} {% if field.errors %}
    {% for error in field.errors %}
  • {{ error }}
  • {% endfor %}
{% endif %}
{% endmacro %}
{{ form.csrf_token }} {{ render_field(form.username) }} {{ render_field(form.password) }} {% if emsg %}

{{ emsg }}

{% endif %}
``` {% endraw %} - `render_field` 是 Jinja2 模板引擎的巨集,接受表單欄位將其渲染成 Html 程式碼,並格式化錯誤資訊 - `emsg` 錯誤資訊單獨做了處理,如果存在會顯示出來 - `form` 中並沒有 `action` 屬性,預設為當前路徑 ## 需要驗證的頁面 為了方便演示,將首頁作為需要驗證的頁面,通過驗證將看到登入者歡迎資訊,頁面上還有個登出連結 首頁檢視函式 `index`: ```python 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`: {% raw %} ```html

歡迎 {{ username }}!

登出 ``` {% endraw %} 登出檢視函式 `logout`: ```python 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` - 登出檢視不需要模板,直接跳轉到登入頁,實際專案中可以增加一個登出頁,展示些有趣的東西 ## 小試牛刀 終於可以試試了,加上啟動程式碼: ```python if __name__ == '__main__': app.run(debug=True) ``` 啟動專案,如果一切正常將看到類似的反饋: ```shell 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](localhost:5000),將看到登入頁,主要瀏覽器地址上的 `next` 查詢引數: ![顯示結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4fcdcb2ab4e94b4ca96b5634d6e11984~tplv-k3u1fbpfcp-zoom-1.image) 填寫正確的使用者名稱和密碼,點選登入,將進入首頁: ![顯示結果](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/90f5decc85b84144a8b41fc12bae6348~tplv-k3u1fbpfcp-zoom-1.image) ## 使用者註冊 上面的演示了,已存在使用者登入的情況,不存在使用者需要完成註冊才能登入。 註冊功能和登入很類似,頁面上多了密碼確認欄位,並且需要驗證兩次輸入的密碼是否一致,後臺邏輯是:如果使用者不存在,且通過檢驗,將使用者資料儲存到 `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` 資訊了 ## 總結 本節課程主要通過一個簡單的使用者登入例項,介紹了 `Flask-Login` 元件的使用,大體步驟是:引入 `Flask-Login` 模組,初始化應用,構造登入使用者類,設定登入頁面入口,使用 `login_user` 建立使用者 `Session`, 用 `user_loader` 恢復登入者,用 `logout_user` 推出登入,還有在檢視函式中如何進行使用者驗證等,最後介紹了一些額外的 `Flask-Login` 特性。 使用者登入是 `Web` 應用的一個常用而又複雜的功能,除了今天介紹的 `Session` 方式之外,還有基於 `RESTful` 的非狀態的 `token` 方式,以及第三方認證機制,比如微信、支付寶等,後面會陸續講解,敬請期待。 > [示例程式碼](https://docs.qq.com/doc/DUnRjV252SUFya1ND)