<style> html, body, .ui-content { background-color: #333; color: #ddd; } body > .ui-infobar { display: none; } .ui-view-area > .ui-infobar { display: block; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { color: #ddd; } .markdown-body h1, .markdown-body h2 { border-bottom-color: #ffffff69; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #fff; } .markdown-body img { background-color: transparent; } .ui-toc-dropdown .nav>.active:focus>a, .ui-toc-dropdown .nav>.active:hover>a, .ui-toc-dropdown .nav>.active>a { color: white; border-left: 2px solid white; } .expand-toggle:hover, .expand-toggle:focus, .back-to-top:hover, .back-to-top:focus, .go-to-bottom:hover, .go-to-bottom:focus { color: white; } .ui-toc-dropdown { background-color: #333; } .ui-toc-label.btn { background-color: #191919; color: white; } .ui-toc-dropdown .nav>li>a:focus, .ui-toc-dropdown .nav>li>a:hover { color: white; border-left: 1px solid white; } .markdown-body blockquote { color: #bcbcbc; } .markdown-body table tr { background-color: #5f5f5f; } .markdown-body table tr:nth-child(2n) { background-color: #4f4f4f; } .markdown-body code, .markdown-body tt { color: #eee; background-color: rgba(230, 230, 230, 0.36); } a, .open-files-container li.selected a { color: #5EB7E0; } </style>} # Python + Flask 虛擬美國股票交易網站 Part5 (使用Flask-Login管理用戶認證) ###### tags: `CS50` `Python` `Flask` ## 前言 ### Flask-Login為Flask提供了用戶session管理功能,可以輕鬆地處理登入登出等功能。 下表為Flask-Login要求用戶類實現的方法和屬性 | Class | Introduction | | ---------------- | ------------ | | is_authenticated | 如果用戶已經通過認證,返回True, 否則返回False| | is_active | 如果允許用戶登錄,返回True, 否則返回False | | is_anonymous | 如果當前用戶未登錄(匿名),返回True, 否則返回False | | get_id() | 以Unicode形式返回用戶的唯一標示符,返回True, 否則返回False | # ### 先安裝flask-login ``` $ pip3 install flask-login ``` ### 然後在 vfinance/extensions.py 以及工廠函數中調用此方法 #### vfinance/extensions.py ``` from flask_login import LoginManager ... login_manager = LoginManager() ``` #### vfinance/__init __.py ``` login_manager.init_app(app) ``` ### 接著讓用戶類繼承Flask-Login提供的UserMixin類 #### vfinance/models.py ``` from flask_login import UserMixin class User(db.Model, UserMixin): ... ``` UserMixin代表已經通過認證的用戶,所以is_authenticated和is_active會回傳True,而is_anonymous則會傳False。get_id()會查找用戶兌現的id屬性值作為id,也就是`Class User`中的`Primary key` > 使用FlaskLogin登入/登出某个用户非常简单,只需要在视图函数中调用FlaskLogin提供的login_user()或logout_user()函数,并传入要登入/登出的用户类对象。在这两个函数背后,FlaskLogin使用Flask的session对象将用户的id值存储到用户浏览器的cookie中(名为user_id),这时表示用户被登入。相对来说,登出则意味着在用户浏览器的cookie中删除这个值。默认情况下,关闭浏览器时,通过Flask的session对象存储在客户端的sessioncookie会被删除,所以用户会登出。另外,FlaskLogin还支持记住登录状态,通过在login_user()中将remember参数设为True即可实现。这时FlaskLogin会在用户浏览器中创建一个名为remember_token的cookie,当通过session设置的user_idcookie因为用户关闭浏览器而失效时,它会重新恢复user_idcookie的值。 [李辉 - Flask Web开发实战 helloflask](http://helloflask.com/book/1/) ## 獲取當前用戶 使用Flask-Login提供的current_user對象,表示當前用戶。調用時會傳回與當欠用戶對應的用戶模型類對象。因為session中只會儲存登錄用戶的id,所以為了讓他返回對應的用戶對象,要設置一個用戶加載函數。這個函數要用login_manager.user_loader的裝飾器,接受用戶id作為參數,傳回對應的用戶對象 #### vfinance/extension.py ``` ... @login_manager.user_loader def load_user(user_id): from bluelog.models import Admin user = Admin.query.get(int(user_id)) return user ``` 設置後,調用current_user時,Flask-Login會調用用戶加載函數並傳回對應的用戶對象。如果當前用戶已經登錄,會返回Admin類實例;如果用戶未登錄,current_user默認會返回Flask-Login內建的AnonymousUserMixin類對象,它的is_authenticated和is_active屬性會返回False,而is_anonymous屬性會返回True。 最後可以通過對current_user對象調用is_authenticated等屬性來判斷當前用戶的認證狀態,它也已經註冊到了模板上下文中,因此可以在模板中根據用戶狀態渲染不同的內容。 例如: ``` <small>  {%if current_user.is_authenticated%}    # 如果用戶已經登入就顯示登出按鈕 <ahref="{{url_for('auth.logout',next=request.full_path)}}">Logout</a>   {% else %}    # 如果用戶沒有已經登入就顯示登入按鈕 <ahref="{{url_for('auth.login',next=request.full_path)}}">Login</a>   {% endif %} </small> ``` ## 註冊/登入/登出 vfinance/blueprints/auth.py ### 登入 ``` import os from flask import Blueprint, render_template, flash, redirect, url_for from flask_login import login_user, logout_user, login_required, current_user from vfinance.extensions import db from vfinance.forms import LoginForm, RegisterForm from vfinance.models import User from vfinance.utils import redirect_back from sqlalchemy.exc import IntegrityError @auth_bp.route("/login", methods = ["GET", "POST"]) def login(): form = LoginForm() if current_user.is_authenticated: # flash('You are already loging','success') return redirect(url_for("home.index")) if form.validate_on_submit(): username = form.username.data password = form.password.data remember = form.remember.data users = User.query.all() for user in users: if username in user.username: if user.validate_password(password): login_user(user,remember) flash('Welcome Back '+user.username, 'info') return redirect_back() # flash("Invalid username or password.", 'warning') else: flash("Invalid username or password.", 'warning') ``` #### vfinance/templates/auth/login.html ``` {% extends 'base.html' %} {% from 'bootstrap/form.html' import render_form %} {% block title %}Login{% endblock %} {% block content %} <div class="container h-100"> <div class="row h-100 page-header justify-content-center align-items-center"> <h1>Log in</h1> </div> <div class="row h-100 justify-content-center align-items-center"> {{ render_form(form, extra_classes='col-6') }} </div> </div> {% endblock %} {% block footer %}{% endblock %} ``` ### 註冊 ``` @auth_bp.route("/register", methods = ["GET", "POST"]) def register(): form = RegisterForm() if form.validate_on_submit(): username = form.username.data password = form.password.data cash = 1000000 user = User( username = username, cash = cash, position = 0 ) user.set_password(password) db.session.add(user) try: db.session.commit() flash("Thanks for registering! Please login!", 'success' ) except IntegrityError: flash("Account already exists", 'danger') db.session.rollback() return redirect_back() return redirect(url_for('home.index')) return render_template("auth/register.html", form = form) ``` #### #### vfinance/templates/auth/register.html ``` {% extends "base.html" %} {% from 'bootstrap/form.html' import render_form %} {% block title %} Register {% endblock title %} {% block content%} <div class="container h-100"> <div class="row h-100 page-header justify-content-center align-items-center"> <h1>Register</h1> </div> <div class="row h-100 justify-content-center align-items-center"> {{ render_form(form, extra_classes='col-6') }} </div> </div> {% endblock %} {% block footer %} {% endblock footer %} ``` ### 登出 ``` @auth_bp.route('/logout') @login_required def logout(): logout_user() flash('Logout success.', 'info') return redirect_back() ``` ## 視圖函數保護 程式許多操作需要登錄後才能進行,因此要將那些需要登入後才能使用的funciton保護起來,如果用戶在未登錄狀態下訪問了某個需要登錄才能使用的資源,程式會重新定位到登入頁面 視圖保護可以用Flask-Login提供的login_required裝飾器,例如登出函數: ``` from flask_login import login_required @auth_bp.route('/logout') @login_required def logout(): logout_user() flash('Logout success.', 'info') return redirect_back() ``` 但在這之前還需要在extension.py腳本中使用login_manager對象的login_view屬性設置登錄試圖的端點值: ``` login_manager.login_view = 'auth.login' login_manager.login_message = 'Helloooo' login_manager.login_message_category = 'warning' ``` 這樣一來當在未登錄狀態下訪問需要登錄的頁面時,程式就會自動導到登錄頁面,並顯示提示消息。