flask flask-login詳解(flask 54)

首次登陸

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

image

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

  • 將用戶對象存入request context中
  • 將用戶ID嫂易,Session ID等信息存入Session中
    <<使用Flask實現(xiàn)用戶登陸認證的詳細過程>>中我們已經(jīng)介紹過如何通過Flask-Login來實現(xiàn)登錄的過程鹃共,其中最重要的代碼就是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搬男,然后執(zhí)行,從而獲取到用戶的ID
  • 通過session['user_id'] = user_id來將用戶的ID存儲進Session當中拜银,后面緊跟著將fresh信息,session id信息遭垛,remember信息存儲進session尼桶。

注意:Flask的session是以cookie為基礎,但是是在Server端使用secret key并使用AES之類的對稱加密算法進行加密的,然后將加密后的cookie發(fā)送給客戶端。由于是加密后的數(shù)據(jù)忌卤,客戶端無法篡改數(shù)據(jù)存炮,也無法獲知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來發(fā)射此signal,當注冊監(jiān)聽此signal的回調函數(shù)收到此signal之后就會執(zhí)行函數(shù)犹赖。這里send有兩個參數(shù)队他,第一個參數(shù)是sender對象,此處通過current_app._get_current_object()來獲取當前的app對象峻村,即此signal的sender設為當前的應用麸折;第二個參數(shù)是該signal攜帶的數(shù)據(jù),此處將user對象做為signal的數(shù)據(jù)傳遞給相應的回調函數(shù)粘昨。關于signal的詳細解釋請參考拙作<<Flask Signals詳解>>

非首次登陸

非首次登陸流程圖如下:

image

在這個流程圖中垢啼,F(xiàn)lask-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)

我們看到只要給路由函數(shù)加一個@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對象每次操作都會重新獲取代理的對象從而實現(xiàn)動態(tài)更新椿息,關于LocalProxy的詳細說明請參考拙作<<Werkzeug(Flask)之Local歹袁、LocalStack和LocalProxy>>

而要實現(xiàn)動態(tài)更新的關鍵就在于_get_user函數(shù),接下來我們看下_get_user函數(shù)是如何獲取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)

在之前的首次登陸那小節(jié)中寝优,我們已經(jīng)知道用戶鑒權成功后条舔,會將User對象保存在當前的request context當中,這時我們調用_get_user函數(shù)時就會直接從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類型凄硼,那么就會執(zhí)行_session_protection()動作,否則不執(zhí)行此操作捷沸。_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'復選框時粱锐,F(xiàn)lask-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裝飾的函數(shù)恶座,用于獲取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裝飾的函數(shù)獲取到user對象,如果沒有獲取到有效的user對象沪袭,就認為是anonymous user
  • 最后將user保存于request context中(無論是正常的用戶還是anonymous用戶)

至此湾宙,我們已經(jīng)將Flask-Login的核心代碼剖析了一遍,如果你有收獲冈绊,不妨點個贊鼓勵一下吧侠鳄!

轉載:http://www.reibang.com/p/5ba6a956d504

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市死宣,隨后出現(xiàn)的幾起案子伟恶,更是在濱河造成了極大的恐慌,老刑警劉巖毅该,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件博秫,死亡現(xiàn)場離奇詭異,居然都是意外死亡眶掌,警方通過查閱死者的電腦和手機挡育,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來朴爬,“玉大人即寒,你說我怎么就攤上這事。” “怎么了母赵?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵逸爵,是天一觀的道長。 經(jīng)常有香客問我凹嘲,道長师倔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任周蹭,我火速辦了婚禮趋艘,結果婚禮上,老公的妹妹穿的比我還像新娘谷醉。我一直安慰自己致稀,他們只是感情好,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布俱尼。 她就那樣靜靜地躺著抖单,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遇八。 梳的紋絲不亂的頭發(fā)上矛绘,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機與錄音刃永,去河邊找鬼货矮。 笑死,一個胖子當著我的面吹牛斯够,可吹牛的內容都是我干的囚玫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼读规,長吁一口氣:“原來是場噩夢啊……” “哼抓督!你這毒婦竟也來了?” 一聲冷哼從身側響起束亏,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤铃在,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后碍遍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體定铜,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年怕敬,在試婚紗的時候發(fā)現(xiàn)自己被綠了揣炕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡东跪,死狀恐怖畸陡,靈堂內的尸體忽然破棺而出矮烹,到底是詐尸還是另有隱情,我是刑警寧澤罩锐,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站卤唉,受9級特大地震影響涩惑,放射性物質發(fā)生泄漏。R本人自食惡果不足惜桑驱,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一竭恬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熬的,春花似錦痊硕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至橡伞,卻和暖如春盒揉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背兑徘。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工刚盈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人挂脑。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓藕漱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親崭闲。 傳聞我的和親對象是個殘疾皇子肋联,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內容