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的核心代码剖析了一遍,如果你有收获,不妨点个赞鼓励一下吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,126评论 22 257
  • 最近在学习flask,用到flask-login,发现网上只有0.1版本的中文文档,看了官方已经0.4了,并且添加...
    ZZES_ZCDC阅读 5,858评论 3 24
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 第二部分 Blog例子 第八章 用户验证 大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份...
    易木成华阅读 1,253评论 0 4
  • 好了,不能再继续玩下去了吧。哈哈,今天还是15岁之前的最后一天呢。 既然今天玩了一天,过了一天这几年在家中一直重复...
    长念危阅读 786评论 0 1