Flask-Login詳解


Flask-Login詳解

關於Flask登錄認證的詳細過程請參見拙作<<使用Flask實現用戶登陸認證的詳細過程>>一文,而本文則偏重於詳細介紹Flask-Login的原理,代碼的解析。

首次登陸

我們首先來看一下首次登錄驗證的流程圖:

 
首次登陸

Flask-Login在登錄過程中主要負責:

  • 將用戶對象存入request context中
  • 將用戶ID,Session ID等信息存入Session中
    <<使用Flask實現用戶登陸認證的詳細過程>>中我們已經介紹過如何通過Flask-Login來實現登錄的過程,其中最重要的代碼就是login_user,如下:
login_user(user, remember=remember_me)

那么login_user具體做了什么呢?我們來看下源碼

def login_user(user, remember=False, force=False, fresh=True): if not force and not user.is_active: return False user_id = getattr(user, current_app.login_manager.id_attribute)() session['user_id'] = user_id session['_fresh'] = fresh session['_id'] = current_app.login_manager._session_identifier_generator() if remember: session['remember'] = 'set' _request_ctx_stack.top.user = user user_logged_in.send(current_app._get_current_object(), user=_get_user()) return True 
  • getattr(user, current_app.login_manager.id_attribute)() 這里login_manager.id_attribute是一個字符串'get_id'。因此這句的意思是獲取User對象的get_id method,然后執行,從而獲取到用戶的ID
  • 通過session['user_id'] = user_id來將用戶的ID存儲進Session當中,后面緊跟着將fresh信息,session id信息,remember信息存儲進session。

注意:Flask的session是以cookie為基礎,但是是在Server端使用secret key並使用AES之類的對稱加密算法進行加密的,然后將加密后的cookie發送給客戶端。由於是加密后的數據,客戶端無法篡改數據,也無法獲知session中的信息,只能保存該session信息,在之后的請求中攜帶該session信息

  • _request_ctx_stack.top.user = user這里是將user對象存儲進當前的request context中,_request_ctx_stack是一個LocalStack對象,top屬性指向的就是當前的request context。關於LocalStack及相關技術,請參考拙作<<Werkzeug(Flask)之Local、LocalStack和LocalProxy>>
  • user_logged_in.send(current_app._get_current_object(), user=_get_user()) 此句中user_logged_in是Flask-Login定義的signal,此處通過send來發射此signal,當注冊監聽此signal的回調函數收到此signal之后就會執行函數。這里send有兩個參數,第一個參數是sender對象,此處通過current_app._get_current_object()來獲取當前的app對象,即此signal的sender設為當前的應用;第二個參數是該signal攜帶的數據,此處將user對象做為signal的數據傳遞給相應的回調函數。關於signal的詳細解釋請參考拙作<<Flask Signals詳解>>

非首次登陸

非首次登陸流程圖如下:

 
非首次登陸

在這個流程圖中,Flask-Login主要起如下作用:

  1. 從session中獲取用戶ID
  2. 當用戶的請求訪問的是受登錄保護的路由時,就要通過用戶ID重新load user,如果load user失敗則進入鑒權失敗處理流程,如果成功,則允許正常處理請求
    那么Flask-Login究竟是如何保護路由的呢?我們來看個例子:
@app.route('/') @app.route('/main') @login_required def main(): return render_template( 'main.html', username=current_user.username) 

我們看到只要給路由函數加一個@login_required裝飾器就可以了,那么這個裝飾器究竟是怎么做到的呢?來看下源碼:

# flask_login/utils.py def login_required(func): @wraps(func) def decorated_view(*args, **kwargs): # 如果request method為例外method,即在EXEMPT_METHODS中的method,可以不必鑒權 if request.method in EXEMPT_METHODS: return func(*args, **kwargs) # 如果_login_disabled為True則不必鑒權 elif 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 
  • 默認情況下只有OPTIONS method在EXEMPT_METHODS set中,而GET、PUT、POST等常見的methods都需要鑒權
  • _login_disabled默認為False
  • 正常鑒權的關鍵在於current_user.is_authenticated是否為True,為True則正常處理請求,為False則進入unauthorized處理流程。那么這個current_user到底怎么就能鑒權了?它是怎么來的呢?來看下定義:
# flask_login/utils.py current_user = LocalProxy(lambda: _get_user()) 

原來current_user是一個LocalProxy對象,其代理的對象需要通過_get_user()來獲取,簡單來說_get_user()會返回兩種用戶,一種是正常的用戶對象(鑒權成功),一種是anonymous用戶對象(鑒權失敗)。而正常的用戶對象其is_authenticated屬性總是為True,相對的anonymous用戶對象的is_authenticated屬性總是為False

LocalProxy對象每次操作都會重新獲取代理的對象從而實現動態更新,關於LocalProxy的詳細說明請參考拙作<<Werkzeug(Flask)之Local、LocalStack和LocalProxy>>

而要實現動態更新的關鍵就在於_get_user函數,接下來我們看下_get_user函數是如何獲取user對象的:

# flask_login/utils.py 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) 

在之前的首次登陸那小節中,我們已經知道用戶鑒權成功后,會將User對象保存在當前的request context當中,這時我們調用_get_user函數時就會直接從request context中獲取user對象return getattr(_request_ctx_stack.top, 'user', None)
但如果是非首次登陸,當前request context中並沒有保存user對象,就需要調用current_app.login_manager._load_user()來去load user對象,接下來再看看如何去load:

# flask_login/login_manager.py def _load_user(self): '''Loads user from session or remember_me cookie as applicable''' user_accessed.send(current_app._get_current_object()) # first check SESSION_PROTECTION config = current_app.config if config.get('SESSION_PROTECTION', self.session_protection): deleted = self._session_protection() if deleted: return self.reload_user() # If a remember cookie is set, and the session is not, move the # cookie user ID to the session. # # However, the session may have been set if the user has been # logged out on this request, 'remember' would be set to clear, # so we should check for that and not restore the session. is_missing_user_id = 'user_id' not in session if is_missing_user_id: cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME) header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME) has_cookie = (cookie_name in request.cookies and session.get('remember') != 'clear') if has_cookie: return self._load_from_cookie(request.cookies[cookie_name]) elif self.request_callback: return self._load_from_request(request) elif header_name in request.headers: return self._load_from_header(request.headers[header_name]) return self.reload_user() 
  • _load_user大體的過程是首先檢查SESSION_PROTECTION設置,如果SESSION_PROTECTION 為strong或者basic類型,那么就會執行_session_protection()動作,否則不執行此操作。_session_protection在session_id不一致的時候(比如IP變化會導致session id的變化)才真正有用,這時,如果為basic類型或者session permanent為True時,只標注session為非新鮮的(not fresh);而如果為strong,則會刪除session中的用戶信息,並重新load user,即調用reload_user

session permanent為True時,用戶退出瀏覽器不會刪除session,其會保留permanent_session_lifetime s(默認是31天),但是當其為False且SESSION_PROTECTION 設為strong時,用戶的session就會被刪除。

  • 接下來的代碼是說當session中沒有用戶信息時(這里通過是否能獲取到user_id來判斷),如果有則直接reload_user,如果沒有,則有三種方式來load user,一種是通過remember cookie,一種通過request,一種是通過request header,依次嘗試。

remember cookie是指,當用戶勾選'remember me'復選框時,Flask-Login會將用戶信息放入到指定的cookie當中,同樣也是加密的。這就是為什么當session中沒有攜帶用戶信息時,我們可以通過remember cookie來獲取用戶的信息

reload_user是如何獲取用戶的呢,來看下源代碼:

# flask_login/login_manager.py 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: # 當無法獲取到有效的用戶id時,就認為是anonymous user ctx.user = self.anonymous_user() else: # user callback就是我們通過@login_manager.user_loader裝飾的函數,用於獲取user object 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 
  • 首先獲取user id,如果獲取不到有效的id,就將user設為anonymous user
  • 獲取到id后,再通過@login_manager.user_loader裝飾的函數獲取到user對象,如果沒有獲取到有效的user對象,就認為是anonymous user
  • 最后將user保存於request context中(無論是正常的用戶還是anonymous用戶)

至此,我們已經將Flask-Login的核心代碼剖析了一遍,如果你有收獲,不妨點個贊鼓勵一下吧!

 

flask_login模塊中user_loader裝飾器引發的思考

今天看書遇到了flask login模塊中的信號機制,看到user_loader這個裝飾器時有些疑惑,為什么需要這個裝飾器呢,先看一下源碼:

 
def user_loader(self, callback):
    '''
    This sets the callback for reloading a user from the session. The
    function you set should take a user ID (a ``unicode``) and return a user object, or ``None`` if the user does not exist. :param callback: The callback for retrieving a user object. :type callback: callable ''' self.user_callback = callback return callback 

看到這不禁疑惑,它的作用只是將被它包裝的函數存到self.user_callback這個屬性中去

那么在哪里用到了它呢,我在login_manager.py中查找,發現只有一個方法使用到了這個熟悉,這個方法是reload_user():

它先從請求上下文中取出最新的請求,如果沒有傳入user,那么會從session中試圖取出對應的user_id,這是一種保護機制,不使用cookie,而使用session,user_id在login時會寫入session,如果登陸時remember參數傳入了True,那么關閉瀏覽器重新打開后session['user_id']將不會被清除,這時候也就可以獲取到了,如果登陸時沒有設置remember為True,那么關閉瀏覽器后user_id會被設為None,則ctx.user = self.anonymous_user(),棧頂的用戶為匿名用戶,也就需要重新登陸了;取出了user_id,並且self.user_callback不為空,則會調用被user_loader裝飾的函數,並傳入user_id,在被裝飾的函數中我們要根據這個user_id來查找並返回對應的用戶實例,如果成功返回,那么當前請求上下文棧頂的用戶就設置為返回的用戶。
你可能會問,為什么要重載用戶呢?因為http協議是無狀態的,每次都會發送一個新的請求,請求上下文的棧頂會被新的請求覆蓋,對應的user屬性也就沒了,所以需要通過reload_user重載上一次記錄在session中並且未被清除的用戶,重載失敗則需要重新登陸,這也就是這個裝飾器的作用了。

 

最后我們看下logout_user()這個方法:

def logout_user():

''' Logs a user out. (You do not need to pass the actual user.) This will also clean up the remember me cookie if it exists. ''' user = _get_user() if 'user_id' in session: session.pop('user_id') if '_fresh' in session: session.pop('_fresh') cookie_name = current_app.config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME) if cookie_name in request.cookies: session['remember'] = 'clear' if 'remember_seconds' in session: session.pop('remember_seconds') user_logged_out.send(current_app._get_current_object(), user=user) current_app.login_manager.reload_user() return True 

logout主要是清除了session和cookie中的關鍵參數,比如login時設置的user_id以及remember等,清除后又調用了reload_user(),根據之前的邏輯,當然不可能重載成功,因為user_id已經為None了,執行到ctx.user = self.anonymous_user()就已經結束了,其實reload_user算是這個模塊中很關鍵的一個函數,login_manager這個類也是這個模塊的核心所在,以后有時間繼續研究。

關於Flask-Login中session失效時間的處理

 

最近需要使用Python開發web系統,主要用到的框架就是Flask,前端使用Jinja2模板引擎和Bootstrap,web容器使用Cherrypy,其中關於Login管理的使用了Flask-Login插件。

基本上也是從零學起,前前后后花了有好幾個月的時間,還是在借鑒了已有的一些項目基礎上。在開發的過程中有很多的想法和體會,記錄下來,有不對的地方歡迎大家指正。

在處理登錄管理的部分,在 Flask-Login 中,如果你不特殊處理的話,session 是在你關閉瀏覽器之后失效的,而如果不關閉瀏覽器的話,失效的時間據說是1年,還是1個月,這個地方沒看到官方說法,總之是很長,在某些業務場景下這樣的處理方式是不能接受的。由於系統的使用者提出了新的需求類似平時的SSO處理機制,大概無任何操作一二十分鍾就提示需要再次登錄,這樣的要求是合理的,之前也沒有太注意這個方面的時間,所以就需要回過頭來研究Flask-Login的session失效時間和設置問題。以前的登錄部分代碼:

 
 1 @app.route('/login', methods=['GET', 'POST'])
 2 def login():
 3     if request.method == 'GET':
 4         return render_template('login.html')
 5 
 6     username = request.form['username']
 7     password = request.form['password']
 8     user = User.get_user(username, password)
 9     if not user:
10         flash('Username or Password is invalid.', 'error')
11         return redirect(url_for('.login'))
12     login_user(user)
13 
14     # flash('Logged in successfully', "info")
15 
16     return render_template('index.html')
 

其中的User是models.py中定義的class,可以用於驗證用戶的合法性。在views部分通過@login_required控制,這樣可以成功驗證用戶並跳轉到相應頁面,但是之前提到的session失效問題默認是很長時間而且一旦關閉瀏覽器即失效。那么如何設置能夠實現制定時間內session失效,比如10minutes,查了一下文檔竟然沒發現說到這個問題,比如這個網站:http://www.pythondoc.com/flask-login/應該是正規的文檔吧,接着查一些論壇,很多人討論這個問題,有說設置REMEMBER_COOKIE_DURATION這個參數的,但是仔細看了這個參數和要解決的問題不是一個問題,接着就想到去stackoverflow上查一下吧,就搜索了一下關鍵詞,找了好幾個問答,其中有說到參數permanent_session_lifetime,

我覺得應該是這個差不多了,接着就這個關鍵詞再次搜索,果然發現了一些很有參考價值問答,其中還提到session.permanent = True這個參數,於是在代碼里進行設置:
from flask import session
from datetime import timedelta

session.permanent = True
app.permanent_session_lifetime = timedelta(minutes=10) 

其中兩個參數意義也比較好理解,第一個為設置session為永久的,第二個再來制定具體時間

但是,關鍵是但是測試了半天貌似不起作用,又查了半天中文的一些論壇,不知原因何在,然后又是在stackoverflow上有人說這個設置不能放在request外面,要放在request里面才能生效,比如這樣:

 
 1 from flask import session
 2 from datetime import timedelta
 3 
 4 @app.route('/login', methods=['GET', 'POST'])
 5 def login():
 6     if request.method == 'GET':
 7         return render_template('login.html')
 8 
 9     username = request.form['username']
10     password = request.form['password']
11     user = User.get_user(username, password)
12     if not user:
13         flash('Username or Password is invalid.', 'error')
14         return redirect(url_for('.login'))
15     login_user(user)
16     session.permanent = True
17     app.permanent_session_lifetime = timedelta(minutes=10)
18     # flash('Logged in successfully', "info")
19 
20     return render_template('index.html')
 

再去測試,已經可以成功按設置的時間來提示登錄了。

這個問題也許對於一些人來不算問題,但是從我自己解決這個問題來看,目前我們的一些文檔,問答還是有很多欠缺的地方,論壇上往往對一個問題互相轉載,很少有獨立的思考和實踐的代碼sample讓初學者參考,在此再次推薦推薦大家一個問答網站:stackoverflow,相信很多人也都在用這個,能夠得到很多有價值的問答,尤其是在國內還不是用的特別廣泛的技術。

對於這個問題,目前也是能使用的階段,也沒有再去研究Flask-Login的一些原理,對於技術只有實踐了你才有可能真正掌握,或者只能停留在理論階段,希望以后有機會再深入研究實踐一下。

相關文章:

flask-login 源碼解析

Flask-Login 使用和進階


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM