英文博客地址:http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
中文翻譯地址:http://www.pythondoc.com/flask-mega-tutorial/userlogin.html
開源中國社區:http://www.oschina.net/translate/the-flask-mega-tutorial-part-v-user-logins
備注:我是三個一起看的,有些部分的中文翻譯太拗口而且還有錯,因此我選擇是比較清晰的中文解釋,而有些部分是直接翻譯英文博客。
上一部分: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))
備注:其實我現在對python中的@符號的用法還是不甚明了。
注意在 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 @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這是我們的登錄視圖函數。
在函數開始的時候我們就檢查 g.user 是不是一個已經認證的用戶,如果已經認證就直接跳轉到主頁面,避免二次登錄。
Flask 中的 g 全局變量是一個在請求生命周期中用來存儲和共享數據。登錄的用戶存儲在這里(g)。
我們在調用redirect()時使用的url_for()方法是Flask定義的從給定的view方法獲取url。如果你想重定向到index頁面,你很可能使用redirect('/index'),但是讓Flask為你構造url是有好處的。見 http://dormousehole.readthedocs.org/en/latest/quickstart.html#url
當我們從登錄表單得到返回數據,接下來要運行的代碼也是新寫的。這兒我們做兩件事。首先我們保存remember_me的布爾值到Flask的 session中,別和Flask-SQLAlchemy的db.session混淆了。之前我們已經知道 flask.g 對象在請求整個生命周期中存儲和共享數據。flask.session 提供了一個更加復雜的服務對於存儲和共享數據。一旦數據存儲在會話對象中,在來自同一客戶端的現在和任何以后的請求都是可用的。數據將保持在session中直到被明確的移除。為了做到這些,Flask為每個客戶端建立各自的 session。
oid.try_login通過Flask-OpenID來執行用戶認證。該函數有兩個參數,用戶在 web 表單提供的 openid 以及我們從 OpenID 提供商得到的數據項列表。因為我們已經在用戶模型類中定義了 nickname 和 email,這也是我們將要從 OpenID 提供商索取的。
基於OpenID的認證是異步的。如果認證成功,Flask-OpenID將調用有由oid.after_login裝飾器注冊的方法。如果認證失敗那么用戶會被重定向到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) 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 僅僅是為了驗證。我們要求一個有效的email,所以如果不提供email,我們是沒法讓用戶登錄的。
接下來,我們將根據email查找數據庫。如果email沒有被找到我們就認為這是一個新的用戶,所以我們將在數據庫中增加一個新用戶,做法就像我們從之前章節學到的一樣。注意我們沒有處理nickname,因為一些OpenID provider並沒有包含這個信息。
接着我們將從Flask session中獲取remember_me的值,如果它存在,那它是我們之前在login view方法中保存到session中的boolean類型的值。
然后,為了注冊這個有效的登錄,我們調用 Flask-Login 的 login_user 函數。
最后,如果在 next 頁沒有提供的情況下,我們會重定向到首頁,否則會重定向到 next 頁。
跳轉到下一頁的這個概念很簡單。比方說我們需要你登錄才能導航到一個頁面,但你現在並未登錄。在Flask-Login中你可以通過 login_required裝飾器來限定未登錄用戶。如果一個用戶想連接到一個限定的url,那么他將被自動的重定向到login頁面。Flask- Login將保存最初的url作為下一個頁面,一旦登錄完成我們便跳轉到這個頁面。
做這個工作Flask-Login需要知道用戶當前在那個頁面。我們可以在app的初始化組件里配置它(文件 app/__init__.py):
lm = LoginManager() lm.init_app(app) lm.login_view = 'login'
備注:在修改(文件 app/__init__.py)的時候 “from app import views, models” 這句話需要放到最后,否則會報錯,找不到 lm。
lm = LoginManager() lm.init_app(app) lm.login_view = 'login' oid = OpenID(app, os.path.join(basedir, 'tmp')) from app import views, models
六、全局變量g.user
在login view方法中我們通過檢查g.user來判斷一個用戶是否登錄了。為了實現這個我們將使用Flask提供的before_request事件。
任何一個被before_request裝飾器裝飾的方法將會在每次request請求被收到時提前與view方法執行。
在這兒來設置我們的g.user變量(文件 app/views.py):
@app.before_request def before_request(): g.user = current_user
七、index視圖
之前的index視圖是不適合現在的,修改如下(文件 app/views.py):
@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://127.0.0.1:5000/ 會被重定向到登錄頁面
備注:我用的是yahoo的OpenID登錄的,要用OpenID,你得先激活yahoo的OpenID,激活方法自行搜索,這里不贅述了。
我的登錄時間有點長,這是我登錄后的主頁。
登錄后沒有登出之前你是沒辦法再回到登錄頁面的,它自動重定向回來。
八、用戶登出
登出的視圖函數是相當地簡單(文件 app/views.py):
@app.route('/logout') def logout(): logout_user() return redirect(url_for('index'))
但我們在模板中還沒有注銷登錄的鏈接。我們將在base.html中的頂部導航欄添加這個鏈接(文件 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>
修改后頁面: