文章轉自 :https://github.com/WapeYang/The-Flask-Mega-Tutorial/blob/master/userlogin.rst
感謝原作者的付出
轉載時間爲:2014-05-06
用戶登錄
回顧
在上一章中,我們已經創建了數據庫以及學會了使用它來存儲用戶以及 blog,但是我們並沒有把它融入我們的應用程序中。在兩章以前,我們已經看到如何創建表單並且留下了一個完全實現的登錄表單。
在本章中我們將會建立 web 表單和數據庫的聯繫,並且編寫我們的登錄系統。在本章結尾的時候,我們這個小型的應用程序將能夠註冊新用戶並且能夠登入和登出。
我們接下來講述的正是我們上一章離開的地方,所以你可能要確保應用程序 microblog 正確地安裝和工作。
配置
像以前章節一樣,我們從配置將會使用到的 Flask 擴展開始入手。對於登錄系統,我們將會使用到兩個擴展,Flask-Login 和 Flask-OpenID。配置情況如下(文件 app__init__.py):
import os from flask.ext.login import LoginManager from flask.ext.openid import OpenID from config import basedir lm = LoginManager() lm.init_app(app) oid = OpenID(app, os.path.join(basedir, 'tmp'))
Flask-OpenID 擴展需要一個存儲文件的臨時文件夾的路徑。對此,我們提供了一個 tmp 文件夾的路徑。
重構用戶模型
Flask-Login 擴展需要在我們的 User 類中實現一些特定的方法。但是類如何去實現這些方法卻沒有什麼要求。
下面就是我們爲 Flask-Login 實現的 User 類(文件 app/models.py):
class User(db.Model): id = db.Column(db.Integer, primary_key = True) nickname = db.Column(db.String(64), unique = True) email = db.Column(db.String(120), unique = True) role = db.Column(db.SmallInteger, default = ROLE_USER) posts = db.relationship('Post', backref = 'author', lazy = 'dynamic') def is_authenticated(self): return True def is_active(self): return True def is_anonymous(self): return False def get_id(self): return unicode(self.id) def __repr__(self): return '<User %r>' % (self.nickname)
is_authenticated 方法有一個具有迷惑性的名稱。一般而言,這個方法應該只返回 True,除非表示用戶的對象因爲某些原因不允許被認證。
is_active 方法應該返回 True,除非是用戶是無效的,比如因爲他們的賬號是被禁止。
is_anonymous 方法應該返回 True,除非是僞造的用戶不允許登錄系統。
最後,get_id 方法應該返回一個用戶唯一的標識符,以 unicode 格式。我們使用數據庫生成的唯一的 id。
user_loader 回調
現在我們已經準備好用 Flask-Login 和 Flask-OpenID 擴展來開始實現登錄系統。
首先,我們必須編寫一個函數用於從數據庫加載用戶。這個函數將會被 Flask-Login 使用(文件 app/views.py):
@lm.user_loader def load_user(id): return User.query.get(int(id))
請注意在 Flask-Login 中的用戶 ids 永遠是 unicode 字符串,因此在我們把 id 發送給 Flask-SQLAlchemy 之前,把 id 轉成整型是必須的,否則會報錯!
登錄視圖函數
接下來我們需要更新我們的登錄視圖函數(文件 app/views.py):
from flask import render_template, flash, redirect, session, url_for, request, g from flask.ext.login import login_user, logout_user, current_user, login_required from app import app, db, lm, oid from forms import LoginForm from models import User, ROLE_USER, ROLE_ADMIN @app.route('/login', methods = ['GET', 'POST']) @oid.loginhandler def login(): if g.user is not None and g.user.is_authenticated(): return redirect(url_for('index')) form = LoginForm() if form.validate_on_submit(): session['remember_me'] = form.remember_me.data return oid.try_login(form.openid.data, ask_for = ['nickname', 'email']) return render_template('login.html', title = 'Sign In', form = form, providers = app.config['OPENID_PROVIDERS'])
注意我們這裏導入了不少新的模塊,一些模塊我們將會在不久後使用到。
跟之前的版本的改動是非常小的。我們在視圖函數上添加一個新的裝飾器。oid.loginhandle 告訴 Flask-OpenID 這是我們的登錄視圖函數。
在函數開始的時候,我們檢查 g.user 是否被設置成一個認證用戶,如果是的話將會被重定向到首頁。這裏的想法是如果是一個已經登錄的用戶的話,就不需要二次登錄了。
Flask 中的 g 全局變量是一個在請求生命週期中用來存儲和共享數據。我敢肯定你猜到了,我們將登錄的用戶存儲在這裏(g)。
我們在 redirect 調用中使用的 url_for 函數是定義在 Flask 中,以一種乾淨的方式爲一個給定的視圖函數獲取 URL。如果你想要重定向到首頁你可能會經常使用 redirect('/index'),但是有很多 好理由 讓 Flask 爲你構建 URLs。
當我們從登錄表單獲取的數據後的處理代碼也是新的。這裏我們做了兩件事。首先,我們把 remember_me 布爾值存儲到 flask 的會話中,這裏別與 Flask-SQLAlchemy 中的 db.session 弄混淆。之前我們已經知道 flask.g 對象在請求整個生命週期中存儲和共享數據。flask.session 提供了一個更加複雜的服務對於存儲和共享數據。一旦數據存儲在會話對象中,在來自同一客戶端的現在和任何以後的請求都是可用的。數據保持在會話中直到會話被明確地刪除。爲了實現這個,Flask 爲我們應用程序中每一個客戶端設置不同的會話文件。
在接下來的代碼行中,oid.try_login 被調用是爲了觸發用戶使用 Flask-OpenID 認證。該函數有兩個參數,用戶在 web 表單提供的openid 以及我們從 OpenID 提供商得到的數據項列表。因爲我們已經在用戶模型類中定義了 nickname 和 email,這也是我們將要從 OpenID 提供商索取的。
OpenID 認證異步發生。如果認證成功的話,Flask-OpenID 將會調用一個註冊了 oid.after_login 裝飾器的函數。如果失敗的話,用戶將會回到登陸頁面。
Flask-OpenID 登錄回調
這裏就是我們的 after_login 函數的實現(文件 app/views.py):
@oid.after_login def after_login(resp): if resp.email is None or resp.email == "": flash('Invalid login. Please try again.') return redirect(url_for('login')) user = User.query.filter_by(email = resp.email).first() if user is None: nickname = resp.nickname if nickname is None or nickname == "": nickname = resp.email.split('@')[0] user = User(nickname = nickname, email = resp.email, role = ROLE_USER) db.session.add(user) db.session.commit() remember_me = False if 'remember_me' in session: remember_me = session['remember_me'] session.pop('remember_me', None) login_user(user, remember = remember_me) return redirect(request.args.get('next') or url_for('index'))
resp 參數傳入給 after_login 函數,它包含了從 OpenID 提供商返回來的信息。
第一個 if 只是爲了驗證。我們需要一個合法的郵箱地址,因此提供郵箱地址是不能登錄的。
接下來,我們從數據庫中搜索郵箱地址。如果郵箱地址不在數據庫中,我們認爲是一個新用戶,因爲我們會添加一個新用戶到數據庫。注意例子中我們處理空的或者沒有提供的 nickname 方式,因爲一些 OpenID 提供商可能沒有它的信息。
接着,我們從 flask 會話中加載 remember_me 值,這是一個布爾值,我們在登錄視圖函數中存儲的。
然後,爲了註冊這個有效的登錄,我們調用 Flask-Login 的 login_user 函數。
最後,如果在 next 頁沒有提供的情況下,我們會重定向到首頁,否則會重定向到 next 頁。
如果要讓這些都起作用的話,Flask-Login 需要知道哪個視圖允許用戶登錄。我們在應用程序模塊初始化中配置(文件app/__init__.py):
lm = LoginManager() lm.init_app(app) lm.login_view = 'login'
全局變量 g.user
如果你觀察仔細的話,你會記得在登錄視圖函數中我們檢查 g.user 爲了決定用戶是否已經登錄。爲了實現這個我們用 Flask 的before_request 裝飾器。任何使用了 before_request 裝飾器的函數在接收請求之前都會運行。 因此這就是我們設置我們 g.user 的地方(文件 app/views.py):
@app.before_request def before_request(): g.user = current_user
這就是所有需要做的。全局變量 current_user 是被 Flask-Login 設置的,因此我們只需要把它賦給 g.user ,讓訪問起來更方便。有了這個,所有請求將會訪問到登錄用戶,即使在模版裏。
首頁視圖
在前面的章節中,我們的 index 視圖函數使用了僞造的對象,因爲那時候我們並沒有用戶或者 blog。好了,現在我們有用戶了,讓我們使用它:
@app.route('/') @app.route('/index') @login_required def index(): user = g.user posts = [ { 'author': { 'nickname': 'John' }, 'body': 'Beautiful day in Portland!' }, { 'author': { 'nickname': 'Susan' }, 'body': 'The Avengers movie was so cool!' } ] return render_template('index.html', title = 'Home', user = user, posts = posts)
上面僅僅只有兩處變化。首先,我們添加了 login_required 裝飾器。這確保了這頁只被已經登錄的用戶看到。
另外一個變化就是我們把 g.user 傳入給模版,代替之前使用的僞造對象。
這是運行應用程序最好的時候了!
登出
我們已經實現了登錄,現在是時候增加登出的功能。
登出的視圖函數是相當地簡單(文件 app/views.py):
@app.route('/logout') def logout(): logout_user() return redirect(url_for('index'))
但是我們還沒有在模版中添加登出的鏈接。我們將要把這個鏈接放在基礎層中的導航欄裏(文件 app/templates/base.html):
<html> <head> {% if title %} <title>{{title}} - microblog</title> {% else %} <title>microblog</title> {% endif %} </head> <body> <div>Microblog: <a href="{{ url_for('index') }}">Home</a> {% if g.user.is_authenticated() %} | <a href="{{ url_for('logout') }}">Logout</a> {% endif %} </div> <hr> {% with messages = get_flashed_messages() %} {% if messages %} <ul> {% for message in messages %} <li>{{ message }} </li> {% endfor %} </ul> {% endif %} {% endwith %} {% block content %}{% endblock %} </body> </html>
實現起來是不是很簡單?我們只需要檢查有效的用戶是否被設置到 g.user 以及是否我們已經添加了登出鏈接。我們也正好利用這個機會在模版中使用 url_for。
結束語
我們現在已經有一個完全實現的登錄系統。在下一章中,我們將會創建用戶信息頁以及將會顯示用戶頭像。
如果你想要節省時間的話,你可以下載 microblog-0.5.zip。