Flask部署到Nginx/Apache后随机出现已登录用户未登录判定问题
Flask部署后偶尔出现未登录提示及多次重定向问题
问题现象
本地运行正常,但部署到Apache或Nginx服务器后,用户偶尔会被提示未登录,随后陷入多次重定向循环。以下是用户从/被重定向到/login共9次后恢复登录状态的日志:
[10:57:35] "GET /user/1 HTTP/1.1" 200 12277 "redacted/fleet" 7.36" [10:57:36] "GET /user/1 HTTP/1.1" 302 770 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2Fuser%2F1 HTTP/1.1" 302 736 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 787 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 745 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 789 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 747 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 789 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 749 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 793 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 751 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 793 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 751 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 794 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 752 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 795 "redacted/user/1" 7.36" [10:57:36] "GET /login?next=%2F HTTP/1.1" 302 753 "redacted/user/1" 7.36" [10:57:36] "GET / HTTP/1.1" 302 795 "redacted/user/1" 7.36" [10:57:37] "GET /login?next=%2F HTTP/1.1" 302 752 "redacted/user/1" 7.36" [10:57:37] "GET / HTTP/1.1" 302 797 "redacted/user/1" 7.36" [10:57:37] "GET /login?next=%2F HTTP/1.1" 302 756 "redacted/user/1" 7.36" [10:57:37] "GET /user/1 HTTP/1.1" 302 822 "redacted/user/1" 7.36" [10:57:37] "GET /login?next=%2Fuser%2F1 HTTP/1.1" 302 756 "redacted/user/1" 7.36" [10:57:37] "GET / HTTP/1.1" 200 4013 "redacted/user/1" 7.36" [10:57:40] "GET /user/1 HTTP/1.1" 200 12273 "redacted/" 7.36"
用户无需重新登录,刷新几次页面即可恢复,但问题会反复出现,且无法稳定复现。
相关代码与配置
User Loader函数
@login_manager.user_loader def load_user(user_id): return db.session.get(User, int(user_id))
技术栈版本
- Flask-SQLAlchemy 3.1.1
- Flask 3.0.2
- Flask-Login 0.6.3
- Flask-WTF 1.2.1
- Nginx 1.18.0 (Ubuntu)
- MySQL 8.0.35(与PhpMyAdmin部署在同一服务器)
Nginx配置
server { listen 80; server_name [REDACTED URL]; access_log /var/log/nginx/access.[REDACTED URL].log; error_log /var/log/nginx/error.[REDACTED URL].log; location / { proxy_pass http://unix:/home/gunicorn/run/[REDACTED URL].sock; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Prefix /; }; }
服务配置(systemd)
[Unit] Description=Development After=network.target [Service] User=gunicorn Group=www-data WorkingDirectory=/home/gunicorn/[REDACTED URL]/[REDACTED NAME] Environment="PATH=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]/venv/bin" ExecStart=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]/venv/bin/gunicorn --workers 3 --bind unix:/home/gunicorn/run/[REDACTED URL].sock -m 007 wsgi:application [Install] WantedBy=multi-user.target
核心应用代码
from flask import Flask, redirect, url_for, render_template, request, flash from flask_login import LoginManager, login_user, login_required, current_user from flask_sqlalchemy import SQLAlchemy from flask_mail import Mail from datetime import timedelta from werkzeug.middleware.proxy_fix import ProxyFix app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = '[REDACTED]' app.config['permanent_session_lifetime'] = timedelta(days=364) app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 ) login_manager = LoginManager(app) login_manager.init_app(app) login_manager.login_view = 'login' mail = Mail(app) db = SQLAlchemy(app) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: flash('Already logged in!', 'danger') return redirect(url_for('home')) email_login_form = EmailLoginForm() if 'email_login_submit' in request.form: if email_login_form.validate_on_submit(): account_check = User.query.filter_by(user_email=email_login_form.user_email.data).first() if account_check is None: flash('Incorrect email or password!', 'danger') return redirect(url_for('login')) if not account_check.check_password(email_login_form.user_password.data): flash('Incorrect email or password!', 'danger') return redirect(url_for('login')) login_user(account_check, remember=True) return redirect(url_for('home')) return render_template('login.html', email_login_form=email_login_form) @app.route("/", methods=['GET', 'POST']) @login_required def home(): return render_template('dashboard.html') if __name__ == "__main__": app.run(host='0.0.0.0')
问题分析与解决建议
1. 多Worker会话不一致问题
Gunicorn启用3个Worker时,每个Worker维护独立的数据库会话和上下文,可能出现:
- 某Worker因数据库会话过期、连接池异常等无法加载用户,
load_user返回None触发未登录重定向 - 其他Worker能正常加载用户,刷新后请求分配到正常Worker即恢复登录
解决:
- 添加请求钩子确保数据库会话在请求结束后正确关闭:
@app.teardown_appcontext def shutdown_session(exception=None): db.session.remove()
- 显式配置
scoped_session管理数据库会话(Flask-SQLAlchemy 3.x默认已处理,显式配置更稳妥):
from sqlalchemy.orm import scoped_session, sessionmaker db.session = scoped_session(sessionmaker(bind=db.engine))
2. Flask-Login会话Cookie配置优化
部署环境下,会话Cookie属性未正确设置可能导致Cookie偶尔无法被识别:
- 添加以下配置到
app.config:
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS环境必须开启,HTTP环境设为False app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['SECRET_KEY'] = '[独立强密钥]' # 生产环境使用单独的强密钥,不要与本地共用
3. ProxyFix与Nginx头验证
确保Nginx传递的头信息被Flask正确识别:
- 设置
app.config['PREFERRED_URL_SCHEME']为http或https(匹配部署环境) - 若Nginx处理SSL,确认
X-Forwarded-Proto传递的是https
4. User Loader异常捕获
load_user未处理异常,数据库临时中断会直接返回None触发未登录:
- 增加异常捕获与日志记录:
import logging @login_manager.user_loader def load_user(user_id): try: return db.session.get(User, int(user_id)) except Exception as e: logging.error(f"加载用户{user_id}失败: {str(e)}") db.session.rollback() return None
5. Gunicorn Worker配置调整
- 先将Worker数量改为1,验证是否为多Worker导致的问题
- 添加
--timeout参数避免Worker因超时重启:
ExecStart=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]/venv/bin/gunicorn --workers 3 --timeout 120 --bind unix:/home/gunicorn/run/[REDACTED URL].sock -m 007 wsgi:application
内容的提问来源于stack exchange,提问作者Reuben
相关产品推荐
相关产品推荐

