flask

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Flask Skill

Flask 生产级实战指南

Production-tested patterns for Flask with the application factory pattern, Blueprints, and Flask-SQLAlchemy.
Latest Versions (verified January 2026):
  • Flask: 3.1.2
  • Flask-SQLAlchemy: 3.1.1
  • Flask-Login: 0.6.3
  • Flask-WTF: 1.2.2
  • Werkzeug: 3.1.5
  • Python: 3.9+ required (3.8 dropped in Flask 3.1.0)

经过生产环境验证的Flask模式,包含应用工厂模式、Blueprints和Flask-SQLAlchemy。
最新版本(2026年1月验证):
  • Flask: 3.1.2
  • Flask-SQLAlchemy: 3.1.1
  • Flask-Login: 0.6.3
  • Flask-WTF: 1.2.2
  • Werkzeug: 3.1.5
  • Python: 要求3.9及以上版本(Flask 3.1.0已弃用3.8)

Quick Start

快速开始

Project Setup with uv

使用uv搭建项目

bash
undefined
bash
undefined

Create project

Create project

uv init my-flask-app cd my-flask-app
uv init my-flask-app cd my-flask-app

Add dependencies

Add dependencies

uv add flask flask-sqlalchemy flask-login flask-wtf python-dotenv
uv add flask flask-sqlalchemy flask-login flask-wtf python-dotenv

Run development server

Run development server

uv run flask --app app run --debug
undefined
uv run flask --app app run --debug
undefined

Minimal Working Example

最小可运行示例

python
undefined
python
undefined

app.py

app.py

from flask import Flask
app = Flask(name)
@app.route("/") def hello(): return {"message": "Hello, World!"}
if name == "main": app.run(debug=True)

Run: `uv run flask --app app run --debug`

---
from flask import Flask
app = Flask(name)
@app.route("/") def hello(): return {"message": "Hello, World!"}
if name == "main": app.run(debug=True)

运行命令:`uv run flask --app app run --debug`

---

Known Issues Prevention

已知问题预防

This skill prevents 9 documented issues:
本指南可预防9种已记录的问题:

Issue #1: stream_with_context Teardown Regression (Flask 3.1.2)

问题 #1: stream_with_context 清理回归问题(Flask 3.1.2)

Error:
KeyError
in teardown functions when using
stream_with_context
Source: GitHub Issue #5804 Why It Happens: Flask 3.1.2 introduced a regression where
stream_with_context
triggers
teardown_request()
calls multiple times before response generation completes. If teardown callbacks use
g.pop(key)
without a default, they fail on the second call.
Prevention:
python
undefined
错误: 使用
stream_with_context
时,清理函数中出现
KeyError
来源: GitHub Issue #5804 原因: Flask 3.1.2引入的回归问题导致
stream_with_context
在响应生成完成前多次触发
teardown_request()
调用。如果清理回调使用
g.pop(key)
且未设置默认值,第二次调用时会失败。
解决方法:
python
undefined

WRONG - fails on second teardown call

WRONG - fails on second teardown call

@app.teardown_request def teardown_request(): g.pop("hello") # KeyError on second call
@app.teardown_request def teardown_request(): g.pop("hello") # KeyError on second call

RIGHT - idempotent teardown

RIGHT - idempotent teardown

@app.teardown_request def teardown_request(): g.pop("hello", None) # Provide default value

**Status**: Will be fixed in Flask 3.2.0 as side effect of PR #5812. Until then, ensure all teardown callbacks are idempotent.

---
@app.teardown_request def teardown_request(): g.pop("hello", None) # Provide default value

**状态**: 该问题将在Flask 3.2.0中通过PR #5812修复。在此之前,请确保所有清理回调都是幂等的。

---

Issue #2: Async Views with Gevent Incompatibility

问题 #2: 异步视图与Gevent不兼容

Error:
RuntimeError
when handling concurrent async requests with gevent Source: GitHub Issue #5881 Why It Happens: Asgiref fails when gevent monkey-patching is active. Asyncio expects a single event loop per OS thread, but gevent's monkey-patching makes
threading.Thread
create greenlets instead of real threads, causing both loops to run on the same physical thread and block each other.
Prevention: Choose either async (with asyncio/uvloop) OR gevent, not both. If you must use both:
python
import asyncio
import gevent.monkey
import gevent.selectors
from flask import Flask

gevent.monkey.patch_all()
loop = asyncio.EventLoop(gevent.selectors.DefaultSelector())
gevent.spawn(loop.run_forever)

class GeventFlask(Flask):
    def async_to_sync(self, func):
        def run(*args, **kwargs):
            coro = func(*args, **kwargs)
            future = asyncio.run_coroutine_threadsafe(coro, loop)
            return future.result()
        return run

app = GeventFlask(__name__)
Note: This "defeats the whole purpose of both" (maintainer comment). Individual async requests work, but concurrent requests fail without this workaround.

错误: 使用gevent处理并发异步请求时出现
RuntimeError
来源: GitHub Issue #5881 原因: 当gevent猴子补丁激活时,Asgiref会失效。Asyncio期望每个OS线程对应一个事件循环,但gevent的猴子补丁会让
threading.Thread
创建协程而非真实线程,导致两个循环在同一物理线程上运行并相互阻塞。
解决方法: 选择异步(使用asyncio/uvloop)或gevent,不要同时使用。如果必须同时使用:
python
import asyncio
import gevent.monkey
import gevent.selectors
from flask import Flask

gevent.monkey.patch_all()
loop = asyncio.EventLoop(gevent.selectors.DefaultSelector())
gevent.spawn(loop.run_forever)

class GeventFlask(Flask):
    def async_to_sync(self, func):
        def run(*args, **kwargs):
            coro = func(*args, **kwargs)
            future = asyncio.run_coroutine_threadsafe(coro, loop)
            return future.result()
        return run

app = GeventFlask(__name__)
注意: 这种方案"违背了两者的设计初衷"(维护者评论)。单个异步请求可以正常工作,但并发请求若无此 workaround 会失败。

Issue #3: Test Client Session Not Updated on Redirect

问题 #3: 测试客户端会话在重定向后未更新

Error: Session state incorrect after
follow_redirects=True
in tests Source: GitHub Issue #5786 Why It Happens: In Flask < 3.1.2, the test client's session wasn't correctly updated after following redirects.
Prevention:
python
undefined
错误: 测试中使用
follow_redirects=True
后,会话状态不正确 来源: GitHub Issue #5786 原因: 在Flask < 3.1.2版本中,测试客户端的会话在跟随重定向后未正确更新。
解决方法:
python
undefined

If using Flask >= 3.1.2, follow_redirects works correctly

If using Flask >= 3.1.2, follow_redirects works correctly

def test_login_redirect(client): response = client.post('/login', data={'email': 'test@example.com', 'password': 'pass'}, follow_redirects=True) assert 'user_id' in session # Works in 3.1.2+
def test_login_redirect(client): response = client.post('/login', data={'email': 'test@example.com', 'password': 'pass'}, follow_redirects=True) assert 'user_id' in session # Works in 3.1.2+

For Flask < 3.1.2, make separate requests

For Flask < 3.1.2, make separate requests

response = client.post('/login', data={...}) assert response.status_code == 302 response = client.get(response.location) # Explicit redirect follow

**Status**: Fixed in Flask 3.1.2. Upgrade to latest version.

---
response = client.post('/login', data={...}) assert response.status_code == 302 response = client.get(response.location) # Explicit redirect follow

**状态**: 已在Flask 3.1.2中修复。请升级到最新版本。

---

Issue #4: Application Context Lost in Threads (Community-sourced)

问题 #4: 线程中丢失应用上下文(社区收集)

Error:
RuntimeError: Working outside of application context
in background threads Source: Sentry.io Guide Why It Happens: When passing
current_app
to a new thread, you must unwrap the proxy object using
_get_current_object()
and push app context in the thread.
Prevention:
python
from flask import current_app
import threading
错误: 后台线程中出现
RuntimeError: Working outside of application context
来源: Sentry.io Guide 原因: 当将
current_app
传递给新线程时,必须使用
_get_current_object()
解包代理对象,并在线程中推送应用上下文。
解决方法:
python
from flask import current_app
import threading

WRONG - current_app is a proxy, loses context in thread

WRONG - current_app is a proxy, loses context in thread

def background_task(): app_name = current_app.name # Fails!
@app.route('/start') def start_task(): thread = threading.Thread(target=background_task) thread.start()
def background_task(): app_name = current_app.name # Fails!
@app.route('/start') def start_task(): thread = threading.Thread(target=background_task) thread.start()

RIGHT - unwrap proxy and push context

RIGHT - unwrap proxy and push context

def background_task(app): with app.app_context(): app_name = app.name # Works!
@app.route('/start') def start_task(): app = current_app._get_current_object() thread = threading.Thread(target=background_task, args=(app,)) thread.start()

**Verified**: Common pattern in production applications, documented in official Flask docs.

---
def background_task(app): with app.app_context(): app_name = app.name # Works!
@app.route('/start') def start_task(): app = current_app._get_current_object() thread = threading.Thread(target=background_task, args=(app,)) thread.start()

**验证**: 这是生产应用中的常见模式,已在Flask官方文档中记录。

---

Issue #5: Flask-Login Session Protection Unexpected Logouts (Community-sourced)

问题 #5: Flask-Login会话保护导致意外登出(社区收集)

Error: Users logged out unexpectedly when IP address changes Source: Flask-Login Docs Why It Happens: Flask-Login's "strong" session protection mode deletes the entire session if session identifiers (like IP address) change. This affects users on mobile networks or VPNs.
Prevention:
python
undefined
错误: 当IP地址变化时,用户被意外登出 来源: Flask-Login Docs 原因: Flask-Login的"strong"会话保护模式会在会话标识符(如IP地址)变化时删除整个会话。这会影响使用移动网络或VPN的用户。
解决方法:
python
undefined

app/extensions.py

app/extensions.py

from flask_login import LoginManager
login_manager = LoginManager() login_manager.session_protection = "basic" # Default, less strict
from flask_login import LoginManager
login_manager = LoginManager() login_manager.session_protection = "basic" # Default, less strict

login_manager.session_protection = "strong" # Strict, may logout on IP change

login_manager.session_protection = "strong" # Strict, may logout on IP change

login_manager.session_protection = None # Disabled (not recommended)

login_manager.session_protection = None # Disabled (not recommended)


**Note**: By default, Flask-Login allows concurrent sessions (same user on multiple browsers). To prevent this, implement custom session tracking.

**Verified**: Official Flask-Login documentation, multiple 2024 blog posts.

---

**注意**: 默认情况下,Flask-Login允许多端并发会话(同一用户在多个浏览器登录)。如需禁止此功能,请实现自定义会话跟踪。

**验证**: 已在Flask-Login官方文档中说明,2024年多篇博客文章也有提及。

---

Issue #6: CSRF Protection Cache Interference (Community-sourced)

问题 #6: CSRF保护与缓存冲突(社区收集)

Error: Form submissions fail with "CSRF token missing/invalid" on cached pages Source: Flask-WTF Docs Why It Happens: If webserver cache policy caches pages longer than
WTF_CSRF_TIME_LIMIT
, browsers serve cached pages with expired CSRF tokens.
Prevention:
python
undefined
错误: 缓存页面上的表单提交失败,提示"CSRF token missing/invalid" 来源: Flask-WTF Docs 原因: 如果Web服务器缓存策略的缓存时长超过
WTF_CSRF_TIME_LIMIT
,浏览器会提供带有过期CSRF令牌的缓存页面。
解决方法:
python
undefined

Option 1: Align cache duration with token lifetime

Option 1: Align cache duration with token lifetime

WTF_CSRF_TIME_LIMIT = None # Never expire (less secure)
WTF_CSRF_TIME_LIMIT = None # Never expire (less secure)

Option 2: Exclude forms from cache

Option 2: Exclude forms from cache

@app.after_request def add_cache_headers(response): if request.method == 'GET' and 'form' in request.endpoint: response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' return response
@app.after_request def add_cache_headers(response): if request.method == 'GET' and 'form' in request.endpoint: response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' return response

Option 3: Configure webserver to not cache POST targets

Option 3: Configure webserver to not cache POST targets

In Nginx: add "proxy_cache_bypass $cookie_session" for form routes

In Nginx: add "proxy_cache_bypass $cookie_session" for form routes


**Verified**: Official Flask-WTF documentation warning, security best practices guides from 2024.

---

**验证**: Flask-WTF官方文档已发出警告,2024年的安全最佳实践指南也有相关内容。

---

Issue #7: Per-Request max_content_length Override (New Feature)

问题 #7: 按请求覆盖max_content_length(新功能)

Feature: Flask 3.1.0 added ability to customize
Request.max_content_length
per-request Source: Flask 3.1.0 Release Notes
Usage:
python
from flask import Flask, request

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB default

@app.route('/upload', methods=['POST'])
def upload():
    # Override for this specific route
    request.max_content_length = 100 * 1024 * 1024  # 100MB for uploads
    file = request.files['file']
    # ...
Note: Also added
MAX_FORM_MEMORY_SIZE
and
MAX_FORM_PARTS
config options in 3.1.0. See security documentation.

功能: Flask 3.1.0新增了按请求自定义
Request.max_content_length
的能力 来源: Flask 3.1.0 Release Notes
用法:
python
from flask import Flask, request

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB default

@app.route('/upload', methods=['POST'])
def upload():
    # Override for this specific route
    request.max_content_length = 100 * 1024 * 1024  # 100MB for uploads
    file = request.files['file']
    # ...
注意: 3.1.0版本还新增了
MAX_FORM_MEMORY_SIZE
MAX_FORM_PARTS
配置项。请查看安全文档

Issue #8: SECRET_KEY Rotation (New Feature)

问题 #8: SECRET_KEY 轮换(新功能)

Feature: Flask 3.1.0 added
SECRET_KEY_FALLBACKS
for key rotation Source: Flask 3.1.0 Release Notes
Usage:
python
undefined
功能: Flask 3.1.0新增了
SECRET_KEY_FALLBACKS
用于密钥轮换 来源: Flask 3.1.0 Release Notes
用法:
python
undefined

config.py

config.py

class Config: SECRET_KEY = "new-secret-key-2024" SECRET_KEY_FALLBACKS = [ "old-secret-key-2023", "older-secret-key-2022" ]

**Note**: Extensions need explicit support for this feature. Flask-Login and Flask-WTF may need updates to use fallback keys.

---
class Config: SECRET_KEY = "new-secret-key-2024" SECRET_KEY_FALLBACKS = [ "old-secret-key-2023", "older-secret-key-2022" ]

**注意**: 扩展需要显式支持此功能。Flask-Login和Flask-WTF可能需要更新才能使用备用密钥。

---

Issue #9: Werkzeug 3.1+ Dependency Conflict

问题 #9: Werkzeug 3.1+ 依赖冲突

Error:
flask==2.2.4 incompatible with werkzeug==3.1.3
Source: Flask 3.1.0 Release Notes | GitHub Issue #5652 Why It Happens: Flask 3.1.0 updated minimum dependency versions: Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. Projects pinned to older versions will have conflicts.
Prevention:
bash
undefined
错误:
flask==2.2.4 incompatible with werkzeug==3.1.3
来源: Flask 3.1.0 Release Notes | GitHub Issue #5652 原因: Flask 3.1.0更新了最低依赖版本:Werkzeug >= 3.1、ItsDangerous >= 2.2、Blinker >= 1.9。固定为旧版本的项目会出现冲突。
解决方法:
bash
undefined

Update all Pallets projects together

Update all Pallets projects together

pip install flask>=3.1.0 werkzeug>=3.1.0 itsdangerous>=2.2.0 blinker>=1.9.0
pip install flask>=3.1.0 werkzeug>=3.1.0 itsdangerous>=2.2.0 blinker>=1.9.0

Or with uv

Or with uv

uv add "flask>=3.1.0" "werkzeug>=3.1.0" "itsdangerous>=2.2.0" "blinker>=1.9.0"

---
uv add "flask>=3.1.0" "werkzeug>=3.1.0" "itsdangerous>=2.2.0" "blinker>=1.9.0"

---

Project Structure (Application Factory)

项目结构(应用工厂模式)

For maintainable applications, use the factory pattern with blueprints:
my-flask-app/
├── pyproject.toml
├── config.py                # Configuration classes
├── run.py                   # Entry point
├── app/
│   ├── __init__.py          # Application factory (create_app)
│   ├── extensions.py        # Flask extensions (db, login_manager)
│   ├── models.py            # SQLAlchemy models
│   │
│   ├── main/                # Main blueprint
│   │   ├── __init__.py
│   │   └── routes.py
│   │
│   ├── auth/                # Auth blueprint
│   │   ├── __init__.py
│   │   ├── routes.py
│   │   └── forms.py
│   │
│   ├── templates/
│   │   ├── base.html
│   │   ├── main/
│   │   └── auth/
│   │
│   └── static/
│       ├── css/
│       └── js/
└── tests/
    ├── conftest.py
    └── test_main.py

对于可维护的应用,建议使用工厂模式结合Blueprints:
my-flask-app/
├── pyproject.toml
├── config.py                # 配置类
├── run.py                   # 入口文件
├── app/
│   ├── __init__.py          # 应用工厂(create_app)
│   ├── extensions.py        # Flask扩展(db, login_manager)
│   ├── models.py            # SQLAlchemy模型
│   │
│   ├── main/                # 主Blueprint
│   │   ├── __init__.py
│   │   └── routes.py
│   │
│   ├── auth/                # 认证Blueprint
│   │   ├── __init__.py
│   │   ├── routes.py
│   │   └── forms.py
│   │
│   ├── templates/
│   │   ├── base.html
│   │   ├── main/
│   │   └── auth/
│   │
│   └── static/
│       ├── css/
│       └── js/
└── tests/
    ├── conftest.py
    └── test_main.py

Core Patterns

核心模式

Application Factory

应用工厂

python
undefined
python
undefined

app/init.py

app/init.py

from flask import Flask from app.extensions import db, login_manager from config import Config
def create_app(config_class=Config): """Application factory function.""" app = Flask(name) app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)

# Register blueprints
from app.main import bp as main_bp
from app.auth import bp as auth_bp

app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix="/auth")

# Create database tables
with app.app_context():
    db.create_all()

return app

**Key Benefits**:
- Multiple app instances with different configs (testing)
- Avoids circular imports
- Extensions initialized once, bound to app later
from flask import Flask from app.extensions import db, login_manager from config import Config
def create_app(config_class=Config): """Application factory function.""" app = Flask(name) app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)

# Register blueprints
from app.main import bp as main_bp
from app.auth import bp as auth_bp

app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix="/auth")

# Create database tables
with app.app_context():
    db.create_all()

return app

**核心优势**:
- 可创建多个使用不同配置的应用实例(用于测试)
- 避免循环导入
- 扩展仅初始化一次,后续绑定到应用

Extensions Module

扩展模块

python
undefined
python
undefined

app/extensions.py

app/extensions.py

from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager
db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = "auth.login" login_manager.login_message_category = "info"

**Why separate file?**: Prevents circular imports - models can import `db` without importing `app`.
from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager
db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = "auth.login" login_manager.login_message_category = "info"

**为什么单独存放?**: 避免循环导入 - 模型可以导入`db`而无需导入`app`。

Configuration

配置

python
undefined
python
undefined

config.py

config.py

import os from dotenv import load_dotenv
load_dotenv()
class Config: """Base configuration.""" SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key") SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db") SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config): """Development configuration.""" DEBUG = True
class TestingConfig(Config): """Testing configuration.""" TESTING = True SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" WTF_CSRF_ENABLED = False
class ProductionConfig(Config): """Production configuration.""" DEBUG = False
undefined
import os from dotenv import load_dotenv
load_dotenv()
class Config: """Base configuration.""" SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-key") SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db") SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config): """Development configuration.""" DEBUG = True
class TestingConfig(Config): """Testing configuration.""" TESTING = True SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" WTF_CSRF_ENABLED = False
class ProductionConfig(Config): """Production configuration.""" DEBUG = False
undefined

Entry Point

入口文件

python
undefined
python
undefined

run.py

run.py

from app import create_app
app = create_app()
if name == "main": app.run()

Run: `flask --app run run --debug`

---
from app import create_app
app = create_app()
if name == "main": app.run()

运行命令:`flask --app run run --debug`

---

Blueprints

Blueprints

Creating a Blueprint

创建Blueprint

python
undefined
python
undefined

app/main/init.py

app/main/init.py

from flask import Blueprint
bp = Blueprint("main", name)
from app.main import routes # Import routes after bp is created!

```python
from flask import Blueprint
bp = Blueprint("main", name)
from app.main import routes # Import routes after bp is created!

```python

app/main/routes.py

app/main/routes.py

from flask import render_template, jsonify from app.main import bp
@bp.route("/") def index(): return render_template("main/index.html")
@bp.route("/api/health") def health(): return jsonify({"status": "ok"})
undefined
from flask import render_template, jsonify from app.main import bp
@bp.route("/") def index(): return render_template("main/index.html")
@bp.route("/api/health") def health(): return jsonify({"status": "ok"})
undefined

Blueprint with Templates

带模板的Blueprint

python
undefined
python
undefined

app/auth/init.py

app/auth/init.py

from flask import Blueprint
bp = Blueprint( "auth", name, template_folder="templates", # Blueprint-specific templates static_folder="static", # Blueprint-specific static files )
from app.auth import routes

---
from flask import Blueprint
bp = Blueprint( "auth", name, template_folder="templates", # Blueprint-specific templates static_folder="static", # Blueprint-specific static files )
from app.auth import routes

---

Database Models

数据库模型

python
undefined
python
undefined

app/models.py

app/models.py

from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from app.extensions import db, login_manager
class User(UserMixin, db.Model): """User model for authentication.""" tablename = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(256), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

def set_password(self, password):
    self.password_hash = generate_password_hash(password)

def check_password(self, password):
    return check_password_hash(self.password_hash, password)

def __repr__(self):
    return f"<User {self.email}>"
@login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id))

---
from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from app.extensions import db, login_manager
class User(UserMixin, db.Model): """User model for authentication.""" tablename = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(256), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)

def set_password(self, password):
    self.password_hash = generate_password_hash(password)

def check_password(self, password):
    return check_password_hash(self.password_hash, password)

def __repr__(self):
    return f"<User {self.email}>"
@login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id))

---

Authentication with Flask-Login

使用Flask-Login实现认证

Auth Forms

认证表单

python
undefined
python
undefined

app/auth/forms.py

app/auth/forms.py

from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError from app.models import User
class LoginForm(FlaskForm): email = StringField("Email", validators=[DataRequired(), Email()]) password = PasswordField("Password", validators=[DataRequired()]) remember = BooleanField("Remember Me") submit = SubmitField("Login")
class RegistrationForm(FlaskForm): email = StringField("Email", validators=[DataRequired(), Email()]) password = PasswordField("Password", validators=[DataRequired(), Length(min=8)]) confirm = PasswordField("Confirm Password", validators=[ DataRequired(), EqualTo("password", message="Passwords must match") ]) submit = SubmitField("Register")
def validate_email(self, field):
    if User.query.filter_by(email=field.data).first():
        raise ValidationError("Email already registered.")
undefined
from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError from app.models import User
class LoginForm(FlaskForm): email = StringField("Email", validators=[DataRequired(), Email()]) password = PasswordField("Password", validators=[DataRequired()]) remember = BooleanField("Remember Me") submit = SubmitField("Login")
class RegistrationForm(FlaskForm): email = StringField("Email", validators=[DataRequired(), Email()]) password = PasswordField("Password", validators=[DataRequired(), Length(min=8)]) confirm = PasswordField("Confirm Password", validators=[ DataRequired(), EqualTo("password", message="Passwords must match") ]) submit = SubmitField("Register")
def validate_email(self, field):
    if User.query.filter_by(email=field.data).first():
        raise ValidationError("Email already registered.")
undefined

Auth Routes

认证路由

python
undefined
python
undefined

app/auth/routes.py

app/auth/routes.py

from flask import render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, login_required, current_user from app.auth import bp from app.auth.forms import LoginForm, RegistrationForm from app.extensions import db from app.models import User
@bp.route("/register", methods=["GET", "POST"]) def register(): if current_user.is_authenticated: return redirect(url_for("main.index"))
form = RegistrationForm()
if form.validate_on_submit():
    user = User(email=form.email.data)
    user.set_password(form.password.data)
    db.session.add(user)
    db.session.commit()
    flash("Registration successful! Please log in.", "success")
    return redirect(url_for("auth.login"))

return render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"]) def login(): if current_user.is_authenticated: return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
    user = User.query.filter_by(email=form.email.data).first()
    if user and user.check_password(form.password.data):
        login_user(user, remember=form.remember.data)
        next_page = request.args.get("next")
        flash("Logged in successfully!", "success")
        return redirect(next_page or url_for("main.index"))
    flash("Invalid email or password.", "danger")

return render_template("auth/login.html", form=form)
@bp.route("/logout") @login_required def logout(): logout_user() flash("You have been logged out.", "info") return redirect(url_for("main.index"))
undefined
from flask import render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, login_required, current_user from app.auth import bp from app.auth.forms import LoginForm, RegistrationForm from app.extensions import db from app.models import User
@bp.route("/register", methods=["GET", "POST"]) def register(): if current_user.is_authenticated: return redirect(url_for("main.index"))
form = RegistrationForm()
if form.validate_on_submit():
    user = User(email=form.email.data)
    user.set_password(form.password.data)
    db.session.add(user)
    db.session.commit()
    flash("Registration successful! Please log in.", "success")
    return redirect(url_for("auth.login"))

return render_template("auth/register.html", form=form)
@bp.route("/login", methods=["GET", "POST"]) def login(): if current_user.is_authenticated: return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
    user = User.query.filter_by(email=form.email.data).first()
    if user and user.check_password(form.password.data):
        login_user(user, remember=form.remember.data)
        next_page = request.args.get("next")
        flash("Logged in successfully!", "success")
        return redirect(next_page or url_for("main.index"))
    flash("Invalid email or password.", "danger")

return render_template("auth/login.html", form=form)
@bp.route("/logout") @login_required def logout(): logout_user() flash("You have been logged out.", "info") return redirect(url_for("main.index"))
undefined

Protecting Routes

保护路由

python
from flask_login import login_required, current_user

@bp.route("/dashboard")
@login_required
def dashboard():
    return render_template("main/dashboard.html", user=current_user)

python
from flask_login import login_required, current_user

@bp.route("/dashboard")
@login_required
def dashboard():
    return render_template("main/dashboard.html", user=current_user)

API Routes (JSON)

API路由(JSON格式)

For REST APIs without templates:
python
undefined
适用于无模板的REST API:
python
undefined

app/api/init.py

app/api/init.py

from flask import Blueprint
bp = Blueprint("api", name)
from app.api import routes

```python
from flask import Blueprint
bp = Blueprint("api", name)
from app.api import routes

```python

app/api/routes.py

app/api/routes.py

from flask import jsonify, request from flask_login import login_required, current_user from app.api import bp from app.extensions import db from app.models import User
@bp.route("/users", methods=["GET"]) @login_required def get_users(): users = User.query.all() return jsonify([ {"id": u.id, "email": u.email} for u in users ])
@bp.route("/users", methods=["POST"]) def create_user(): data = request.get_json() if not data or "email" not in data or "password" not in data: return jsonify({"error": "Missing required fields"}), 400
if User.query.filter_by(email=data["email"]).first():
    return jsonify({"error": "Email already exists"}), 409

user = User(email=data["email"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()

return jsonify({"id": user.id, "email": user.email}), 201

Register with prefix:
```python
app.register_blueprint(api_bp, url_prefix="/api/v1")

from flask import jsonify, request from flask_login import login_required, current_user from app.api import bp from app.extensions import db from app.models import User
@bp.route("/users", methods=["GET"]) @login_required def get_users(): users = User.query.all() return jsonify([ {"id": u.id, "email": u.email} for u in users ])
@bp.route("/users", methods=["POST"]) def create_user(): data = request.get_json() if not data or "email" not in data or "password" not in data: return jsonify({"error": "Missing required fields"}), 400
if User.query.filter_by(email=data["email"]).first():
    return jsonify({"error": "Email already exists"}), 409

user = User(email=data["email"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()

return jsonify({"id": user.id, "email": user.email}), 201

注册时添加前缀:
```python
app.register_blueprint(api_bp, url_prefix="/api/v1")

Critical Rules

关键规则

Always Do

必须遵守

  1. Use application factory pattern - Enables testing, avoids globals
  2. Put extensions in separate file - Prevents circular imports
  3. Import routes at bottom of blueprint
    __init__.py
    - After
    bp
    is created
  4. Use
    current_app
    not
    app
    - Inside request context
  5. Use
    with app.app_context()
    - When accessing db outside requests
  1. 使用应用工厂模式 - 支持测试,避免全局变量
  2. 将扩展存放在单独文件 - 避免循环导入
  3. 在Blueprint的
    __init__.py
    底部导入路由
    - 需在
    bp
    创建完成后导入
  4. 使用
    current_app
    而非
    app
    - 在请求上下文中使用
  5. 使用
    with app.app_context()
    - 在请求外访问数据库时使用

Never Do

禁止操作

  1. Never import
    app
    in models
    - Causes circular imports
  2. Never access
    db
    before app context
    - RuntimeError
  3. Never store secrets in code - Use environment variables
  4. Never use
    app.run()
    in production
    - Use Gunicorn
  5. Never skip CSRF protection - Keep Flask-WTF enabled

  1. 永远不要在模型中导入
    app
    - 会导致循环导入
  2. 永远不要在应用上下文外访问
    db
    - 会触发RuntimeError
  3. 永远不要在代码中存储密钥 - 使用环境变量
  4. 生产环境永远不要使用
    app.run()
    - 使用Gunicorn
  5. 永远不要跳过CSRF保护 - 保持Flask-WTF启用

Common Errors & Fixes

常见错误与修复

Circular Import Error

循环导入错误

Error:
ImportError: cannot import name 'X' from partially initialized module
Cause: Models importing app, app importing models
Fix: Use extensions.py pattern:
python
undefined
错误:
ImportError: cannot import name 'X' from partially initialized module
原因: 模型导入app,同时app导入模型
修复: 使用extensions.py模式:
python
undefined

WRONG - circular import

WRONG - circular import

app/init.py

app/init.py

from app.models import User # models.py imports db from here!
from app.models import User # models.py imports db from here!

RIGHT - deferred import

RIGHT - deferred import

app/init.py

app/init.py

def create_app(): # ... setup ... from app.models import User # Import inside factory
undefined
def create_app(): # ... setup ... from app.models import User # Import inside factory
undefined

Working Outside Application Context

应用上下文外操作

Error:
RuntimeError: Working outside of application context
Cause: Accessing
current_app
,
g
, or
db
outside request
Fix:
python
undefined
错误:
RuntimeError: Working outside of application context
原因: 在请求外访问
current_app
g
db
修复:
python
undefined

WRONG

WRONG

from app import create_app app = create_app() users = User.query.all() # No context!
from app import create_app app = create_app() users = User.query.all() # No context!

RIGHT

RIGHT

from app import create_app app = create_app() with app.app_context(): users = User.query.all() # Has context
undefined
from app import create_app app = create_app() with app.app_context(): users = User.query.all() # Has context
undefined

Blueprint Not Found

Blueprint未找到

Error:
werkzeug.routing.BuildError: Could not build url for endpoint
Cause: Using wrong blueprint prefix in
url_for()
Fix:
python
undefined
错误:
werkzeug.routing.BuildError: Could not build url for endpoint
原因: 在
url_for()
中使用了错误的Blueprint前缀
修复:
python
undefined

WRONG

WRONG

url_for("login")
url_for("login")

RIGHT - include blueprint name

RIGHT - include blueprint name

url_for("auth.login")
undefined
url_for("auth.login")
undefined

CSRF Token Missing

CSRF令牌缺失

Error:
Bad Request: The CSRF token is missing
Cause: Form submission without CSRF token
Fix: Include token in templates:
html
<form method="post">
    {{ form.hidden_tag() }}  <!-- Adds CSRF token -->
    <!-- form fields -->
</form>

错误:
Bad Request: The CSRF token is missing
原因: 表单提交时未携带CSRF令牌
修复: 在模板中包含令牌:
html
<form method="post">
    {{ form.hidden_tag() }}  <!-- Adds CSRF token -->
    <!-- form fields -->
</form>

Testing

测试

python
undefined
python
undefined

tests/conftest.py

tests/conftest.py

import pytest from app import create_app from app.extensions import db from config import TestingConfig
@pytest.fixture def app(): app = create_app(TestingConfig) with app.app_context(): db.create_all() yield app db.drop_all()
@pytest.fixture def client(app): return app.test_client()
@pytest.fixture def runner(app): return app.test_cli_runner()

```python
import pytest from app import create_app from app.extensions import db from config import TestingConfig
@pytest.fixture def app(): app = create_app(TestingConfig) with app.app_context(): db.create_all() yield app db.drop_all()
@pytest.fixture def client(app): return app.test_client()
@pytest.fixture def runner(app): return app.test_cli_runner()

```python

tests/test_main.py

tests/test_main.py

def test_index(client): response = client.get("/") assert response.status_code == 200
def test_register(client): response = client.post("/auth/register", data={ "email": "test@example.com", "password": "testpass123", "confirm": "testpass123", }, follow_redirects=True) assert response.status_code == 200

Run: `uv run pytest`

---
def test_index(client): response = client.get("/") assert response.status_code == 200
def test_register(client): response = client.post("/auth/register", data={ "email": "test@example.com", "password": "testpass123", "confirm": "testpass123", }, follow_redirects=True) assert response.status_code == 200

运行命令:`uv run pytest`

---

Deployment

部署

Development

开发环境

bash
flask --app run run --debug
bash
flask --app run run --debug

Production with Gunicorn

生产环境使用Gunicorn

bash
uv add gunicorn
uv run gunicorn -w 4 -b 0.0.0.0:8000 "run:app"
bash
uv add gunicorn
uv run gunicorn -w 4 -b 0.0.0.0:8000 "run:app"

Docker

Docker部署

dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY . .

RUN pip install uv && uv sync

EXPOSE 8000
CMD ["uv", "run", "gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"]
dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY . .

RUN pip install uv && uv sync

EXPOSE 8000
CMD ["uv", "run", "gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "run:app"]

Environment Variables (.env)

环境变量(.env)

SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_ENV=production

SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_ENV=production

References

参考资料


Last verified: 2026-01-21 | Skill version: 2.0.0 | Changes: Added 9 known issues (stream_with_context regression, async/gevent conflicts, test client sessions, threading context, Flask-Login session protection, CSRF cache, new 3.1.0 features, Werkzeug dependencies) Maintainer: Jezweb | jeremy@jezweb.net

最后验证日期: 2026-01-21 | 技能版本: 2.0.0 | 更新内容: 新增9种已知问题(stream_with_context回归、async/gevent冲突、测试客户端会话、线程上下文、Flask-Login会话保护、CSRF缓存、3.1.0新功能、Werkzeug依赖) 维护者: Jezweb | jeremy@jezweb.net",