PythonILove

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

2019-07-09  本文已影响0人  joKerAndy

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

def _set_cookie(self, response):
     ...省略...
        response.set_cookie(cookie_name,
                            value=data,
                            expires=expires,
                            domain=domain,
                            path=path,
                            secure=secure,
                            httponly=httponly)
@app.route('/login/<name>')
def login(name):
    """模拟登录"""
    session['login'] = True
    session['andy'] = 'jang'
    return redirect(url_for('.main', name=name))
REMEMBER_COOKIE_DURATION = datetime.timedelta(days=1)
PERMANENT_SESSION_LIFETIME = datetime.timedelta(minutes=10)

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


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

上一篇下一篇

猜你喜欢

热点阅读