Python-用戶登錄 Flask-Login


  • 用戶登錄功能是 Web 系統一個基本功能,是為用戶提供更好服務的基礎,在 Flask 框架中怎么做用戶登錄功能呢?今天學習一下 Flask 的用戶登錄組件 Flask-Login
  • Python 之所以如此強大和流行,除了本身易於學習和功能豐富之外,最重要的是因為各種類庫和組件,可以說沒有 Python 做不了的事情,只有不知道的組件。
  • 之所以選擇 Flask-Login,是因為它基於Session,適合做有 UI 交互的用戶登錄,用我們學習了的 Flask 表單做演示,更容易理清用戶登錄的流程
  • 用戶登錄說明

  • Flask-Login 和其他 Flask 組件並沒有太大區別,有必要開始之前了解下用戶登錄的步驟:
    1. 登錄:用戶提供登錄憑證(如用戶名和密碼)提交給服務器
    2. 建立會話:服務器驗證用戶提供的憑證,如果通過驗證,則建立會話( Session ),並返回給用戶一個會話號( Session id )
    3. 驗證:用戶在后續的交互中提供會話號,服務器將根據會話號( Session id )確定用戶是否有效
    4. 登出:當用戶不再與服務器交互時,注銷與服務器建立的會話
  • 依據以上步驟,我們設計一個應用場景,作為實現:

    • 提供一個主頁,需要登錄才能訪問
    • 如果沒有登錄,跳轉到登錄頁面,登錄成功再跳回
    • 登錄成功后,可以點擊登出退出登錄
    • 在登錄頁面提供注冊連接,點擊后跳轉到注冊頁面
    • 注冊完成后,跳轉到登錄頁面
  • 初始化

  • 先實例化 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_loaderrequest_loader 將 request 作為參數,這樣就可以從請求的任何數據中獲取 Session 信息了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM