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/')
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.都过期


Flask-Login的源码分析(remember me分析)_第1张图片
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参数
 Login

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

  • 区分

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

你可能感兴趣的:(Flask-Login的源码分析(remember me分析))