根因:元景迭代中为通过 QC 门禁反复叠加了 24 个自定义中间件
(auth_gate_strict / session_gate / login_param_source / route_guard /
entry_semantics / http_semantics / notfound / get_only_405 / ...),
导致标准登录链路被破坏:
- auth_gate_strict 手写 Fernet 解密 + 用 aiohttp_session 的会话创建时间
created 按 1 天判过期 → 所有受保护端点带 cookie 仍 401
- route_guard 的 _has_business_params 用 await request.read() 消费 POST body
→ up_login 收到 params_kw={} → password_encode(None) 崩 500
改动:
- app/scense.py 回归 57 行标准入口:ServerEnv + load_appbase/load_rbac/
load_pybricks + 业务模块 load_*,零自定义中间件
- 24 个自造中间件归档到 app/_archive_disabled_20261028_151505/(可回滚)
- /healthz 改用 ahserver 标准 FunctionProcessor(RegisterFunction 注册 +
conf.website.startswiths 映射),不再用中间件拦截
实测闭环(127.0.0.1:9380 + https://gbak.opencomputing.cn):
healthz 200 | 匿名 401 | admin 登录 200+cookie | 带cookie 200 |
伪造cookie 401 | 注册 200+自动登录 | 新用户登录 200+cookie |
新用户访问 403(已认证无权限,RBAC 正确行为)| 登出 200 |
错误密码 200 业务信封(不崩 500)
register/login/logout/session 全部由 rbac 模块 up_login.dspy /
register.dspy / logout.dspy + ahserver session 中间件提供。
179 lines
8.7 KiB
Python
179 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""HTTP 语义中间件的**装配**(Bug 28PuqnfpWzB2ypJoUTU_N / R17-2)。
|
||
|
||
问题
|
||
----
|
||
404/405/400 三类语义修复写在 `apps/yuanjing/app/http_semantics_middleware.py`,
|
||
但目标机实际运行入口是 `app/scense.py`(`ps -ef` 只有一个
|
||
`venv/bin/python app/scense.py` 进程),`apps/yuanjing` 侧的装配代码
|
||
(`app/yuanjing.py` 的 `_register_http_semantics`)从未执行,conf/config.json 的
|
||
`middlewares` 也是 null → 语义修复在目标机从未生效:
|
||
不存在端点仍 500、DELETE/PUT 仍 200、缺必填参数仍 200/500。
|
||
|
||
本模块把「装配」抽成**与入口无关**的一个函数,任何入口(scense.py / yuanjing.py)
|
||
都能调用:
|
||
|
||
from app.semantic_wiring import install_semantic_middlewares
|
||
install_semantic_middlewares(app) # aiohttp Application
|
||
|
||
装配顺序(= append 顺序,外层优先)
|
||
-----------------------------------
|
||
1. resource_preflight_middleware —— 路由前置 404(Bug P-Wn8N / WXXvX):
|
||
在鉴权之前判定 wwwroot 下资源是否存在,不存在直接 404(fpath is None 兜底前置化)。
|
||
2. auth_required_middleware —— 未登录访问已存在 .dspy → 401(Bug WhOYy9nvG):
|
||
端点若不在公开白名单且无会话凭据,直接 401,不再放行到业务处理器。
|
||
3. write_endpoint_semantics_middleware —— 写类端点 GET → 405 / POST 无参 → 400
|
||
(Bug CQIpIo / YSNA7 / Zhpd9 的 26+ 条 sqlor 1364 泛化 500)。
|
||
4. resource_notfound_middleware —— 异常侧 404 兜底(保留原有覆盖)。
|
||
5. http_semantics_middleware —— 405/400/404 既有实现(保留原有覆盖)。
|
||
6. business_status_middleware —— 业务信封 {"status":400,...} 回写 HTTP 状态行
|
||
(Bug DGzTjOkr / F12 / F16)。
|
||
|
||
注:真实入口 `app/scense.py` 的构造期装配只按名字加载两项
|
||
(`resource_notfound_middleware` / `http_semantics_middleware`),因此
|
||
`resource_notfound_middleware` 自身已内建**组合链**(`compose_guard_chain()`),
|
||
把上面 1/2/3/6 四层串在自己内部 —— 两条装配路径(构造期 / 钩子期)都能拿到完整语义。
|
||
本模块列全 6 项是为了在钩子路径下按名称核验「语义层是否就位」;重复装配无副作用
|
||
(幂等:405/401/404 命中即早返回,400 回写只在 status==200 时生效)。
|
||
|
||
契约
|
||
----
|
||
* 幂等:重复调用不会重复 append(按函数对象去重)。
|
||
* 容错:任一注册失败只打印 ERROR + traceback 并返回 ok=False,**不抛出**——
|
||
装配点在 ahapp_built 钩子里,抛错会让 run_app 失败、整机启不起来。
|
||
* 可核验:装配结果落成进程内记录 + `logs/http_semantics_wiring.json` 证据文件
|
||
(middlewares 条数 / 名字 / 是否含全部语义中间件),供部署后自查与复验取证。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
import os
|
||
import traceback
|
||
|
||
log = logging.getLogger("yuanjing.semantic_wiring")
|
||
|
||
EVIDENCE_BASENAME = "http_semantics_wiring.json"
|
||
|
||
# 本应用负责装配的语义中间件(顺序 = append 顺序 = 外层优先)
|
||
SEMANTIC_MIDDLEWARE_NAMES = (
|
||
"resource_notfound_middleware",
|
||
"http_semantics_middleware",
|
||
"business_status_middleware",
|
||
)
|
||
|
||
|
||
def _mw_name(mw):
|
||
return getattr(mw, "__name__", getattr(mw, "__qualname__", repr(mw)))
|
||
|
||
|
||
def _evidence_path():
|
||
app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
return os.path.join(app_root, "logs", EVIDENCE_BASENAME)
|
||
|
||
|
||
def write_evidence(payload):
|
||
"""把装配结果写到 logs/http_semantics_wiring.json(失败不影响启动)。"""
|
||
path = _evidence_path()
|
||
try:
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, "w", encoding="utf-8") as fp:
|
||
json.dump(payload, fp, ensure_ascii=False, indent=2)
|
||
return path
|
||
except OSError as exc: # noqa: PERF203
|
||
log.error("[wiring] 证据文件写入失败 %s: %s", path, exc)
|
||
return None
|
||
|
||
|
||
def _load_middlewares():
|
||
"""导入全部语义中间件(顺序即装配顺序)。"""
|
||
from app.route_guard_middleware import (
|
||
auth_required_middleware,
|
||
resource_preflight_middleware,
|
||
write_endpoint_semantics_middleware,
|
||
)
|
||
from app.notfound_middleware import resource_notfound_middleware
|
||
from app.http_semantics_middleware import http_semantics_middleware
|
||
from app.business_status_middleware import business_status_middleware
|
||
# [fix Zhpd9MXyeGtznDMqini6X / R50] 响应契约守卫必须装配在**链尾(最内层)**:
|
||
# 只有贴住路由处理器才能看到「处理器返回值本身」。R50 目标机日志实测链尾
|
||
# 返回的是 function 对象(aiohttp_session/__init__.py:196
|
||
# `RuntimeError: Expect response, not <class 'function'>`),异常上抛后被
|
||
# 改写成鉴权表象,导致 38 条 500 一条也判不出来。守卫在链尾救援该调用并
|
||
# 把契约破坏归因为带处理器身份的 500(见 app/response_contract_guard.py)。
|
||
from app.response_contract_guard import response_contract_guard
|
||
return (resource_preflight_middleware,
|
||
auth_required_middleware,
|
||
write_endpoint_semantics_middleware,
|
||
resource_notfound_middleware,
|
||
http_semantics_middleware,
|
||
business_status_middleware,
|
||
response_contract_guard)
|
||
|
||
|
||
def install_semantic_middlewares(app, entry=None):
|
||
"""把 404/401/405/400 语义中间件装进 aiohttp app,返回装配结果 dict。
|
||
|
||
:param app: aiohttp Application(通常来自 ahapp_built 钩子)
|
||
:param entry: 入口名(仅用于证据/日志,如 "app/scense.py")
|
||
:return: {"ok": bool, "middlewares": int, "names": [...], "missing": [...], "entry": ...}
|
||
"""
|
||
result = {"ok": False, "middlewares": 0, "names": [], "missing": [],
|
||
"entry": entry or "", "error": ""}
|
||
try:
|
||
# `python app/scense.py` 时 sys.path[0] 是 app/ 子目录,
|
||
# `from app.notfound_middleware import ...` 需要应用根在 sys.path 上。
|
||
import sys
|
||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||
app_root = os.path.dirname(app_dir)
|
||
for p in (app_root, app_dir):
|
||
if p not in sys.path:
|
||
sys.path.insert(0, p)
|
||
|
||
middlewares = _load_middlewares()
|
||
|
||
have = {_mw_name(m) for m in (getattr(app, "middlewares", None) or [])}
|
||
added = []
|
||
# aiohttp 的 Application.middlewares 只有 property、**没有 setter**:
|
||
# 旧实现 `app.middlewares = mws` 必然 AttributeError,被 except 吞掉后
|
||
# 装配"看似完成"实则未生效(Bug Wp6gXrrEquzg3njONz60b 的静默降级路径)。
|
||
# 正确做法逐条 append;app 已 freeze(_make_handler 已构建中间件链)时
|
||
# append 会抛 RuntimeError —— 必须显式标记 frozen 由调用方降级,不得当成功。
|
||
try:
|
||
for mw in middlewares:
|
||
if _mw_name(mw) in have:
|
||
continue
|
||
app.middlewares.append(mw)
|
||
have.add(_mw_name(mw))
|
||
added.append(_mw_name(mw))
|
||
except RuntimeError as exc:
|
||
present = [_mw_name(m) for m in (getattr(app, "middlewares", None) or [])]
|
||
result["frozen"] = True
|
||
result["names"] = present
|
||
result["middlewares"] = len(present)
|
||
result["missing"] = [n for n in SEMANTIC_MIDDLEWARE_NAMES if n not in present]
|
||
result["error"] = "app 已 freeze,中间件链已构建,append 不生效:%s" % (exc,)
|
||
log.error("[wiring] app 已 freeze,语义中间件无法生效 entry=%s: %s", entry, exc)
|
||
written = write_evidence(result)
|
||
if written:
|
||
result["evidence"] = written
|
||
return result
|
||
|
||
names = [_mw_name(m) for m in app.middlewares]
|
||
missing = [n for n in SEMANTIC_MIDDLEWARE_NAMES if n not in names]
|
||
result.update(ok=not missing, middlewares=len(names), names=names,
|
||
missing=missing, added=added)
|
||
if missing:
|
||
log.error("[wiring] 语义中间件缺失 %s(已装 %s)入口=%s", missing, names, entry)
|
||
else:
|
||
log.info("[init] middlewares=%d semantic=%d added=%s entry=%s",
|
||
len(names), len(SEMANTIC_MIDDLEWARE_NAMES), added, entry)
|
||
except Exception as exc: # noqa: BLE001
|
||
result["error"] = "%s: %s" % (type(exc).__name__, exc)
|
||
log.error("[wiring] 装配失败 entry=%s: %s", entry, exc)
|
||
log.error("[wiring] traceback: %s", traceback.format_exc())
|
||
written = write_evidence(result)
|
||
if written:
|
||
result["evidence"] = written
|
||
return result
|