原文:Flask 官方文档 Application Dispatching;署名:Pallets / Flask 文档贡献者。本文为经授权中文译写,依据 2026 年 10 月 5 日读取的 Flask 3.1.x stable 文档,并把安全修订与原文示例明确区分。
应用分发,是在 WSGI 层把多个应用组合成一个入口的过程。可组合的对象不限于 Flask:任何符合 WSGI 接口的应用都可以接入,因此也能让 Django 和 Flask 应用在同一个 Python 解释器中并排运行。是否适合这样组织,取决于应用内部怎样管理配置、状态和资源。
这与把一个大型 Flask 应用拆成多个 Python 包不同。这里组合的是多个独立的应用实例,它们可以是同一个应用工厂创建的不同实例,也可以完全是不同应用;每个实例有自己的配置,入口在 WSGI 层决定把请求交给哪一个。
隔离边界:原文用“完全隔离”强调应用配置与 Flask 实例的独立性。它并不意味着进程、权限或故障隔离:这些实例仍可能共享解释器、进程内模块和资源。需要租户级安全隔离或独立故障边界时,应另行设计进程与部署架构。

怎样使用本文中的 application
下面每种方法最终都会得到一个名为 application 的 WSGI 可调用对象,它可以交给任意兼容的 WSGI 服务器。开发阶段可使用 flask run 启动开发服务器;生产部署请按官方部署指南配置正式 WSGI 服务。调试服务器不应直接承担生产入口。
先看一个最小 Flask 应用:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
Flask 实例自身就是 WSGI 应用。这是它可以被中间件包装,也可以与其他框架的 WSGI 应用共同分发的原因。后续示例中的 frontend_app、backend_app、myapplication 都是占位模块名,需要在自己的项目中实现,不能把文档片段当作现成完整仓库。
按固定路径组合应用
如果已经有两个独立应用,希望让它们在同一 Python 进程内并排工作,可以使用 Werkzeug 的 DispatcherMiddleware。它根据 URL 路径前缀选择应用,把多个 WSGI 应用组成一个更大的 WSGI 应用。
例如,让前台作为默认应用处理 /,后台挂载到 /backend:
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, {
'/backend': backend
})
请求匹配 /backend 挂载点时,会交给 backend;其他路径由默认的 frontend 处理。中间件会相应调整 WSGI 的 SCRIPT_NAME 与 PATH_INFO,让子应用按其挂载位置处理路由。这里的 /backend 是 URL 挂载前缀,不是给 backend 内所有路由手动再加一遍前缀。
原文订正:正文说明中仍出现旧名称 werkzeug.wsgi.DispatcherMiddleware,但原文代码和现行 Werkzeug 文档均使用 werkzeug.middleware.dispatcher.DispatcherMiddleware。本文统一采用后者。固定路由集合通常直接使用这个中间件即可。
按子域名动态创建应用
有时需要为同一个应用创建多个实例,各自使用不同配置。只要把应用创建逻辑放在函数中,就可以按需多次调用;具体组织方式可参考应用工厂模式。
常见的场景是“每个子域名对应一个用户或租户”。Web 服务器先把这些子域名的请求送到同一个 WSGI 入口,分发器读取主机名,再寻找对应的应用实例。如果实例尚不存在,就调用工厂创建并缓存,后续请求复用它。
WSGI 层适合承担这项工作:它先检查请求,再把 environ 和 start_response 原样交给被选中的子应用。下面完整保留原文的结构示例。
安全提示:此原版示例用于说明动态创建与缓存,不能直接作为生产 Host 校验。后文列出的域名边界、assert 和缓存问题都实际存在,不能忽略。
from threading import Lock
class SubdomainDispatcher:
def __init__(self, domain, create_app):
self.domain = domain
self.create_app = create_app
self.lock = Lock()
self.instances = {}
def get_application(self, host):
host = host.split(':')[0]
assert host.endswith(self.domain), 'Configuration error'
subdomain = host[:-len(self.domain)].rstrip('.')
with self.lock:
app = self.instances.get(subdomain)
if app is None:
app = self.create_app(subdomain)
self.instances[subdomain] = app
return app
def __call__(self, environ, start_response):
app = self.get_application(environ['HTTP_HOST'])
return app(environ, start_response)
对应的创建函数可以按子域名查询用户:
from myapplication import create_app, get_user_for_subdomain
from werkzeug.exceptions import NotFound
def make_app(subdomain):
user = get_user_for_subdomain(subdomain)
if user is None:
# 没有对应用户时,仍需返回一个可处理请求的WSGI应用。
# NotFound实例可作为WSGI应用,产生默认404响应。
return NotFound()
return create_app(user)
application = SubdomainDispatcher('example.com', make_app)
get_user_for_subdomain 和 create_app 都需要由应用自行实现。未知子域名时,NotFound() 作为 WSGI 应用返回 404;原文也提到可以改成重定向到主站。正常情况下则根据用户配置创建子应用。
原示例的四个边界
- Host 来自请求,不能盲信。badexample.com 也满足 endswith(“example.com”),但并不是 example.com 的子域。正确的域名关系至少需要精确匹配根域或检查点分隔边界,并处理规范化、端口、允许的标签等问题。
- assert 不是输入校验。Python 使用 -O 优化时可以移除 assert;即使启用,它也不是合适的可控 HTTP 拒绝响应。原来的 split(‘:’)[0] 也不是通用的 authority 解析方式,不能正确覆盖 IPv6 等形式。
- 原缓存可能无限增长。每个陌生子域名都可能创建并缓存一个 NotFound 对象;如果允许未知租户创建应用,还会造成更昂贵的资源消耗。必须在创建与缓存之前限定合法租户,必要时设置容量与生命周期策略。
- 锁只保护当前进程。threading.Lock 防止该分发器实例中的并发创建;多个 WSGI 工作进程各有自己的缓存和锁。工厂在锁内执行,较慢的创建过程会串行阻塞其他创建请求。
编辑修订:有限 Host 白名单
如果合法租户数量可枚举,可以采用更直接的办法:在配置中明确列出允许的 Host 与租户键,拒绝其他值。下面是编辑补充的替代结构,目的在于消除原版的后缀误匹配与未知 Host 缓存;它不是原文代码,也未作运行验证。
from threading import Lock
from werkzeug.exceptions import BadRequest, NotFound
class AllowedHostDispatcher:
def __init__(self, host_to_tenant, create_app):
self.host_to_tenant = dict(host_to_tenant)
self.create_app = create_app
self.lock = Lock()
self.instances = {}
def __call__(self, environ, start_response):
# 配置仅使用明确的ASCII域名及允许端口;未知形式直接拒绝。
host = environ.get('HTTP_HOST', '').lower()
tenant = self.host_to_tenant.get(host)
if tenant is None:
return BadRequest('Unrecognized host')(environ, start_response)
with self.lock:
app = self.instances.get(tenant)
if app is None:
app = self.create_app(tenant)
if app is None:
return NotFound()(environ, start_response)
self.instances[tenant] = app
return app(environ, start_response)
# 示例白名单;由部署配置管理,不能由请求参数填充。
application = AllowedHostDispatcher({
'alice.example.com': 'alice',
'alice.example.com:443': 'alice',
'bob.example.com': 'bob',
'bob.example.com:443': 'bob',
}, make_app)
这份示例只把 ASCII 主机名转为小写,并执行精确白名单查找。它有意拒绝未列出的端口、尾随点、国际化域名表示和其他 authority 形式;若业务需要这些形式,须在配置阶段建立统一规范并配套校验,不能简单“清洗”任意输入后放行。缓存键来自有限白名单,因此不会因陌生 Host 无限扩张;仍需根据每个应用的成本评估可承载租户数量。
Host 路由只决定请求交给哪个应用,不会自动完成身份验证或租户授权。子应用仍需检查当前用户能否访问该租户;反向代理层也应限制 Host,并仅在可信代理配置下处理转发头。Flask 3.1 的 TRUSTED_HOSTS 可对应用自身请求进行 Host 校验,但外层分发器已经先读取了 Host,因此它不能替代外层创建实例前的检查。
按路径动态分发
按 URL 路径分发与按子域名分发很相似:不读取 Host,而是查看路径的第一个分段,决定由哪个应用处理。下面是原文的完整结构:
from threading import Lock
from wsgiref.util import shift_path_info
class PathDispatcher:
def __init__(self, default_app, create_app):
self.default_app = default_app
self.create_app = create_app
self.lock = Lock()
self.instances = {}
def get_application(self, prefix):
with self.lock:
app = self.instances.get(prefix)
if app is None:
app = self.create_app(prefix)
if app is not None:
self.instances[prefix] = app
return app
def __call__(self, environ, start_response):
app = self.get_application(_peek_path_info(environ))
if app is not None:
shift_path_info(environ)
else:
app = self.default_app
return app(environ, start_response)
def _peek_path_info(environ):
segments = environ.get('PATH_INFO', '').lstrip('/').split('/', 1)
if segments:
return segments[0]
return None
_peek_path_info 只查看第一个路径段,暂时不修改请求环境。找到对应子应用后,shift_path_info 才把这个分段从 PATH_INFO 移到 SCRIPT_NAME,让子应用接收到剩余路径。如果工厂返回 None,则交给 default_app,且不会先消费路径前缀。
这就是它与前面的子域名例子的关键区别:路径分发允许回退到默认应用,而原子域名例子的工厂始终要返回某个 WSGI 应用。
from myapplication import create_app, default_app, get_user_for_prefix
def make_app(prefix):
user = get_user_for_prefix(prefix)
if user is not None:
return create_app(user)
application = PathDispatcher(default_app, make_app)
这里的 get_user_for_prefix 也由应用提供。根路径或空 PATH_INFO 经过原示例的 split 处理后会得到空字符串前缀,而不是一定得到 None;工厂应把空前缀作为明确的默认或未匹配情况处理。路径键应经过业务层合法性与权限检查,不能直接拿任意 URL 分段创建新租户。
原路径版本只缓存非 None 的成功结果,避免了把每个失败前缀永久缓存下来,但仍可能反复触发昂贵的用户查询。实际部署应限定有效前缀,并考虑请求限流、工厂成本、成功实例的数量及回收策略。多进程缓存和线程锁的限制同样适用。
把分发与业务边界分清楚
三个例子分别展示了固定挂载点、按子域名创建实例、按路径创建实例。无论采用哪种方式,外层得到的仍然是一个 WSGI 应用。固定映射可以直接使用 DispatcherMiddleware;动态方案则需要自行完成合法路由键的解析、应用工厂、缓存与错误响应。
独立应用实例适合需要独立配置的场景。如果只是给一个应用拆模块,而配置、扩展和应用上下文本来就需要共享,Flask 蓝图与包组织通常是另一种结构选择。本文不把“多实例”当成自动获得权限隔离的手段。
审查说明:全部原版代码与编辑修订仅作静态阅读;未启动 Flask、未运行 Python 或 WSGI 服务、未发送 Host 攻击请求。没有把示例中的占位函数说成已经实现。生产使用前仍需验证代理配置、URL 生成、租户权限、并发创建、缓存生命周期及多进程行为。
署名与许可:Pallets / Flask 文档贡献者;原文见文首链接。插图为编辑自绘,安全补充与替代代码已标明。相关 Flask 许可信息见官方许可页。
适用代码许可
以下为原示例适用的完整 BSD-3-Clause 许可与版权声明;中文说明和编校增补已标明。
BSD-3-Clause License Copyright 2010 Pallets Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.













暂无评论内容