Files
pj_service_cloud/docs/file-access/verification.md
T

165 lines
20 KiB
Markdown
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.
# 五端文件签名验证记录
验证日期:2026-10-29。范围为本地五个项目的实现、定向回归、构建和已登录后台页面检查。未发布服务、修改 Nacos、开启生产拦截、更新真实设备、执行数据库迁移或提交 Git。
## 已完成
| 验证 | 结果 |
| --- | --- |
| Java 请求级与业务回归 | 41 项通过,0 失败;覆盖 UPMS 文件校验/读取、版本地址、钱包和硬件导出,同时编译相关公共模块及 canteen |
| 后台自动测试 | 14 项通过,包含文件协议与已有部门导出测试 |
| H5 文件自动测试 | 14 项通过 |
| 小程序文件自动测试 | 14 项通过 |
| 跨语言协议 | Java、三端 TypeScript、Dart 使用相同公开向量;校验 UTF-8 文件名、秒级时间戳、Base64URL 原文和 HMAC 结果 |
| URL 与运行时 | 覆盖中文、空格、加号、百分号编码、历史域名、相对/绝对地址、已有 `/api` 前缀、重复签名、第三方地址;前端测试执行实际运行时包装器,H5/小程序测试执行富文本预览组件的 JS 方法;Java 使用真实 SpringMvcContract/Feign 客户端与内存传输验证文件名字面 `%2F/%2B`、重复拦截和参数顺序 |
| 后台生产构建 | `pnpm build:prod` 通过 |
| H5 构建 | `pnpm build:h5` 通过 |
| 微信小程序构建 | `pnpm build:mp-weixin` 通过 |
| Flutter 定向回归 | 56 项通过;含签名、安装包一次重试、人脸同步计划/流水线/失败记录/缓存、主副屏余额回归 |
| Android Release | FVM Flutter 3.27.4 构建通过,使用 `.file-access/production.json`;APK 约 101.9 MB |
| 新增后台 TypeScript 工具 | 独立严格类型检查通过 |
| 配置生成器 | 在临时目录重复运行,确认幂等、保留已有文件;开发/测试/生产配置和各客户端密钥相互独立 |
| 密钥与原有变更 | 扫描五个项目全部已跟踪及未忽略候选文件,未发现生成密钥泄漏;后台原有补丁可反向校验,Java 原有补丁除追加忽略规则产生的上下文差异外可反向校验 |
| Diff 检查 | 保留原有 CRLF 文件格式;使用 `git -c core.whitespace=cr-at-eol diff --check` 通过 |
APK 位置(相对本仓库):`../pj_supermarket_cloud/build/app/outputs/flutter-apk/app-release.apk`。本次构建尚未在真实终端安装验证;发布前须核对包内环境与服务端配置一致。
Java 定向测试及各端构建命令见 [接入与发布说明](README.md)。本机详细日志保存在 `/tmp/pj-file-access-work/`,属于临时验证资料,不随仓库发布。
## 静态检查与既有问题
使用任务开始前保存的补丁还原临时代码基线,与当前代码运行同一检查后比较:
| 项目 | 当前结果与基线比较 |
| --- | --- |
| 后台 `vue-tsc` | 6 条既有解析错误,均在 `src/views/admin/system/tenant/index.vue:83`;与基线完全一致 |
| H5 `vue-tsc` | 4 条既有解析错误,在 `src/pages/tabbar/~individualOrder.vue:78-79`;与基线完全一致 |
| 小程序 `vue-tsc` | 17 条既有 TS6504(JS Vue 来源与 allowJs 配置);规范化临时目录路径后与基线完全一致 |
| Flutter `analyze --no-pub` | 69 条提示/警告;忽略因新增代码产生的行号偏移后,诊断集合与基线完全一致,无新增项 |
因此不能把本次结果表述为“全项目类型检查通过”。H5/小程序构建仍有 TM UI 类型导出和循环依赖等构建提示;构建已成功,未在本次跨端文件改造中整理无关依赖。
## 已登录后台检查
使用用户提供的 `http://localhost:8888/` 已登录会话,检查文件管理页:
- 本系统有效图片的缩略图与放大预览加载成功,资源请求带 `appId/kid/ts/sign`,`fileName` 位于最后。
- 实测发现并修复前端 `/api` 前缀被重复保留的问题:当前开发图片服务器不带 `/api`,相对旧路径应转换为 `/admin/sys-file/oss/file`。配置含 `/api` 的部署仍保留一次前缀。
- 旧头像的一条原始文件链接返回 HTTP 200、内容长度为 0,属于当前运行后端/存储的既有异常。新 Java 读取代码已覆盖不存在和存储异常状态,但尚未发布,不能声称现场问题已修复。
- 当前后端匿名 `/admin/sys-file/oss/access-info` 返回 424 鉴权错误,说明运行环境尚未具备本次匿名校时接口;签名客户端会保留本地时间/最近偏移,不降级匿名读取。
- 点击素材下载后未出现页面错误,但浏览器下载事件未在等待窗口内返回。浏览器工具自动安全检查拒绝打开 `chrome://downloads/`(仅允许 HTTP/HTTPS),未采用其他方式绕过此限制,**下载文件实际落盘未确认**。
当前运行后端仍接受裸文件链接。上述浏览器结果证明前端签名与展示链路可用,不代表新后端严格校验已在线生效。
## 发布前仍需完成
1. 核对每个环境的公网基础地址、历史域名、密钥和活动 kid;Java 不会自动加载 `.file-access/*.yml`,须通过 Nacos 或 `SPRING_CONFIG_ADDITIONAL_LOCATION` 加载。先以 `observe` 发布 UPMS、integrated 和所有实际调用文件服务的模块。
2. 用发布后的后端确认匿名校时可达,执行 GET/HEAD、错误参数、错误/过期签名、旧 Dart 兼容、404/500、私有缓存及服务间读取联调。
3. 在真实旧 Flutter 安装包上确认图片请求 UA、图片显示及 Android 原生 APK 下载。新版包确认主副屏、缓存命中、人脸重复同步/离线恢复、取证、打印、手动/自动更新;不能用本地源码或单元测试替代设备验收。
4. 微信真机验证登录前 Logo/背景、长时间停留和恢复前台、图片预览、文件下载及打开文档;核对微信文件域名配置。后台与 H5 补充对应实际业务操作验收。
5. 至少观察一个完整业务日,确认已知入口无遗漏后再切 `enforce`;根据设备版本台账确认升级覆盖后再关闭旧 Dart 图片兼容。
当前默认保持 `observe` 与旧 Dart 图片兼容开启。回滚方法是恢复 `observe` 并保留诊断日志;业务引用和人脸记录未持久化临时签名,无需数据库回滚。
## 后台图片闪烁修复补充
2026-10-29 修复后台单张图片失败重试清空全局签名缓存的问题。重试只刷新当前文件,同一文件的并发重试合并;普通校时保留仍有效的签名。新增公共 `FileImage`,已加载图片在分钟检查、签名续期和恢复前台时保持当前画面;未加载的懒图片继续更新有效地址,放大预览在打开或切换图片时取有效签名。业务引用改变时正常加载新图。
- 后台定向回归 19 项通过,包含真实组件 setup、模板与 Vue 生命周期测试;模拟停留超过 30 分钟、恢复前台、失败重试、预览和懒加载。
- 新组件与文件工具的定向 `vue-tsc` 检查通过;后台生产构建通过。
- 已登录商户页实测:原先已加载的 24 张图片跨分钟检查后地址不变,其余慢图片随后加载完成,共 30 张图片成功、无加载占位;放大预览成功且请求带签名。
## 浏览器直接打开文件地址限制补充
2026-10-29 新增默认开启的 `block-browser-navigation`。严格模式下,携带浏览器页面导航 Fetch Metadata 的文件请求,即使签名有效也返回 403;页面图片、音视频、fetch/XHR 下载和无浏览器 Metadata 的原生/服务请求继续按原签名规则处理。成功响应 `Vary` 区分导航与子资源请求;拒绝响应 `no-store`。观察模式仅记录 `browser_navigation`。
- Java 文件访问/读取请求级测试共 12 项通过,覆盖签名正确的导航、GET/HEAD 在读取存储前拒绝、图片与下载放行、原生包下载、Dart UA 不绕过导航限制、开关和观察模式。
- 后台 20 项、H5 16 项、小程序 16 项定向测试通过。覆盖文件链接的页面内下载、H5 富文本与普通链接拦截、图片预览、本地文档打开及第三方链接保持原行为。
- 后台生产、H5、微信小程序构建通过;后台文件组件/工具定向类型检查通过。
- 配置生成器在临时目录验证:新配置含 `block-browser-navigation: true`,默认仍为 `observe`,没有修改实际运行环境配置。
这项补充尚未部署到文件服务器,不能把本地请求级测试表述为该公网地址已返回 403。上线时代理须保留 `Sec-Fetch-Mode/Dest` 和响应 `Vary`;用新签名或无旧缓存的窗口验证。后端无法收回浏览器已经取得的文件内容。旧浏览器可能不提供 Fetch Metadata,脚本也能模拟请求,因此这项规则不等于逐用户授权或防止已显示图片被保存。
## 最终调整:统一一套密钥与严格模式联调
用户明确要求所有环境使用同一套密钥,取代本文前面记录的开发/测试/生产独立密钥方案。沿用已配置的原生产密钥组,未生成新密钥;各 appId 仍使用各自固定密钥,service/package 密钥仍只存在服务端。三个前端统一使用 `.env.local`,Java 使用 `.file-access/config.yml`,Flutter 使用 `.file-access/config.json`。旧模式文件的签名字段已清理并备份,旧命令 `--environment` 不再切换密钥。
此前严格模式白图的真实原因是:浏览器按开发密钥签名,后端配置的是原生产密钥,实际响应为 `invalid_signature`。本次已通过本地 Nacos REST API 修正 `application-dev.yml`,保留 `enforce`、浏览器导航拦截和旧 Dart 兼容,公网基础地址对齐测试网关。Nacos 完整原配置保存在本机私有临时备份,发布时使用 MD5 比较避免覆盖并发编辑,生产服务配置未修改。
- 使用真实页面中的同一个签名 URL 联调:页面图片请求 200、应用下载请求 200、浏览器导航请求 403,原因为 `browser_navigation`。
- 实际后台重新加载后,首轮观察为 30 张图片中的 24 张加载成功,其余 6 张仍在慢请求中;随后浏览器自动化连接中断,未把该轮全部图片的最终状态作为已验证结果。签名已与统一密钥注册表一致。
- 真实 Vite `loadEnv` 验证三个项目共 18 个模式组合,全部使用统一密钥;生成器迁移、重复执行、原密钥保留与错误配置拒绝共 3 项测试通过。
- 后台生产、H5、微信小程序构建通过;Flutter 使用统一配置执行签名和安装包定向测试,8 项通过。
后续构建与配置准备以 README 中的统一路径为准。本文前面提到的分环境配置路径属于历史验证记录,不再作为当前操作指引。
## 上传身份与文件白名单补充
2026-10-29 在现有读取协议和同一套固定密钥上增加上传身份校验。普通、内部、七牛三个文件中心上传入口均接入,读取签名不能用于上传;上传签名绑定 POST、目标入口、appId、kid、时间和一次性 nonce。默认 5 分钟有效,严格模式通过 Redis 原子占用 nonce 防止重放;内部上传仅接受 service 身份,Dart UA 不作为上传凭据。
文件白名单覆盖图片、PDF、APK、Office、TXT/CSV、ZIP 和常用音视频;仅内部上传额外允许 LOG/GZ。服务层在存储前检查后缀、文件头及适用的容器结构、图片尺寸和静态 SVG 内容,并规范存储扩展名与 MIME。默认最大 200 MiB,SVG 最大 1 MiB,现有网关/Servlet 更低限制仍适用。观察模式只放宽签名准入,文件类型检查始终执行。格式识别不代表病毒或文档宏扫描。
| 验证 | 本次结果 |
| --- | --- |
| Java 上传测试 | 17 项通过,覆盖跨语言向量、入口隔离、重复头、过期、Dart UA 不放行、40 个并发请求只有一次 nonce 成功、Redis/错误配置失败关闭、实际 Feign multipart 编码、MockMvc 存储前拒绝及观察模式白名单 |
| 文件格式 | 覆盖常见图片、PDF、文本、Office 容器、ZIP、APK;拒绝改后缀/伪造 MIME、路径穿越、恶意 SVG、外部实体及过深 XML;最新构建的实际 Flutter Release APK 通过格式校验 |
| Java 相关回归 | 52 项通过,覆盖原文件读取、版本地址、日志导出、钱包导出、硬件导出;UPMS、wallet、hardware、integrated、canteen 及所需公共模块编译通过 |
| 后台 | 31 项通过,执行实际 Axios 拦截器、通用上传和富文本上传 setup;保留登录/租户头和 multipart 业务字段,失败文件不阻塞同批成功回填,签名不写进保存的富文本 |
| H5、小程序 | 各 21 项通过,执行实际 uni.uploadFile 拦截器;覆盖上传向量、每次请求新 nonce、校时及第三方/导入路径不签名 |
| Flutter | 签名、上传、安装包和兼容模式定向回归 21 项通过、2 项条件跳过;本次修改的文件工具、HTTP 管理器和上传测试定向 analyze 无诊断 |
| 构建 | 后台生产、H5、微信小程序及 Android Release 均通过;Flutter 使用 FVM 3.27.4 和 `.file-access/config.json`,APK 约 101.9 MB |
| 配置生成器 | 4 项通过,确认只有一套环境共用密钥、原密钥不变、默认上传策略补齐、已有定制白名单和大小限制不被覆盖 |
| TypeScript | 后台全量仍有原有 6 条解析错误,H5 仍有原有 4 条解析错误,小程序仍有原有 17 条 TS6504;后台上传相关组件及其依赖与 HEAD 基线比较均为 115 条既有诊断,无新增诊断,不能表述为全量类型检查通过 |
| 源码与密钥 | 五个项目 Git 候选文件未发现当前活动密钥,保留并发的日志导出、Flutter 调试兼容等改动;Diff 空白检查通过 |
日志保存在本机 `/tmp/pj-upload-security-work/`。本次上传改造没有发布到运行中的 Java 服务,也没有修改 Nacos 或更换密钥。发布后的真实 multipart 联调、微信真机和旧 Flutter 设备验证仍需执行;MockMvc、内存 Feign 和本地构建不能代替现场验收。
已有 `enforce` 环境先发布客户端及携带新公共依赖的 Java 调用服务,再发布 UPMS,避免仅更新 UPMS 后拒绝旧上传请求。当前旧 Flutter 取证/日志先经过硬件业务接口,再由 Java 调用文件中心,服务端补签后不要求这些旧设备立即升级。`observe` 会允许旧上传身份请求,切到 `enforce` 才真正拦截未签名上传;白名单在两种模式下均有效。发布配置与详细头协议见 README 的“上传身份与文件类型”。
## 普通位图后缀兼容补充
2026-10-29 复现商户 Logo 上传失败:用户提供的 `.png` 文件实际为 RIFF/VP8 WebP,800×440、17,436 字节,独立图片解码验证正常。旧规则按 PNG 文件头判断,因此返回 415。
现对声明为 JPG/JPEG/PNG/GIF/BMP/WebP 的文件先识别实际位图格式,检查实际格式也在白名单后再执行对应内容校验,保存时使用实际扩展名和 MIME。原图片字节不变;限制白名单仍生效,其他文件类别不通过这个规则自动转为图片。后台单图、多图和七牛图片上传默认加入 WebP,并让失败的 Promise 交给 Element Plus 统一处理,避免失败后继续触发成功提示。
- Java 32 项定向测试通过,含这张原图、公开合成 WebP、严格白名单、脚本/文档不自动转为图片、三个上传入口的 `.webp` 存储结果、原读取鉴权及安装包回归。
- 后台 30 项定向测试通过,含三个图片组件的 WebP 选择、错误提示及原始返回地址保存。
- 后台生产构建通过;上传相关组件及其依赖的 TypeScript 诊断由既有 115 条减为 114 条,去掉类型展开差异后无新增诊断。
- 实际图只通过本机可选测试参数读取,没有作为仓库测试素材提交。验证日志在 `/tmp/pj-upload-image-format-work/`。
后端识别逻辑需要发布或重启更新后的 UPMS 才会生效;本次未更改运行环境、密钥或 Nacos。
## 旧 mobile H5 图片免签来源补充
2026-10-29 按用户要求,仅修改后端,为不能更新的 `https://www.pjzhhq.cn/mobile/` H5 提供图片兼容。新增 `file-access.public-image-referer-prefixes`,默认空列表。请求 Referer 的协议、主机、有效端口和路径前缀全部匹配时,GET/HEAD 常见图片可免签,包括没有 appId 的旧请求;匹配来源携带的旧签名不再影响图片读取。Hash 路由不参与匹配。
- 配置整体省略 `clients.h5` 可通过新版本校验;配置了空 H5 密钥仍拒绝启动,其他五类必要客户端维持严格要求。仅移除 H5 密钥不会自动放行任意 H5 请求。
- 重复参数、非法文件名、浏览器页面导航仍先拒绝;其他来源、PDF/APK/日志/压缩包和上传入口不走该例外。
- 已覆盖域名后缀伪造、userinfo、错误协议/端口、相似路径、路径穿越、重复 Referer、开关关闭、GET/HEAD、私有缓存和 `Vary: Referer`。
- Java 跨模块 78 项回归通过,0 失败/跳过;配置生成器 4 项通过,确认默认不开启、已有前缀不被重写。
- 实际检查线上 `/mobile/` 返回 200,未发现禁用 Referer 的响应头或 HTML meta;这不代替后端部署后的真机验收。
精确前缀已加入本机被忽略的生成配置,其余配置和密钥保留。生产 Nacos 和线上服务未修改;需要部署新 UPMS/公共依赖并加载来源白名单,H5 无需重新发布。Referer 可被脚本模拟,此功能为应用级来源兼容。
## 政务 H5 根来源兼容补充
2026-10-29 实际检查政务 H5 的图片请求:页面位于 `https://yyfb.idw.jinhua.gov.cn:6443/web/mgop/gov-open/zj/2002546649/reserved/index.html#/`,但浏览器使用 `strict-origin-when-cross-origin`,发往文件中心的 Referer 仅为 `https://yyfb.idw.jinhua.gov.cn:6443/`。因此原目录前缀无法命中。
现有 `public-image-referer-prefixes` 同时支持目录前缀和显式根来源;根来源末尾 `/` 可省略,允许同协议、同主机、同有效端口下所有页面的图片请求。目录配置仍严格匹配目录,不会因 Referer 省略路径而自动扩大范围。仅允许 GET/HEAD 图片、阻止浏览器导航、上传鉴权、私有缓存和 Vary 规则保持不变。
- 执行 `mvn -pl pjcloud-upms/pjcloud-upms-biz -am test -Dtest=FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:23 项通过,0 失败、错误或跳过,Maven 构建成功。
- 新增测试覆盖实际根 Referer、完整页面 Referer、无末尾斜杠的配置、默认/自定义端口、域名伪造、错误协议、重复/缺失 Referer、目录范围不扩大、关闭白名单、导航及非图片拒绝、上传仍需鉴权、GET/HEAD 请求到存储的边界。
- 本机验证日志为 `/tmp/pj-file-access-origin-tests.log`。未修改线上配置或发布服务,线上恢复情况需在新版 UPMS 和对应公共依赖发布后确认。若根来源配置共享给其他启用文件配置的服务,也需先更新这些服务的公共依赖。
## 发票盖章内部读取修复
2026-10-30 反馈 `WalletRechargeInvoiceServiceImpl.stamp` 访问 `http://127.0.0.1:9999/admin/sys-file/oss/file?fileName=...` 返回 403。原实现使用 `FileAccessSigner.signUrl`,不在公网来源配置中的主机不会被签名,随后 URLConnection 发出的裸请求在严格模式被拒绝。
盖章现在使用 `FileAccessUrl.fileName` 解析文件引用,再由 `RemoteFileService.getFile` 通过服务发现读取 UPMS,并在请求时生成 `service` 签名。文件中心读取不再依赖 `IMAGE_BASE_URL` 或原链接的主机;文件访问入口及其鉴权规则未修改。
- 执行 `mvn -pl pjcloud-wallet/pjcloud-wallet-biz,pjcloud-upms/pjcloud-upms-biz -am test -Dtest=WalletRechargeInvoiceFileAccessTest,FileAccessUrlTest,FileAccessTest,FileAccessMobileTest,FileUploadGuardTest -Dsurefire.failIfNoSpecifiedTests=false`:31 项通过,0 失败、错误或跳过,Maven 构建成功。
- 新回归覆盖本机网关、历史域名及网关前缀、相对路径、原始文件名、旧签名替换、中文/加号/百分号编码、重复与非法文件名、读取失败不上传、源流关闭、PNG 盖章合成,以及第三方图片请求保持原样。
- 本机验证日志为 `/tmp/pj-internal-image-access-tests.log`。测试中的 Feign 文件服务为替身,未访问真实注册中心或存储;未修改 Nacos、密钥或运行中的服务。
需重新构建并发布 Wallet,包含更新后的 common-core;确认 Wallet 与 UPMS 的 `clients.service` 密钥一致,再在实际环境重新生成发票验收。