Flask 的视图是函数,因此可以用 Python 装饰器包装它们,在进入业务逻辑前检查登录状态,或在返回结果后统一进行模板渲染。关键在于保留函数身份,明确包装顺序,并确认每一层真正承担的职责。
本文完整整理自 Pallets 的 Flask 官方文档 View Decorators,并合并短文 Caching。作者归属 Pallets / Flask 文档贡献者;核对日期为 2026 年 10 月 5 日,源站为 Flask 3.1.x 稳定文档。示例仅做静态审查,没有启动应用、安装扩展或运行请求测试。

用 wraps 保留被包装函数的身份
装饰器接收一个函数,创建另一个函数包住它,再返回这个新函数。包装后,真正被调用的对象已经换了。如果没有额外处理,函数名、文档字符串等元数据也会变成包装函数的属性。
因此,原文使用 functools.wraps(),把原函数的重要元数据复制到包装函数上。在 Flask 中,这也有实际意义:默认路由端点名称通常来自视图函数名。多个包装函数若都叫 decorated_function,会干扰注册与调试。所有下述包装器都保留 wraps(f)。
登录检查装饰器
设想一个只允许登录用户访问的视图:未登录时跳到登录页,已登录时调用原函数。原文有两个前提:登录端点名为 login;当前用户保存在 g.user,匿名用户对应 None。
from functools import wraps
from flask import g, request, redirect, url_for
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
*args 与 **kwargs 把路由参数继续传给原视图,使装饰器不依赖某一种函数签名。应用必须在处理请求时先初始化 g.user,例如由已有的身份加载逻辑完成;这个变量不会因为写了装饰器就自动存在。它也不是长期存放用户信息的全局对象。
使用时,route() 放在最外层,也就是源码中最上面的一行:
@app.route('/secret_page')
@login_required
def secret_page():
return '这是登录后可见的页面。'
相对原文的编辑修改:原文这里用 pass 表示视图内容省略;本稿改成返回字符串,避免读者把没有有效响应的示意函数直接当作可用视图。app 仍须由应用创建,身份加载与登录处理也仍需另外实现。
Python 按从下到上的顺序应用装饰器:先得到 login_required(secret_page),再把包装后的函数交给 route 注册。如果先注册原始视图,之后才在局部名字上套装饰器,路由保存的函数就可能没有经过预期包装。
把 next 带回登录请求,并限制回跳目标
未登录请求跳到登录页时,next 出现在 GET 查询参数中。登录表单通常以 POST 提交,要继续携带它,需要把它放进隐藏输入,再由登录处理代码从 request.form 取出。
原文问题与修订:源页隐藏输入只有 type 和 value,没有 name。没有名称的控件不会以 next 字段提交。下面补上 name="next":
<input
type="hidden"
name="next"
value="{{ request.args.get('next', '') }}"
/>
这段模板应在正常启用自动转义的 HTML 模板中渲染,不应把来自请求的值加上 safe 过滤器。隐藏字段仅表示浏览器界面不显示它,用户仍可修改内容,因此不能把该字段当作可信目标。
如果登录成功后直接执行 redirect(request.form['next']),攻击者可能构造外站地址,形成开放重定向。应用需要验证同源或采用更严格的内部目标允许列表。下面给出一个范围较小、规则明确的替代方案:只允许回到首页或受保护页面。
修订步骤一:把登录检查装饰器中的重定向行改成只携带路径:
return redirect(url_for('login', next=request.path))
修订步骤二:在登录处理函数“已经验证凭据并建立会话”的成功分支中,使用固定端点生成允许目标:
# 此片段放在登录成功分支内;不是完整登录视图。
allowed_next = {
url_for('index'),
url_for('secret_page'),
}
next_path = request.form.get('next', '')
target = next_path if next_path in allowed_next else url_for('index')
return redirect(target)
这个修订要求应用存在 index 和 secret_page 两个端点;它通过精确匹配限制目标,并且有意不保留任意查询参数、绝对 URL 或其他路径。与原文的 next=request.url 相比,回跳能力更窄,但不会依赖客户端提供的主机名。若业务需要更广的回跳范围,应按该应用路由与代理配置设计同源校验,不能去掉校验直接恢复任意 URL。
装饰器仍然不是完整登录系统。密码验证、会话建立、CSRF 防护、退出登录和服务器端授权检查都不在这个片段中;这些依赖必须由应用本身实现。
缓存装饰器:先明确缓存对象与键
如果某个计算耗时,而五分钟前的结果仍然足够新,可以把计算结果暂存一段时间。合并阅读的 Caching 短文说明:Flask 本身不提供缓存实现,Flask-Caching 等扩展可以提供多种后端,也允许实现自定义后端。
下面的官方示例假设已经有一个初始化好的 cache 对象,具备 get 和 set 方法。它不是从 Flask 自动导出的变量:
from functools import wraps
from flask import request
def cached(timeout=5 * 60, key='view/{}'):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
cache_key = key.format(request.path)
rv = cache.get(cache_key)
if rv is not None:
return rv
rv = f(*args, **kwargs)
cache.set(cache_key, rv, timeout=timeout)
return rv
return decorated_function
return decorator
这里比普通装饰器多一层函数:cached(...) 接收超时与键模板,返回真正的装饰器;装饰器再接收视图函数,生成包装函数。每次请求先用 request.path 生成缓存键,命中且值不是 None 就直接返回,否则执行原视图,把结果缓存指定时间。默认超时 5 * 60 表示五分钟,具体时间单位仍需与所用后端 API 对应。
该示例用 None 表示缓存未命中,所以无法区分“缓存里确实保存了 None”和“没有值”。不过,普通 Flask 视图本身也不应把 None 作为最终有效响应。实际缓存还要考虑后端能否保存该返回类型、缓存失效以及重复并发计算等行为;原文没有实现这些部分。
为什么不能直接给私人页面套用这个缓存
request.path 只含路径,不包含查询参数、请求方法、用户身份、租户、权限或语言。于是 /report?page=1 和 /report?page=2 会得到同一个键;两个不同用户访问同一路径,也会得到同一个键。若结果依赖这些条件,缓存可能返回错误数据,甚至把一个人的私人结果交给另一个人。
鉴权顺序和缓存隔离是两个独立要求。源码中应把登录检查写在缓存装饰器上方,让请求先经过检查。例如,包装顺序可表示为:
@app.route('/some_path')
@login_required
@cached()
def some_view():
return '示意响应'
这段仅说明顺序,不授权把原例的路径键用于私人内容。如果缓存位于登录检查外层,缓存命中时甚至可能直接返回,而不调用鉴权层;即使登录检查在前,两个已登录用户仍可能因为缓存键相同而串用结果。
使用原例键规则时,应把用途限制在经确认不依赖用户、查询参数或其他请求差异的公共读取结果,并明确允许的方法。私人响应通常应避免这种通用整页缓存,或由应用设计包含所需身份、租户、权限版本和输入条件的缓存键,以及失效策略。带会话 Cookie、个性化头部或其他请求专属信息的响应,更不能不加区分地共享。
让视图返回字典,由装饰器渲染模板
原文把模板装饰器这一模式追溯到 TurboGears 社区:视图只返回传给模板的字典,模板渲染由包装器统一完成。下面三种写法在各自独立使用时表达相同结果。不要把三个同名端点同时注册进同一个应用。
@app.route('/')
def index():
return render_template('index.html', value=42)
@app.route('/')
@templated('index.html')
def index():
return dict(value=42)
@app.route('/')
@templated()
def index():
return dict(value=42)
如果没有显式给模板名,装饰器采用当前端点名,把其中的点替换为斜杠,再追加 .html。例如端点 admin.index 对应 admin/index.html。实现如下:
from functools import wraps
from flask import request, render_template
def templated(template=None):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
template_name = template
if template_name is None:
template_name = (
f"{request.endpoint.replace('.', '/')}.html"
)
ctx = f(*args, **kwargs)
if ctx is None:
ctx = {}
elif not isinstance(ctx, dict):
return ctx
return render_template(template_name, **ctx)
return decorated_function
return decorator
返回值约定很重要:字典作为模板上下文;None 视为一个空字典;其他类型原样返回。因此视图仍能在某些分支返回 redirect(...)、字符串或 Flask 支持的其他响应形式,不会被强行当作字典展开。
模板名来自开发者传入的参数或已注册端点,而不是直接使用一个任意用户输入的文件路径。模板渲染也仍须遵守正常的自动转义规则。这个同步包装器并没有实现异步视图的等待逻辑;不要不经适配就把它套在异步函数上。
把 Werkzeug 路由端点映射到视图
若需要更灵活地使用 Werkzeug 路由系统,可以先用 Rule 定义规则与端点名,再用 Flask 的 endpoint 装饰器把这个名字关联到视图函数:
from flask import Flask
from werkzeug.routing import Rule
app = Flask(__name__)
app.url_map.add(Rule('/', endpoint='index'))
@app.endpoint('index')
def my_index():
return "Hello world"
这里的端点是 index,Python 函数名却是 my_index;两者通过显式注册关联起来。endpoint 负责端点到函数的映射,Rule 负责 URL 规则。这个注册过程本身不会额外添加登录检查或其他安全策略,仍需按应用要求组合相应包装器。
静态审核结论与适用范围
本文完整保留了登录、缓存、模板和端点四类模式,并合并缓存依赖说明。发现的具体问题已经分别标明:原隐藏输入缺少名称,回跳地址需要验证,路径缓存键不能覆盖个性化请求差异,包装顺序可能让缓存绕过鉴权。补充代码和原文差异已在相邻段落说明。
这些片段没有硬编码秘密,也没有 shell 命令或任意求值。应用依赖 g.user、缓存后端、登录处理、模板文件和路由配置仍必须正确初始化。本次没有验证真实请求、缓存命中、模板渲染或任何修订片段的运行结果;静态审核没有发现某类问题,也不等于证明没有漏洞。
来源与许可
主要来源:Pallets 的 View Decorators 与 Caching。版权归属 Pallets / Flask 文档贡献者。官方仓库采用 BSD 3-Clause。本稿对全文作中文翻译与编辑,保留作者、引用、许可,新增明确标识的修订和原创图。
BSD 3-Clause 许可声明(原文保留)
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.












暂无评论内容