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

JxBetter發表於2019-02-16

今天看書遇到了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_user這個登陸函式中去看看:

def login_user(user, remember=False, duration=None, 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`
        if duration is not None:
            try:
                # equal to timedelta.total_seconds() but works with Python 2.6
                session[`remember_seconds`] = (duration.microseconds +
                                               (duration.seconds +
                                                duration.days * 24 * 3600) *
                                               10**6) / 10.0**6
            except AttributeError:
                raise Exception(`duration must be a datetime.timedelta, `
                                `instead got: {0}`.format(duration))

    _request_ctx_stack.top.user = user
    user_logged_in.send(current_app._get_current_object(), user=_get_user())
    return True

可以看到,login_user這個函式接受user這個主要的引數,getattr(user, current_app.login_manager.id_attribute)()這句是為了呼叫user中的get_id方法

self.id_attribute = ID_ATTRIBUTE
ID_ATTRIBUTE = `get_id`

注意在getattr後面還有個()所以會呼叫對應的方法,所以user_id中就存放了登陸使用者的id號,並寫入到session中去,如果設定了remember為True的話,關掉瀏覽器重新開啟後,使用者不會退出,函式的最後_request_ctx_stack.top.user = user,將當前user加入到請求上下文的棧頂,就能用current_user獲取了。
上面說到self.user_callback已經存了被user_loader裝飾的函式,那麼在哪裡用到了它呢,我在login_manager.py中查詢,發現只有一個方法使用到了這個熟悉,這個方法是reload_user():

def reload_user(self, user=None):
    ```
    This set the ctx.user with the user object loaded by your customized
    user_loader callback function, which should retrieved the user object
    with the user_id got from session.

    Syntax example:
    from flask_login import LoginManager
    @login_manager.user_loader
    def any_valid_func_name(user_id):
        # get your user object using the given user_id,
        # if you use SQLAlchemy, for example:
        user_obj = User.query.get(int(user_id))
        return user_obj

    Reason to let YOU define this self.user_callback:
        Because we won`t know how/where you will load you user object.
    ```
    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. Refer to"
                    "https://flask-login.readthedocs.io/"
                    "en/latest/#how-it-works for more info.")
            user = self.user_callback(user_id)
            if user is None:
                ctx.user = self.anonymous_user()
            else:
                ctx.user = user
    else:
        ctx.user = 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這個類也是這個模組的核心所在,以後有時間繼續研究。

相關文章