最近發現項目中使用的flask-login中有些bug,直接使用官網的方式確實可以用,但僅僅是可以用,對於原理或解決問題沒有什么幫助,最近通過查看網上資料、分析源碼、通過demo、從零開始總結了flask-login的使用方式及內部實現原理。
先說使用,安裝組件就不說了,太簡單。
安裝好了之后就是把組件注冊到flask中,這個簡單說下,具體flask如何注冊這些擴展的原理后續再補上,引用官網的說法:登錄管理(login manager)包含了讓你的應用和 Flask-Login 協同工作的代碼,比如怎樣從一個 ID 加載用戶,當用戶需要登錄的時候跳轉到哪里等等。具體注冊代碼如下:。
# encoding:utf-8 from flask.ext.login import LoginManager app = Flask(__name__) login_manager = LoginManager() login_manager.init_app(app)
login_manager.login_view = "login" #配置如果需要登錄調整的路由
注冊好了之后,就可以使用了,以下是路由函數的寫法,具體login_required、login_user的實現原理后面會再說
@app.route('/') @login_required #進入首頁需要判斷用戶是否登入,沒有登錄則跳轉到注冊時配置的路由 def index(): return render_template('index.html') @app.route('/login', methods = ['POST', 'GET']) def login(): if request.method == 'POST': req = request.get_json() username = req['username'] userpassword = req['userpassward'] if auth_user(username, userpassword): #判斷用戶名密碼是否正確,這里隨便寫一個方法,寫死用戶名密碼就可以 login_user(User(14,'root', 'root')) #用戶名密碼驗證通過調用login_user把user注冊到請求上下文session中,這個session其實就是一個LocalStack return url_for('index') flash(u'無效的用戶名或密碼') return render_template('login.html')
還差一部分,創建models模塊,用處兩個,一個用來定義User類,二用來注冊回調函數,這個回調函數通過user_id返回User實例。這里只是想弄清楚login的原理所有沒用數據庫,盡量聚焦
from flask.ext.login import UserMixin from app import login_manager class User(UserMixin): __tablename__ = 'users' def __init__(self, id,password, username): ''' :param id: :param username: ''' self.id = id #這個屬性一定要有,否則自己要重寫get_id方法,不信自己去試下 self.password = password self.username = username def __repr__(self): return '<User %r>' % self.username @login_manager.user_loader def load_user(user_id): print user_id return User(14, 'root', 'root')
ok,基本使用完成了,前端再寫兩個簡單頁面就可以index.html和login.html。代碼如下,這里主要理解后端流程,前端能用就可以
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>hello, {{ name }}</title> </head> <body> <form name="myform"> <label>用戶名:</label> <input type="text" name="fname"/> <br> <label>密碼:</label> <input type="text" name="address"/> <button type="button" onclick="login()">提交</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> function login() { axios({ method: 'post', url: '/login', data: { username: myform.fname.value, userpassward: myform.address.value } }).then(function (response) { location.href = response.data }).catch(function (error) { console.log(error); }); } </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> ceshi <button type="button" onclick="login()">提交</button> </body> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> {# 發送用戶名、密碼#} function login() { axios({ method: 'post', url: '/test', data: { username: 'c', userpassward: 'y' } }).then(function (response) { console.log(response.data); }).catch(function (error) { console.log(error); }); } </script> </html>
具體使用過程梳理完成。
再看看其實現的原理,后端路由接收到前端請求后,會進入login_require裝飾器中,而此裝飾器用來判斷用戶鑒權情況,如下圖:

那用戶登錄鑒權的關鍵在裝飾器@login_required中,先看下整體的鑒權流程,再深入各個部分,下圖為鑒權的整體流程圖

此圖對應的代碼
def login_required(func): @wraps(func) def decorated_view(*args, **kwargs): if current_app.login_manager._login_disabled: return func(*args, **kwargs) elif not current_user.is_authenticated: return current_app.login_manager.unauthorized() return func(*args, **kwargs) return decorated_view
流程解析
1. 判斷請求是否需要鑒權,current_app.login_manager._login_disabled,通常的命令get、post等請求都需要鑒權,此屬性為False。
2. 判斷當前用戶是否鑒權,current_user.is_authenticated。current_user--------從哪獲取?繼續分析代碼
current_user = LocalProxy(lambda: _get_user())
current_user通過LoaclProxy創建了一個無名函數_get_user,為什么用lambda:_get_user()而不直接使用_get_user?我也沒想明白,有牛人可以解釋一下。
LoaclProxy具體可以參考https://www.jianshu.com/p/3f38b777a621,說白了就是理解為把方法地址傳給變量,以后可以動態調用代理方法
獲取current_user通過_get_user()函數,先看下該函數的代碼
def _get_user(): if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'): current_app.login_manager._load_user() return getattr(_request_ctx_stack.top, 'user', None)
解釋一下,首先需要知道請求上下文,代碼中的_request_ctx_stack.top中是否有user這個變量,如果沒有則_load_user,如果有直接取這個user屬性返回給前面的說用來鑒權使用。
_load_user會調用reload_user函數,
def reload_user(self, user=None): ctx = _request_ctx_stack.top if user is None: user_id = session.get('user_id') if user_id is None: ctx.user = self.anonymous_user() else: if self.user_callback is None: raise Exception( "No user_loader has been installed for this " "LoginManager. Add one with the " "'LoginManager.user_loader' decorator.") user = self.user_callback(user_id) if user is None: ctx.user = self.anonymous_user() else: ctx.user = user else: ctx.user = user
該函數判斷_request_ctx_stack.top賦值user對象,對象可以傳入,也可以使用我們在使用過程中定義的def load_user(user_id),這個user_callback就是我們定義的load_user函數。
獲取user如圖

登入時和重新請求時都會將user放入到棧中,每次請求后都出清理top中的user。放入top時會設置user_id到session中。
