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,是它消除了什么:

  1. 心智负担归零:调用方按「请求真正走过的顺序」从上往下写列表,所见即所得。框架的「反着注册」被装配件吸收,永远不许泄漏到业务侧;
  2. 顺序正确性变成可 review 的:列表顺序 = 执行顺序,code review 时一眼能看出「日志在 CORS 里面/外面」这类问题;原来要人脑做一次反转推理,出错率高;
  3. 注释即文档:那段注册顺序/执行顺序对照表直接写在函数 docstring 里。中间件顺序是最容易「上次还能跑这次诡异了」的配置,把契约写死在装配处最可靠。

2.1 五层中间件各自干什么(一句话版)

request_context   最外层:建请求上下文(request_id、用户身份 → ContextVar,见 00 章)
cors              跨域头
auth              身份/权限校验
logging           请求/响应日志
request_store     最内层:请求级数据存取(配合上下文)

注意 ①⑤ 与 00 章 ContextVar 的呼应:最外层 set 上下文、最内层存数据,路由函数里随处 get——中间件洋葱正是 ContextVar 的 set/reset 生命周期。

三、可迁移的套路

  1. 横切关注点三选一:装饰器声明(鉴权)、依赖注入(框架 Depends)、中间件(全站级)——按「作用范围」选,单接口用装饰器、一组路由用依赖、全站用中间件;
  2. 框架反直觉的机制,写一个 3 行的装配适配函数消化掉,让调用侧永远用直觉顺序;
  3. 顺序敏感的配置,把「执行顺序契约」写成注释/常量列表,review 时按列表对,不靠脑内反转。

↓下一步:05 章 · Pydantic v2、StrEnum 与注册表工厂

进入 keel 阅读