85 lines
13 KiB
Markdown
85 lines
13 KiB
Markdown
# GVA 横切接口对照审计
|
||
|
||
基准:`C:/Users/Yvan/AppData/Local/Temp/gva-parity-02f3783`(commit `02f37833255e0e339c3d69199cb5a468f17de9fc`,2026-08-06)。
|
||
范围:只审查已迁移后台接口共用的 HTTP/router/middleware 行为;未迁移的代码生成、AI 等模块不计入缺口。
|
||
`GET /sysError/getSysErrorSolution` 调用 LLMAuto,按要求排除。
|
||
|
||
## 结论摘要
|
||
|
||
| 项 | 结论 | 严重度 |
|
||
|---|---|---|
|
||
| 路由实际集合 | 业务路由方法和路径基本对齐;本地 `engine.Routes()` 不含动态本地静态文件,而 GVA `StaticFS` 会注册静态 `GET`/`HEAD` wildcard | P1(API sync/route-table 可见集合) |
|
||
| 静态文件 URL | GVA 以 `local.store-path` 注册静态路由;本地 NoRoute 以 `local.path-prefix` 动态处理。默认值相同,配置分离时行为不同 | P1(配置不一致时) |
|
||
| Swagger `basePath` | GVA 生成文档的 `docs.SwaggerInfo.BasePath` 在无前缀时为空字符串;本地动态文档把空前缀规范化为 `/`。带 `/admin` 前缀时两端都输出 `/admin` | P3(严格文档/客户端比较) |
|
||
| 统一响应/状态码 | 成功/业务失败均 HTTP 200 + `{code,data,msg}`;未登录 401;强制改密 409;panic 500;限流 200,和 GVA 一致 | 对齐 |
|
||
| 请求绑定错误 | `ShouldBindJSON/Query` 错误均转换为 HTTP 200、code 7、data `{}`;与 GVA 一致 | 对齐 |
|
||
| JWT/刷新/多点登录 | 黑名单、临近过期刷新、cookie/`new-token` 头、多点登录旧 token 作废逻辑对齐;本地用持久化黑名单并 singleflight,属于实现差异 | P2(实现/故障语义需回归) |
|
||
| MustChangePwd | 允许 `/user/changePassword`、`/user/getUserInfo`、`/jwt/jsonInBlacklist`,其余 409/code 10001;对齐 | 对齐 |
|
||
| Casbin | 本地每次从 `casbin_rule` 读取并直接 Enforce;GVA 使用内存 enforcer。`freshCasbin` 本地恒成功,GVA `LoadPolicy` 失败会返回 code 7 | P1 |
|
||
| DataScope | 顺序均为 JWT -> MustChangePwd -> Casbin -> DataScope;本地额外注入 actor/request metadata,不改变已迁移接口授权规则 | 对齐(增强) |
|
||
| CORS | GVA 基准的 `Cors/CorsByRules` 在 `router.go` 中均为注释、默认未安装;本地无条件安装并按 runtime 执行 allow-all/whitelist/strict。默认 `whitelist` 空配置时差异不显著,但 strict/allow-all 会改变可达性与响应头 | P1(启用配置时行为不兼容) |
|
||
| 登录/验证码限流 | GVA 仅在 `POST /base/login`、`POST /base/captcha` 路由挂限流;本地是全局 middleware,按 URL 后缀匹配,未匹配路由或其他方法也可能计数。超限 HTTP 200/code 7、缓存异常 fail-open;另有 key 前缀 `KRA_SecLimit` vs `GVA_SecLimit` | P2(作用面与跨版本缓存兼容) |
|
||
| request/trace metadata | RequestMeta 最先执行,`X-Request-Id`、`X-Trace-Id`、W3C `traceparent` 的校验/回写顺序一致 | 对齐 |
|
||
| access log | 单一 body/response capture、脱敏、1 MiB response cap、route/status/bytes/identity/trace 字段一致;版本头不同 | P2 |
|
||
| operation audit | 已迁移的 GVA `OperationRecord` 分组路由均被本地 `operationRoutes` 覆盖;AI `getSysErrorSolution` 排除 | 对齐(需防漏项测试) |
|
||
| error audit | panic/错误级日志走单一 error sink;本地响应 envelope 的未预期业务失败由 `ErrorAudit` 转为 error log;GVA 由 logger core 捕捉 error level | 对齐(实现不同) |
|
||
| panic recovery | broken-pipe 仅记录并 abort,普通 panic 返回 500;对齐 | 对齐 |
|
||
| API sync/init seed | 本地初始化/同步使用 `engine.Routes()`;本地会去掉 router-prefix,GVA 基准直接使用带 prefix 的 `GVA_ROUTERS`。静态 routes 缺失叠加影响 sync/seed | P1(自定义 prefix) |
|
||
|
||
## 逐项核验
|
||
|
||
### 1. Router 与静态文件
|
||
|
||
GVA `server/initialize/router.go` 的顺序为 `RequestMeta -> GinRecovery -> AccessLog -> UploadResponseHeaders -> StaticFS -> Swagger -> Public/Private`;Private 依次安装 `JWTAuth -> MustChangePwdGuard -> CasbinHandler -> DataScope`。本地 `internal/server/gin.go` 为 `RequestMeta -> Recovery -> AccessLog -> CORS -> ErrorAudit -> SecurityRateLimit`,再建立 Public/Private;Private 依次为 `Auth -> MustChangePassword -> AccessControl -> OperationAudit`。已迁移业务 handler 的路径/HTTP method 与路由契约测试一致。
|
||
|
||
本地没有调用 `StaticFS`,而是在 `NoRoute` 中用 `serveLocalStorage` 按 `local.path-prefix` 动态匹配;因此动态静态文件不会出现在 `engine.Routes()` 或 `/api/syncApi` 输入中。GVA `StaticFS(global.GVA_CONFIG.Local.StorePath, ...)` 会注册静态 wildcard 的 GET 和 HEAD,并进入 `GVA_ROUTERS`。默认配置两者都为 `uploads/file`,但如果 `store-path != path-prefix`,两端暴露的 URL 集合不同。基准 GVA 的预生成 Swagger 文档也没有 `/uploads/file/*filepath`,而 Kra 的动态 Swagger 同样不会把 NoRoute fallback 当作文档路由,因此 Swagger 本身不单独计为已确认差异。静态响应头(`nosniff`,非安全媒体 `Content-Disposition: attachment`)的效果一致;本地额外拒绝目录。
|
||
|
||
Swagger 文档的根路径还有一个低等级差异:基准 `server/docs/docs.go` 的 `BasePath` 默认值为空字符串,`server/initialize/router.go` 仅在配置有前缀时写入;本地 `internal/server/swagger.go:59-63` 将空前缀强制写成 `"/"`。这通常不会改变浏览器请求,但会影响严格 JSON 快照或按 `basePath` 拼接 URL 的客户端。
|
||
|
||
**测试建议**:构造临时目录和 `store-path != path-prefix`;断言 GVA 期望的 `GET`/`HEAD /<store-path>/*filepath` 是否进入 route table 与 sync diff,且确认两端 Swagger 文档的静态路由口径;断言本地动态静态响应仍支持 GET/HEAD、目录 404/403、头部一致。
|
||
|
||
### 2. 统一响应与绑定错误
|
||
|
||
本地 `internal/server/httpx/response.go` 与 GVA `server/model/common/response/response.go` 均定义 `CodeSuccess=0`、`CodeError=7`、`CodePasswordChangeRequired=10001`,正常/业务失败均 `HTTP 200` envelope。`NoAuth` 是 `401` 且 `data:null`;MustChangePwd 是 `409`;Recovery 普通 panic 是 `500` 空响应;CORS 拒绝 `403`;限流是 `200` + code 7。handler 统一在 `ShouldBindJSON/ShouldBindQuery` 失败时写 code 7,未引入 Gin 默认 400。
|
||
|
||
**测试建议**:每类 endpoint 发送 malformed JSON、类型错误 query、缺字段 JSON,比较 status、code、data 和 msg;覆盖 401/409/403/429-like(实际 200)/500。
|
||
|
||
### 3. JWT、刷新和多点登录
|
||
|
||
GVA `middleware/jwt.go` 先检查 blacklist,再解析 token;临近过期时签发新 token,并回写 `new-token`、`new-expires-at` 和 cookie。登录在多点模式下将旧 Redis JWT 加入 blacklist,再写入新 active token。Kra `middleware/auth.go` 通过 `AuthService.AuthenticateToken` 完成同一顺序,并在刷新时回写相同 headers/cookie;登录 usecase 的 `RotateActiveToken` 在多点模式作废旧 token,刷新则传空 old token 保持旧 token 不被提前作废。Kra 用 `jwt_blacklists` 持久化查询并对同一 token 使用 singleflight,GVA 使用无错误返回的缓存 blacklist;因此数据库故障时 Kra 会把所有 token 映射为“异地登陆或令牌失效”并拒绝请求,而 GVA 仍可能继续 JWT 解析。这是故障语义差异,需在部署策略和回归测试中明确。
|
||
|
||
### 4. MustChangePwd、Casbin、DataScope
|
||
|
||
两端允许列表完全一致:`/user/changePassword`、`/user/getUserInfo`、`/jwt/jsonInBlacklist`。Kra `AccessControl` 会去掉 router-prefix 后按 `authority_id + path + method` 授权,并在同一 request context 注入 DataScope/Actor;GVA `CasbinHandler` 做相同 prefix trim,随后 `DataScope` 注入数据域身份。Kra 的授权实现每请求读取数据库并构造 Casbin model,GVA 使用全局 enforcer 缓存;所以 Kra 写策略后即时可见,而 GVA 依赖 `FreshCasbin` 或写路径主动 reload。
|
||
|
||
`GET /api/freshCasbin` 是明确缺口:GVA 调 `e.LoadPolicy()`,失败返回 HTTP 200/code 7/“刷新失败”;Kra handler 直接 HTTP 200/code 0/“刷新成功”,且不会暴露加载失败。即便 Kra 当前无内存 enforcer,该接口的可观察错误语义仍不一致。
|
||
|
||
### 5. CORS、限流与 metadata
|
||
|
||
CORS 代码实现本身的 allow-all、strict-whitelist、匹配 origin、OPTIONS 204、`GET /health` 例外和头部名称与 GVA helper 相近,但基准 `server/initialize/router.go` 将 `Cors/CorsByRules` 两行都保留为注释,默认并未安装该 middleware;Kra 在 `NewGinEngine` 中无条件安装并每次读取 runtime。因此在 `strict-whitelist` 或 `allow-all` 配置下,Kra 会拒绝/放行并添加 CORS 头,而基准仍按普通路由处理。若部署契约要求启用 CORS,应在基准与本地明确同样的安装开关;否则应把该差异视为 P1。
|
||
|
||
登录/验证码限流的窗口、超限响应、缓存异常 fail-open 一致,但作用面不完全相同:GVA 只在 `POST /base/login`、`POST /base/captcha` 路由挂 `SecurityLimit()`,Kra 则在全局 middleware 中按 URL 后缀匹配,未匹配路由或其他方法也可能读取/递增计数。两端 key 前缀仍不同:Kra `KRA_SecLimit`,GVA `GVA_SecLimit`。滚动部署共享缓存时会导致计数不共享,建议统一前缀或明确迁移策略。
|
||
|
||
RequestMeta 两端都位于最前,生成/透传 request id、trace id、span id,并按合法 `traceparent` 优先、`X-Trace-Id` 回退;响应头和 context 字段一致。AccessLog 都只读一次 body、包装一次 writer、脱敏敏感 JSON/header、响应捕获上限 1 MiB,并记录 route/status/latency/bytes/identity/request/trace metadata。
|
||
|
||
版本响应头是第二个明确可见差异:GVA 写 `X-Gva-Version`,Kra 写 `X-Kra-Version`。如果前端、网关或探针按 GVA header 读取版本,这会造成兼容性缺口;建议双写过渡或在契约中明确新 header。
|
||
|
||
### 6. Operation/Error audit 与 recovery
|
||
|
||
GVA 通过各资源 router group 的 `Use(middleware.OperationRecord())` 选择写操作;Kra 在全局 Private 后通过 `recordsOperation` 白名单按 method+route suffix 选择,已迁移的 user/api/authority/menu/department/position/dictionary/params/security/system/token/version/export/error/login-log/data-access/timed-task/info/email 路径均有覆盖。`sysError/getSysErrorSolution` 属 LLMAuto,按要求不纳入。
|
||
|
||
两端 operation audit 均复用 AccessLog 的 request/response capture,GET 将 query 转为 JSON,multipart 写 `[文件]`,下载响应超限写 `[超出记录长度]`,并记录 request/trace/device metadata。GVA logger core 和 Kra logging error sink 都会将 Error 级别日志落入 `sys_error`;Kra `ErrorAudit` 额外把未预期的 code 7 envelope 提升为 error log,并排除已知客户端失败、sysError/logViewer 自记录,避免重复。
|
||
|
||
Recovery 对 broken pipe 只记录并 abort,普通 panic 记录请求/stack 并返回 500;行为一致。
|
||
|
||
## 修复/验证计划(按优先级)
|
||
|
||
1. **P1 路由集合与 API sync/seed**:决定静态文件是否应纳入 API/route-table 同步。若要严格 GVA parity,应注册与 GVA 同形的 `GET`/`HEAD` static wildcard,并以 `store-path` 作为 URL;若保留动态 NoRoute,应在 sync/seed 明确排除并补文档/测试。额外覆盖非空 router-prefix,并保留两端 Swagger 对静态路由的已验证口径。
|
||
2. **P1 FreshCasbin**:让 `freshCasbin` 调用实际策略刷新或至少执行数据库/缓存可用性检查;失败返回 GVA 相同的 code 7 envelope。增加故障注入测试。
|
||
3. **P1 router-prefix sync**:明确数据库中的 API path 是带前缀还是无前缀;当前 Kra `NormalizeRoutePath` 去前缀,而 GVA `GVA_ROUTERS` 直接保留前缀。选择兼容策略并覆盖初始化、`syncApi`、Casbin authorize 三条链路。
|
||
4. **P1 CORS 启用策略**:让 CORS 是否安装由与 GVA 相同的部署开关决定,或把 Kra 的无条件 CORS 明确记录为专有契约;覆盖 strict/allow-all/OPTIONS。
|
||
5. **P2 版本头**:兼容期双写 `X-Gva-Version` 与 `X-Kra-Version`,并增加 header contract test。
|
||
6. **P2 限流与黑名单故障**:收窄限流到两个 POST route,统一/双读 `GVA_SecLimit` key,并明确黑名单存储故障的 fail-open/fail-closed 行为。
|
||
7. **P2 横切回归**:运行 malformed binding、JWT expired/refresh/multipoint/**blacklist-store failure**、MustChangePwd allow-list、Casbin deny/fresh failure、DataScope ownership、CORS strict/OPTIONS、rate-limit fail-open、GET/HEAD static、operation/error audit、panic/broken-pipe 的 HTTP 集成测试。
|
||
8. **P3 文档兼容**:决定是否把无前缀 Swagger `basePath` 从 `"/"` 调整为空字符串,并固定 snapshot 规则。
|