Flask-Login的源码分析(remember me分析)

Flask-Login
官网介绍:用于管理Flask的user session的,其实就是登录、登出和“记住我”功能。

  • Flask提供的2种cookie的写入方式

  • 第一种:使用response对象的set_cookie()方法
    在Flask-Login中设置remember_token就是采用这种方式,在login_manager.py文件中。这种方式cookie都是明文,不安全(remember_token是自己实现的加密,cookie的值都是经过sha512签名过的)。
def _set_cookie(self, response):
     ...省略...
        response.set_cookie(cookie_name,
                            value=data,
                            expires=expires,
                            domain=domain,
                            path=path,
                            secure=secure,
                            httponly=httponly)
  • 第二种:针对第一种的弊端,Flask提供的session对象,可以方便的对写入的cookie进行签名(Flask必须配置SECRET_KEY)
@app.route('/login/<name>')
def login(name):
    """模拟登录"""
    session['login'] = True
    session['andy'] = 'jang'
    return redirect(url_for('.main', name=name))
  • 针对两种cookie的写入方式,设置过期时间的方式也不同

  • 使用response对象的set_cookie()方法
    这种方式在是通过expires参数来设置过期时间,默认是会话结束时session失效,在Flask-Login中是通过在Flask的配置文件settings.py配置失效时间的:
REMEMBER_COOKIE_DURATION = datetime.timedelta(days=1)
  • session对象的方式:这种方式默认也是会话结束时session失效,可以通过设置session.permanent=True可已将session的有效期延长为PERMANENT_SESSION_LIFETIME指定的时长:
PERMANENT_SESSION_LIFETIME = datetime.timedelta(minutes=10)
  • Flask在使用Flask-login时,存在下边几种情况

(假设用户登录过,且都点击了remember按钮)
1.session过期,但是remember me对应的set_cookie方法未过期
2.session未过期,但是remember me对应的set_cookie方法过期
3.都未过期
4.都过期


QQ图片20190709154559.png
  • 对于第一种情况:当我们再次刷新页面后,界面会重新渲染,回调utils.py中的_user_context_processor()方法,这个方法在Flask-Login初始化时,向模板上下文注册user对象时调用
def _user_context_processor():
    return dict(current_user=_get_user())
def _get_user():
    #请求发来时,这2个条件都满足
    if has_request_context() and not hasattr(_request_ctx_stack.top, 'user'):
        current_app.login_manager._load_user()
    #从上下文对象中取出user对象,注册到模板的上下文对象中
    return getattr(_request_ctx_stack.top, 'user', None)
def _load_user(self):
       ...省略...
        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:
                #第一种情况,has_cookie为True,走这个if分支
                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()
def _load_from_cookie(self, cookie):
        #从refresh_token中取出user_id,
        user_id = decode_cookie(cookie)
        if user_id is not None:
            #user_id赋值到session对象中去
            session['user_id'] = user_id
            session['_fresh'] = False
        #重新加载user对象
        self.reload_user()

        if _request_ctx_stack.top.user is not None:
            app = current_app._get_current_object()
            user_loaded_from_cookie.send(app, user=_get_user())
def reload_user(self, user=None):
        ctx = _request_ctx_stack.top

        if user is None:
            #从session中取出user_id,这是在_load_from_cookie()方法中提前写入的
            user_id = session.get('user_id')
            if user_id is None:
                #如果user_id为空,则加载匿名对象
                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_id不为空,则执行我们自定义的user_callback从业务层中拿到user对象
                user = self.user_callback(user_id)
                if user is None:
                    ctx.user = self.anonymous_user()
                else:
                    #将user对象绑定到上下文对象上,使用时,就从上下文对象的栈顶取出user对象注测到模板的
                    #上下文对象中
                    ctx.user = user
        else:
            ctx.user = user

大体思路就是:session过期了,就从remember me对应的cookie中取出user_id,赋值给session,然后从业务层中拿到我们的user对象,绑定到请求上下文对象中供使用,此时,is_authenticated=True

对于第二种情况:当我们再次刷新页面后,就像情况1一样,还是会调用_user_context_processor()方法,不同的是在_load_user()方法中is_missing_user_id为False,直接调用reload_user()方法,从session中直接取出user_id,判断user_id不为空,则直接调用业务层的方法得到user对象,以后的流程跟情况一完全一样。

对于第三种情况:按照源码的分析,第三种情况的流程和第一种情况完全一样

对于第四种情况:按照源码的分析,第三种情况的流程和第一种情况基本一样,不同在于最后reload_user()方法加载的user_id始终为空,这时会自动加载Flask_login种定义的AnonymousUser对象,也就是is_authenticated=False

  • 总结

用户重新渲染界面时,会重新加载当前用户对象,如果session中有user_id,则根据这个user_id到业务层中拿去当前用户对象,如果session中没有user_id,则从remember_token中拿取user_id,并放到session对象中一份,然后根据user_id到业务层取user对象,如果remember_token中也没有user_id,则直接返回Flask-Login自定义的不记名对象AnonymousUserMixin,此时,is_authenticated=False,在界面显示上就是未登录的状态。

  • 扩展

对于采用login_required修饰的视图

def login_required(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        if request.method in EXEMPT_METHODS:
            return func(*args, **kwargs)
        elif current_app.login_manager._login_disabled:
            return func(*args, **kwargs)
        #也是采用is_authenticated字段判断是否需要重新登录
        elif not current_user.is_authenticated:
            return current_app.login_manager.unauthorized()
        return func(*args, **kwargs)
    return decorated_view

 def unauthorized(self):
        user_unauthorized.send(current_app._get_current_object())
        #支持自定义未登录的处理方式,而不仅仅是跳转到登录界面
        if self.unauthorized_callback:
            return self.unauthorized_callback()

        if request.blueprint in self.blueprint_login_views:
            login_view = self.blueprint_login_views[request.blueprint]
        else:
            #我们配置的login_vew,指定login的路由
            login_view = self.login_view

        if not login_view:
            abort(401)

        if self.login_message:
            if self.localize_callback is not None:
                flash(self.localize_callback(self.login_message),
                      category=self.login_message_category)
            else:
                #向模板flash消息
                flash(self.login_message, category=self.login_message_category)

        config = current_app.config
        if config.get('USE_SESSION_FOR_NEXT', USE_SESSION_FOR_NEXT):
            login_url = expand_login_view(login_view)
            session['next'] = make_next_param(login_url, request.url)
            redirect_url = make_login_url(login_view)
        else:
            #拼接当前地址到login的路由后边,以便登录之后重新回到当前界面
            redirect_url = make_login_url(login_view, next_url=request.url)

        return redirect(redirect_url)
模板中指定next参数
 <a href="{{ url_for('auth.login', next=request.full_path) }}">Login</a>

这样login_required装饰器就实现了视图保护功能。

  • 区分

上边介绍cookie过期和视图包括最终都是将用户的is_authenticated置为False,这就无法区分视图到底是因为cookie过期无法访问还是因为未登录无法访问。不知道这么说对不对,如果对,有什么解决方式吗?

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

推荐阅读更多精彩内容

  • 关于Flask登录认证的详细过程请参见拙作<<使用Flask实现用户登陆认证的详细过程>>一文,而本文则偏重于详细...
    geekpy阅读 28,486评论 5 28
  • 第二部分 Blog例子 第八章 用户验证 大部分程序需要追踪用户身份。当用户连接到程序,通过一系列步骤使自己的身份...
    易木成华阅读 1,255评论 0 4
  • 声明:本文仅限于简书发布,其他第三方网站均为盗版,原文地址: Flask-Login 使用和进阶 在我们使用 Fl...
    liuliqiang阅读 16,999评论 5 21
  • 会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话。常用的会话跟踪技术是Cookie与Se...
    chinariver阅读 5,501评论 1 49
  • 文件总览 如下图所示,Flask-login的工作目录主要是login_manager、mixins、signal...
    yiludege阅读 1,692评论 0 3