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