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

179 lines
8.7 KiB
Python
Raw Permalink 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 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