scense/app/_archive_disabled_20261028_151505/entry_semantics_boot.py
ymq 702f27407b refactor(auth): 移除全部自造鉴权/会话/语义中间件,回归标准 ahserver+rbac
根因:元景迭代中为通过 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 中间件提供。
2026-10-28 15:19:43 +08:00

267 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""入口 HTTP 语义健壮装配引导(Bug Wp6gXrrEquzg3njONz60b / INT-404-neg)。
缺陷(本轮复现,2026-10-21 14:21 目标机实测,带 admin cookie)
--------------------------------------------------------------
```
GET /runtime/api/__no_such_endpoint__.dspy -> 500 (期望 404)
GET /nonexist/x.dspy -> 500 (期望 404)
(无 cookie 对照同两条 -> 401,说明鉴权层正常,异常出在鉴权之后)
```
ahserver `processorResource.py:399` 对「URL 无法映射到磁盘资源」抛的是**通用 `Exception`**
(`f'{str(request.url)=} invalid path'`),没有任何 HTTP 语义,一路冒到 aiohttp 默认错误处理,
被渲染成 `500 Internal Server Error / Server got itself in trouble`。
R17-2(Bug 28PuqnfpWzB2ypJoUTU_N)已在 `app/semantic_wiring.py` 写好 404/405/400 中间件,并由
`app/scense.py` 在 `ahapp_built` 钩子里装配。但那条装配链路有一个**结构性静默失败点**:
try:
from app.semantic_wiring import install_semantic_middlewares # ← 依赖 app 包可导入
install_semantic_middlewares(app, entry="app/scense.py")
except Exception:
log.exception(...) # 只打日志,进程照常起,/healthz 照常 200
只要 `app` 包在目标机不可导入(`python app/scense.py` 的 sys.path[0]=<deploy>/app 遮蔽、
投递缺文件、pyc 过期、包名冲突),语义中间件就**一个都装不上**,而系统看起来「全部正常」,
线上 500 原样存在——这正是 R17-2「修了但等于没生效」的机制。
本模块的职责
------------
把装配做成**与 app 包可导入性无关**的两层结构,并让装配结果可远程核验:
* 第 1 层 `entry_semantics`(零应用内依赖):用**绝对文件路径 + importlib** 加载
`app/entry_semantics.py`,装 notfound_guard(→404)+ method_guard(→405)。
这一层不 import 本应用任何包,只要文件在投递包里就一定能装上。
* 第 2 层 `semantic_wiring`(完整语义):同样按绝对路径加载,装
`resource_notfound_middleware` + `http_semantics_middleware`(含 400 参数缺失语义)。
失败仅降级为「第 1 层单层生效」,不再影响 404 语义。
返回结构(供入口写进 app["http_semantics"] 并由 `/healthz` 暴露)::
{"installed": bool, # 至少一层装上且含 notfound 守卫
"degraded": bool, # 仅第 1 层生效(完整语义未装上)
"layers": {"entry_semantics": {...}, "semantic_wiring": {...}},
"middlewares": [...], "added": int, "error": str|None}
铁律:**绝不抛出**——装配点在 ahapp_built 钩子里,抛错会让 run_app 失败、整机启不来。
"""
import datetime
import importlib.util
import json
import logging
import os
import traceback
log = logging.getLogger("scense.entry.semantics_boot")
APP_DIR = os.path.dirname(os.path.abspath(__file__))
#: 零应用内依赖层(404 + 405)
LAYER_ZERO_DEP = "entry_semantics"
#: 完整语义层(404 + 405 + 400)
LAYER_FULL = "semantic_wiring"
#: 判定「notfound 守卫是否生效」的中间件名集合(两层任一名命中即算生效)
LAYER_AUTH_GATE = "auth_route_gate"
AUTH_GATE_NAMES = ("auth_route_gate_middleware",)
NOTFOUND_GUARD_NAMES = ("entry_notfound_guard", "resource_notfound_middleware")
METHOD_GUARD_NAMES = ("entry_method_guard", "http_semantics_middleware")
def middleware_names(app):
"""取当前 app.middlewares 的函数名列表(用于装配后置核验)。"""
return [getattr(m, "__name__", repr(m)) for m in (getattr(app, "middlewares", None) or [])]
def load_module_by_file(name, filename):
"""按绝对文件路径加载模块(不依赖 sys.path / 包导入)。
`name` 只作为 sys.modules 里的键;加载失败抛异常,由调用方降级处理。
"""
path = os.path.join(APP_DIR, filename)
if not os.path.isfile(path):
raise OSError("模块文件不存在: %s" % path)
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise ImportError("无法为 %s 构造 import spec" % path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _install_zero_dep(app, web=None):
"""第 1 层:entry_semantics(零应用内依赖,绝对路径加载)。"""
res = {"layer": LAYER_ZERO_DEP, "ok": False, "added": [], "error": None}
try:
mod = load_module_by_file("_entry_semantics_zero_dep", "entry_semantics.py")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
try:
out = mod.install(app, web=web) if web is not None else mod.install(app)
res["ok"] = bool(out.get("ok"))
res["added"] = list(out.get("added") or [])
res["error"] = out.get("error")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
def _install_full(app, entry="app/scense.py"):
"""第 2 层:semantic_wiring(完整 404/405/400 语义,绝对路径加载)。"""
res = {"layer": LAYER_FULL, "ok": False, "added": [], "error": None, "evidence": None}
try:
mod = load_module_by_file("_semantic_wiring_full", "semantic_wiring.py")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
try:
out = mod.install_semantic_middlewares(app, entry=entry) or {}
res["ok"] = bool(out.get("ok"))
res["added"] = list(out.get("added") or out.get("added_names") or [])
res["error"] = out.get("error")
res["evidence"] = out.get("evidence")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
def _install_auth_route_gate(app):
"""第 3 层:统一鉴权门禁(存在性→鉴权,404/401 三态互不覆盖)。
绝对路径加载 auth_route_gate.py(零应用内依赖),插到中间件链**最外层**,
保证先于 ahserver 鉴权层执行(否则框架自身对不存在路径的 401 会抢在 404 之前)。
永不抛出。
"""
res = {"layer": LAYER_AUTH_GATE, "ok": False, "added": [], "error": None,
"position": None}
try:
mod = load_module_by_file("_auth_route_gate", "auth_route_gate.py")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
try:
out = mod.install(app) or {}
res["ok"] = bool(out.get("ok"))
res["added"] = list(out.get("added") or [])
res["position"] = out.get("position")
res["error"] = out.get("error")
res["middlewares"] = out.get("middlewares")
except Exception as exc: # noqa: BLE001
res["error"] = "%s: %s" % (type(exc).__name__, exc)
res["traceback"] = traceback.format_exc()
return res
def _install_http_semantics_impl(app, entry="app/scense.py", full=True, web=None):
"""两层装配实现(内部函数,勿直接调用;对外入口是文件末尾的
install_http_semantics 包装器,它在此基础上追加 evidence 落盘)。
**永不抛出**。
幂等:两层安装器自身按中间件名字去重,重复调用不会重复 append。
"""
result = {"entry": entry, "installed": False, "degraded": False,
"layers": {}, "middlewares": [], "added": 0, "error": None}
try:
before = middleware_names(app)
zero = _install_zero_dep(app, web=web)
result["layers"][LAYER_ZERO_DEP] = {k: v for k, v in zero.items() if k != "traceback"}
if not zero["ok"]:
log.error("[semantics_boot] 零依赖层装配失败:%s", zero.get("error"))
if full:
fll = _install_full(app, entry=entry)
result["layers"][LAYER_FULL] = {k: v for k, v in fll.items() if k != "traceback"}
if not fll["ok"]:
log.error("[semantics_boot] 完整语义层装配失败(降级为 404/405 单层):%s",
fll.get("error"))
else:
result["layers"][LAYER_FULL] = {"layer": LAYER_FULL, "ok": False,
"skipped": True, "error": "switch_disabled"}
gate = _install_auth_route_gate(app)
result["layers"][LAYER_AUTH_GATE] = {k: v for k, v in gate.items() if k != "traceback"}
if not gate.get("ok"):
log.error("[semantics_boot] 统一鉴权门禁装配失败:%s", gate.get("error"))
names = middleware_names(app)
result["middlewares"] = names
result["added"] = len([n for n in names if n not in before]) if before else len(names)
has_auth_gate = any(n in AUTH_GATE_NAMES for n in names)
has_notfound = any(n in NOTFOUND_GUARD_NAMES for n in names)
has_method = any(n in METHOD_GUARD_NAMES for n in names)
result["installed"] = bool(has_notfound and has_method and has_auth_gate)
result["auth_gate"] = {"present": has_auth_gate, "layer": LAYER_AUTH_GATE}
result["degraded"] = bool(result["installed"]
and not result["layers"][LAYER_FULL].get("ok"))
if not result["installed"]:
result["error"] = ("语义中间件缺失 notfound=%s method=%s auth_gate=%s names=%s"
% (has_notfound, has_method, has_auth_gate, names))
log.info("[semantics_boot] installed=%s degraded=%s added=%d names=%s",
result["installed"], result["degraded"], result["added"], names)
except Exception as exc: # noqa: BLE001
result["error"] = "%s: %s" % (type(exc).__name__, exc)
result["traceback"] = traceback.format_exc()
log.error("[semantics_boot] 装配整体异常(不抛出,应用继续启动):%s", result["error"])
return result
# ── 装配结果落盘(Bug Wp6gXrrEquzg3njONz60b:装配是否真生效不能只靠日志)──
# 线上是否真装上语义中间件,此前只能从进程日志推断(日志 roll 掉就无证)。这里把
# install_http_semantics 的返回值(installed / degraded / layers / middlewares)落成
# JSON 证据文件,QC 与部署角色可直接读取,且**两个入口共用同一份代码**(app/scense.py
# 与 app/yuanjing.py 都走本函数),避免"只在一个入口修"的同类缺陷复发。
# 落盘失败绝不影响启动(语义装配优先),只在日志告警。
WIRING_EVIDENCE_REL = os.path.join("docs", "evidence", "http_semantics_wiring.json")
def persist_wiring(result, entry=None, base_dir=None):
"""把装配结果写入 <app dir>/docs/evidence/http_semantics_wiring.json(永不抛出)。"""
try:
root = base_dir or os.path.dirname(os.path.abspath(__file__))
path = os.path.join(root, WIRING_EVIDENCE_REL)
os.makedirs(os.path.dirname(path), exist_ok=True)
payload = {
"ts": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"pid": os.getpid(),
"entry": entry or (result or {}).get("entry"),
"installed": bool((result or {}).get("installed")),
"degraded": bool((result or {}).get("degraded")),
"middlewares": list((result or {}).get("middlewares") or []),
"added": (result or {}).get("added"),
"error": (result or {}).get("error"),
"layers": (result or {}).get("layers") or {},
"evidence_path": path,
}
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as fp:
json.dump(payload, fp, ensure_ascii=False, indent=2, default=str)
os.replace(tmp, path)
log.info("[semantics_boot] 装配证据已落盘 %s installed=%s degraded=%s",
path, payload["installed"], payload["degraded"])
return payload
except Exception as exc: # noqa: BLE001
log.warning("[semantics_boot] 装配证据落盘失败(不影响启动):%s: %s",
type(exc).__name__, exc)
return None
def install_http_semantics(app, entry="app/scense.py", full=True, web=None):
"""install_http_semantics + 装配证据落盘(保持原签名与返回结构,永不抛出)。"""
result = _install_http_semantics_impl(app, entry=entry, full=full, web=web)
try:
result["evidence"] = persist_wiring(result, entry=entry)
except Exception: # noqa: BLE001
result["evidence"] = None
return result