美文网首页PythonILove
Flask-Login的源码分析(remember me分析)

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

作者: joKerAndy | 来源:发表于2019-07-09 17:10 被阅读0次

    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过期无法访问还是因为未登录无法访问。不知道这么说对不对,如果对,有什么解决方式吗?

    相关文章

      网友评论

        本文标题:Flask-Login的源码分析(remember me分析)

        本文链接:https://www.haomeiwen.com/subject/dmbxkctx.html