You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 07:38:11