flask-login原理詳解
最近發現專案中使用的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>login
<!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>index.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_viewlogin_required
流程解析
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中。