13 KiB
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;行为一致。
修复/验证计划(按优先级)
- P1 路由集合与 API sync/seed:决定静态文件是否应纳入 API/route-table 同步。若要严格 GVA parity,应注册与 GVA 同形的
GET/HEADstatic wildcard,并以store-path作为 URL;若保留动态 NoRoute,应在 sync/seed 明确排除并补文档/测试。额外覆盖非空 router-prefix,并保留两端 Swagger 对静态路由的已验证口径。 - P1 FreshCasbin:让
freshCasbin调用实际策略刷新或至少执行数据库/缓存可用性检查;失败返回 GVA 相同的 code 7 envelope。增加故障注入测试。 - P1 router-prefix sync:明确数据库中的 API path 是带前缀还是无前缀;当前 Kra
NormalizeRoutePath去前缀,而 GVAGVA_ROUTERS直接保留前缀。选择兼容策略并覆盖初始化、syncApi、Casbin authorize 三条链路。 - P1 CORS 启用策略:让 CORS 是否安装由与 GVA 相同的部署开关决定,或把 Kra 的无条件 CORS 明确记录为专有契约;覆盖 strict/allow-all/OPTIONS。
- P2 版本头:兼容期双写
X-Gva-Version与X-Kra-Version,并增加 header contract test。 - P2 限流与黑名单故障:收窄限流到两个 POST route,统一/双读
GVA_SecLimitkey,并明确黑名单存储故障的 fail-open/fail-closed 行为。 - 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 集成测试。
- P3 文档兼容:决定是否把无前缀 Swagger
basePath从"/"调整为空字符串,并固定 snapshot 规则。