回顧
在前面的系列章節中,我們創建了一個數據庫并且學著用用戶和郵件來填充,但是到現在我們還沒能夠植入到我們的程序中。 兩章之前,我們已經看到怎么去創建網絡表單并且留下了一個實現完全的登陸表單。
在這篇文章中,我們將基于我門所學的網絡表單和數據庫來構建并實現我們自己的用戶登錄系統。教程的最后我們小程序會實現新用戶注冊,登陸和退出的功能。
為了能跟上這章節,你需要前一章節最后部分,我們留下的微博程序。請確保你的程序已經正確安裝和運行。
在前面的章節,我們開始配置我們將要用到的Flask擴展。為了登錄系統,我們將使用兩個擴展,Flask-Login 和 Flask-OpenID. 配置如下所示 (fileapp\__init__.py):
1
2
3
4
5
6
7
8
|
import os from flaskext.login import LoginManager from flaskext.openid import OpenID from config import basedir lm = LoginManager() lm.setup_app(app) oid = OpenID(app, os.path.join(basedir, 'tmp' )) |
Flask-OpenID 擴展為了可以存儲臨時文件,需要一個臨時文件夾路徑。為此,我們提供了它的位置。
重訪我們的用戶模型
Flask-Login擴展需要在我們的User類里實現一些方法。除了這些方法以外,類沒有被要求實現其它方法。
下面是我們的User類 (fileapp/models.py):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 .name) |
is_authenticated方法是一個誤導性的名字的方法,通常這個方法應該返回True,除非對象代表一個由于某種原因沒有被認證的用戶。
is_active方法應該為用戶返回True除非用戶不是激活的,例如,他們已經被禁了。
is_anonymous方法應該為那些不被獲準登錄的用戶返回True。
最后,get_id方法為用戶返回唯一的unicode標識符。我們用數據庫層生成唯一的id。
用戶加載回調
現在我們通過使用Flask-Login和Flask-OpenID擴展來實現登錄系統
首先,我們需要寫一個方法從數據庫加載到一個用戶。這個方法會被Flask-Login使用(fileapp/views.py):
1
2
3
|
@lm .user_loader def load_user( id ): return User.query.get( int ( id )) |
記住Flask-Login里的user id一直是unicode類型的,所以在我們把id傳遞給Flask-SQLAlchemy時,有必要把它轉化成integer類型。
登錄視圖函數
接下來我們要更新登錄視圖函數(fileapp/views.py):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from flask import render_template, flash, redirect, session, url_for, request, g from flaskext.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.loginhandler。它告訴Flask-OpenID這是我們的登錄視圖函數。
在方法體的開頭,我們檢測是是否用戶是已經經過登錄認證的,如果是就重定向到index頁面。這兒的思路是如果一個用戶已經登錄了,那么我們不會讓它做二次登錄。
全局變量g是Flask設置的,在一個request生命周期中,用來存儲和共享數據的變量。所以我猜你已經想到了,我們將把已經登錄的用戶放到g變量里。
我們在調用redirect()時使用的url_for()方法是Flask定義的從給定的view方法獲取url。如果你想重定向到index頁面,你h很可能使用redirect('/index'),但是我們有很好的理由讓Flask為你構造url。
當我們從登錄表單得到返回數據,接下來要運行的代碼也是新寫的。這兒我們做兩件事。首先我們保存remember_me的布爾值到Flask的session中,別和Flask-SQLAlchemy的db.session混淆了。我們已經知道在一個request的生命周期中用Flask的g對象來保存和共享數據。沿著這條線路Flask的session提供了更多,更復雜的服務。一旦數據被保存到session中,它將在同一客戶端發起的這次請求和這次以后的請求中永存而不會消亡。數據將保持在session中直到被明確的移除。為了做到這些,Flask為每個客戶端建立各自的session。
下面的oid.try_login是通過Flask-OpenID來執行用戶認證。這個方法有兩個參數,web表單提供的openid和OpenID provider提供的我們想要的list數據項。由于我們定義了包含nickname和email的User類,所以我們要從找nickname和email這些項。
基于OpenID的認證是異步的。如果認證成功,Flask-OpenID將調用有由oid.after_login裝飾器注冊的方法。如果認證失敗那么用戶會被重定向到login頁面。
Flask-OpenID登錄回調
這是我們實現的after_login方法(app/views.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@oid .after_login def after_login(resp): if resp.email is None or resp.email = = "": flash( 'Invalid login. Please try again.' ) 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' )) |
傳給after_login方法的resp參數包含了OpenID provider返回的一些信息。
第一個if聲明僅僅是為了驗證。我們要求一個有效的email,所以一個沒有沒提供的email我們是沒法讓他登錄的。
接下來,我們將根據email查找數據庫。如果email沒有被找到我們就認為這是一個新的用戶,所以我們將在數據庫中增加一個新用戶,做法就像我們從之前章節學到的一樣。注意我們沒有處理nickname,因為一些OpenID provider并沒有包含這個信息。
做完這些我們將從Flask session中獲取remember_me的值,如果它存在,那它是我們之前在login view方法中保存到session中的boolean類型的值。
然后我們調用Flask-Login的login_user方法,來注冊這個有效的登錄。
最后,在最后一行我們重定向到下一個頁面,或者如果在request請求中沒有提供下個頁面時,我們將重定向到index頁面。
跳轉到下一頁的這個概念很簡單。比方說我們需要你登錄才能導航到一個頁面,但你現在并未登錄。在Flask-Login中你可以通過login_required裝飾器來限定未登錄用戶。如果一個用戶想連接到一個限定的url,那么他將被自動的重定向到login頁面。Flask-Login將保存最初的url作為下一個頁面,一旦登錄完成我們便跳轉到這個頁面。
做這個工作Flask-Login需要知道用戶當前在那個頁面。我們可以在app的初始化組件里配置它(app/__init__.py):
1
2
3
|
lm = LoginManager() lm.setup_app(app) lm.login_view = 'login' |
全局變量g.user
如果你注意力很集中,那么你應該記得在login view方法中我們通過檢查g.user來判斷一個用戶是否登錄了。為了實現這個我們將使用Flask提供的before_request事件。任何一個被before_request裝飾器裝飾的方法將會在每次request請求被收到時提前與view方法執行。所以在這兒來設置我們的g.user變量(app/views.py):
1
2
3
|
@app .before_request def before_request(): g.user = current_user |
這就是它要做的一切,current_user全局變量是被Flask-Login設定的,所以我們只需要把它拷貝到更容易被訪問的g變量就OK了。這樣,所有的請求都能訪問這個登錄的用戶,甚至于內部的模板。
index視圖
在之前的章節中我們用假代碼遺留了我們的index視圖,因為那個時候我們系統里并沒有用戶和博客文章。現在我們有用戶了,所以,讓我們來完成它吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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傳給了模板,替換了之間的假對象。
現在可以運行我們的應用了。
當我們連接到http://localhost:5000你將會看到登陸頁面。記著如果你通過OpenID登錄那么你必須使用你的提供者提供的OpenID URL。你可以下面URL中的任何一個OpenID provider來為你產生一個正確的URL。
作為登錄進程的一部分,你將會被重定向到OpenID提供商的網站,你將在那兒認證和授權你共享給我們應用的一些信息(我們只需要email和nickname,放心,不會有任何密碼或者其他個人信息被曝光)。
一旦登錄完成你將作為已登錄用戶被帶到index頁面。
試試勾選remember_me復選框。有了這個選項當你在瀏覽器關閉應用后重新打開時,你還是已登錄狀態。
注銷登錄
我們已經實現了登錄,現在是時候來實現注銷登錄了。
注銷登錄的方法灰常簡單(file app/views.py):
1
2
3
4
|
@app .route( '/logout' ) def logout(): logout_user() return redirect(url_for( 'index' )) |
但我們在模板中還沒有注銷登錄的鏈接。我們將在base.html中的頂部導航欄添加這個鏈接(file app/templates/base.html):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<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 。