KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
04 · 声明式鉴权与中间件管道:把「横切关注点」从业务里抽出去 — keel 龙骨
问题一:每个接口都要鉴权,但鉴权逻辑写在函数体里,一千个接口就有一千种漏写的可能。问题二:中间件的执行顺序反直觉,注册顺序和执行顺序是反的,谁来管?生产代码的答案:声明(装饰器/checkers)+ 一层把「反直觉」消化掉的管道装配函数。
问题一:每个接口都要鉴权,但鉴权逻辑写在函数体里,一千个接口就有一千种漏写的可能。问题二:中间件的执行顺序反直觉,注册顺序和执行顺序是反的,谁来管?生产代码的答案:声明(装饰器/checkers)+ 一层把「反直觉」消化掉的管道装配函数。
一、声明式鉴权:requires 参数
@route("POST", "/apps/{app_id}/run", requires=[
PermissionChecker(
resource="agent",
id_from="path.app_id", # 声明:资源 ID 在路径参数里
level="edit",
)
])
def handle_run(app_id: str, request: RunRequest):
staff_id = get_current_staff_id() # 函数体里没有任何鉴权代码
...
函数体里一行鉴权都没有。鉴权以「声明」的形式挂在装饰器参数上:资源类型是 agent、资源 ID 从 path.app_id 取、要求 edit 权限。框架在进入函数前统一执行 checkers。
和命令式(函数体内手写鉴权)对比:
| 命令式 | 声明式 | |
|---|---|---|
| 漏写风险 | 每个接口都可能漏 | 忘了声明就默认无鉴权(要靠审计兜底),但规则集中可批量审计 |
| 规则复用 | 复制粘贴 | 同一条 Checker 声明贴在 N 个接口上 |
| 改权限 | 找遍所有函数体 | 改声明即可 |
| 特殊逻辑 | 想怎么写怎么写 | 复杂场景退回 skip_auth=True + 函数内自检(如 SSE 端点用 staff_id 做归属校验) |
可迁移的套路:横切关注点(鉴权、限流、审计)优先做成装饰器参数/依赖注入这种「声明」,让业务函数保持纯业务;声明覆盖不了的复杂场景,留一个显式的逃生口(skip_auth)并配审计。
二、中间件管道:把「倒着注册」消化在装配函数里
很多 Web 框架的中间件是洋葱模型,注册顺序和执行顺序相反——后注册的在最外层、最先执行。这是出了名的反直觉。
def _register_middleware_pipeline(
app,
pipeline: list,
) -> None:
"""
按「执行顺序」注册中间件管道。
洋葱模型的规则是「后注册 = 最外层 = 最先执行」,
本函数接收按执行顺序(外→内)排列的列表,内部自动反转后注册,
从而消除调用方需要"倒着写"的心智负担。
"""
for setup_fn in reversed(pipeline): # ← 唯一的机关:reversed
setup_fn(app)
def setup_middlewares(app) -> None:
"""
注册顺序(从上到下): 实际请求执行顺序(从外到内):
1. request_context ← 最外层 7. request_store ← 最内层
2. cors 6. logging
...(略)
"""
_register_middleware_pipeline(app, [
setup_request_context, # 按人脑直觉的顺序写
setup_cors,
setup_auth,
setup_logging,
setup_request_store,
])
这十几行值得学的不是 reversed,是它消除了什么:
- 心智负担归零:调用方按「请求真正走过的顺序」从上往下写列表,所见即所得。框架的「反着注册」被装配件吸收,永远不许泄漏到业务侧;
- 顺序正确性变成可 review 的:列表顺序 = 执行顺序,code review 时一眼能看出「日志在 CORS 里面/外面」这类问题;原来要人脑做一次反转推理,出错率高;
- 注释即文档:那段注册顺序/执行顺序对照表直接写在函数 docstring 里。中间件顺序是最容易「上次还能跑这次诡异了」的配置,把契约写死在装配处最可靠。
2.1 五层中间件各自干什么(一句话版)
request_context 最外层:建请求上下文(request_id、用户身份 → ContextVar,见 00 章)
cors 跨域头
auth 身份/权限校验
logging 请求/响应日志
request_store 最内层:请求级数据存取(配合上下文)
注意 ①⑤ 与 00 章 ContextVar 的呼应:最外层 set 上下文、最内层存数据,路由函数里随处 get——中间件洋葱正是 ContextVar 的 set/reset 生命周期。
三、可迁移的套路
- 横切关注点三选一:装饰器声明(鉴权)、依赖注入(框架 Depends)、中间件(全站级)——按「作用范围」选,单接口用装饰器、一组路由用依赖、全站用中间件;
- 框架反直觉的机制,写一个 3 行的装配适配函数消化掉,让调用侧永远用直觉顺序;
- 顺序敏感的配置,把「执行顺序契约」写成注释/常量列表,review 时按列表对,不靠脑内反转。