部署在GAE上的Flask应用Flask-Login current_user状态不一致问题
Flask-Login在GAE弹性环境下current_user状态不一致问题
问题现象
将Flask应用部署到Google App Engine(GAE)弹性环境后,遇到Flask-Login的current_user.is_authenticated返回结果不稳定的问题:
- 本地开发服务器上功能完全正常
- GAE环境中,用户登录后
@login_required装饰的路由能正常访问,但导航栏依赖current_user展示的菜单会随机切换:有时显示登录后的用户菜单,有时显示未登录的注册/登录菜单,多次刷新页面会在两种状态间来回跳。
已尝试的解决方法
在app.yaml的gunicorn启动命令中添加--preload参数,问题未解决。
相关代码片段
1. Flask-Login初始化代码(__init__.py)
login_manager = LoginManager() login_manager.init_app(app) @login_manager.user_loader def load_user(user_id): return User.query.filter_by(id=user_id).first()
2. 导航栏模板代码(基础布局)
{% if current_user.is_authenticated %} <li class="nav-item active ml-3"> <span class="nav-link">User: <a href="/">{{ current_user.email }}</a></span></li> <li class="nav-item ml-3"> <a class="btn btn-outline-secondary btn-sm mt-1 reg" href="{{ url_for('home.logout')}}">Logout <span class="sr-only">(current)</span></a></li> {% else %} <li class="nav-item active"> <a class="nav-link" href="/">Register <span class="sr only">(current)</span></a></li> <li class="nav-item active"> <a class="nav-link" href="{{ url_for('home.login') }}">Login <span class="sr-only">(current)</span></a></li> {% endif %}
3. GAE配置(app.yaml)
runtime: python #for some reason this needs to be python-compat if you run with dev_appserver env: flex threadsafe: true entrypoint: gunicorn -b :$PORT wsgi:app --preload --timeout 120 runtime_config: python_version: 3 handlers: - url: /.* script: main.app secure: always
4. User模型代码
class User(BaseModel, UserMixin): __tablename__ = "user" id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name_last = db.Column(db.String, nullable=True) name_first = db.Column(db.String, nullable=True) email = db.Column(db.String, nullable=True) is_admin = db.Column(db.Boolean, default=False) opt_out = db.Column(db.Boolean, default=False) print_opt_in = db.Column(db.Boolean, default=False) date_created = db.Column(db.DateTime(), server_default=func.now()) date_modified = db.Column(db.DateTime(), onupdate=func.now()) organizations = db.relationship('Organization', secondary=user_organizations, lazy='subquery', backref=db.backref('users', lazy=True)) code = db.Column(db.String, nullable=True) # need this to make many-to-many name show up in flask-admin def __str__(self): return self.name_last + ', ' + self.name_first def __repr__(self): return self.name_first + ' ' + self.name_last # return id to be used in storage def get_id(self): return self.id
5. 登录视图代码
@home_blueprint.route('/', methods=['get','post']) @home_blueprint.route('/login', methods=['get','post']) def login(): form = LoginForm() message = request.form.get('message') if request.method=="POST": if current_user: logout_user() user = User.query.filter_by(email=form.email.data.lower()).first() if not user: message = f'{form.email.data} not found.' return render_template('home/login.html', form=form, message=message) else: # 生成4位验证码并保存到用户表,发送邮件后跳转至验证页 code = randrange(1000,9999) print(code) user.code = code db.session.add(user) db.session.commit() message = send_verification_email(form.email.data, code) payload = {'email':form.email.data} email_encoded = urlencode(payload) return redirect(url_for('home.login2', message=message, email_encoded=email_encoded)) else: return render_template('home/login.html', form=form, message=message) @home_blueprint.route('/login2', methods=['get','post']) def login2(): form = LoginForm2() message = request.form.get('message') email_encoded = request.args.get('email_encoded') parsed = parse_qsl(email_encoded) if request.method=="POST": if current_user: logout_user() user = User.query.filter_by(email=form.email.data.lower()).first() if not user: flash(f'{form.email.data} not found.') return render_template('home/login.html', form=form) else: code = request.form.get('code', type=str) if code == user.code: login_user(user) return redirect(url_for('jobs.job_admin', email=user.email)) else: message = "Code and email do not match." form.email.data = parsed[0][1] return render_template('home/login2.html', form=form, message=message) else: form.email.data=parsed[0][1] return render_template('home/login2.html', form=form, message=message)
解决思路
1. 会话存储一致性问题
GAE弹性环境默认多实例部署,若Flask应用使用默认的内存会话(SESSION_TYPE='null'),会话数据仅存在单个实例内存中。当请求被分配到不同实例时,部分实例能读取到登录会话,部分读取不到,就会出现菜单切换的情况。
解决方法:
配置Flask使用分布式会话存储,比如:
- 使用Redis:安装
flask-redis和flask-session,配置SESSION_TYPE='redis',对接GAE的Memorystore for Redis服务 - 使用Google Cloud Datastore:安装
flask-session和google-cloud-datastore,自定义会话存储后端
2. User模型的get_id方法问题
你的User模型重写了get_id()方法,返回的是UUID对象,但Flask-Login要求该方法返回字符串类型的用户ID。会话中存储的UUID对象跨实例序列化/反序列化时可能出错,导致load_user无法正确查询用户。
解决方法:
修改get_id()方法,返回UUID的字符串形式:
def get_id(self): return str(self.id)
3. Gunicorn的worker配置问题
GAE弹性环境下gunicorn默认启动多个worker进程,若会话相关初始化代码存在进程隔离问题,也会导致状态不一致。可以尝试:
- 明确指定gunicorn的worker数量,比如添加
--workers=2参数(根据实例资源调整) - 检查是否有全局变量被错误用于存储会话数据,这类数据在多worker进程间不共享
4. 会话Cookie的配置问题
检查Flask的会话Cookie配置,确保:
SESSION_COOKIE_SECURE=True(虽app.yaml已设secure: always,但建议在Flask代码中显式配置)SESSION_COOKIE_HTTPONLY=True,防止前端篡改CookieSESSION_COOKIE_SAMESITE='Lax'或'Strict',避免跨站Cookie问题PERMANENT_SESSION_LIFETIME设置合理过期时间
在Flask应用中添加配置:
app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
5. 数据库查询的一致性问题
load_user函数中使用User.query.filter_by(id=user_id).first(),若数据库存在读写分离或延迟,可能导致部分实例查询不到用户数据。可以:
- 确保查询使用读一致性级别(比如SQLAlchemy的
with_for_update()或调整数据库一致性设置) - 检查GAE数据库配置,若启用只读副本,确保会话相关查询指向主库
内容的提问来源于stack exchange,提问作者kwkbarker
相关产品推荐
相关产品推荐

