Files
pj_service_cloud/docs/file-access/README.md
T
dzc bbde6cc87f fix:图片访问
fix:权限修改
2026-10-29 14:28:38 +08:00

12 KiB
Raw Blame History

文件访问签名接入与发布

本次保护 /admin/sys-file/oss/file 的全部文件。默认 observe 只记录签名校验结果;只有加载完整配置并切到 enforce 后,普通裸链接才会被拒绝。旧 Dart 图片兼容是独立开关。客户端密钥可提取、UA 可伪造、有效接口地址仍可被模拟请求使用,这是一层应用级校验,不是用户或租户文件授权。

配置准备

服务端配置见 config.example.yml。密钥为至少 32 字节随机数据的 Base64 字符串,HMAC 使用 Base64 解码后的字节。不要复用 OAuth 客户端信息或密码加密密钥。

在 pj_service_cloud 执行以下命令可为并列的五个项目生成匹配的本地配置:

python3 scripts/generate-file-access-config.py \
  --public-base-url https://www.pjzhhq.cn/api \
  --allowed-origin https://stgl.pj.gov.cn

按用户最终要求,所有环境共用一套密钥配置,admin/h5/mini/flutter/service/package 各自保留固定密钥;开发、测试、生产不再另建密钥组。只有公网基础地址等部署参数随目标环境填写。脚本统一读取 .file-access/keys.json,重复执行不会更换密钥;旧命令的 --environment 参数仍接受,但不再切换密钥。

迁移时沿用原生产密钥组,避免已构建客户端失效。脚本只更新受管理的文件访问配置字段,保留其他业务环境变量;移除旧 Vite 模式文件中的文件访问字段,避免其覆盖公共配置。旧文件备份位于 Java 项目的 .file-access/migration-backup/,不参与构建。当前活动配置均被 Git 忽略:

首次生成只做一次,其他构建机器和 CI 同步这套现有配置。密钥统一后无需随部署环境重新生成。

项目 本地文件 使用方式
Java .file-access/config.yml Nacos 共享配置或 Spring 外部配置
后台 .env.local 所有 Vite 构建模式共用
H5 和小程序 各项目的 .env.local 所有 Vite 构建模式共用
Flutter .file-access/config.json 所有构建使用 --dart-define-from-file=.file-access/config.json

前端的配置项为 VITE_FILE_ACCESS_APP_ID、VITE_FILE_ACCESS_KEY_ID、VITE_FILE_ACCESS_KEY、VITE_FILE_ACCESS_ALLOWED_ORIGINS。Flutter 去掉 VITE_ 前缀。Java 的 clients 包含 admin/h5/mini/flutter/service/package;service 和 package 密钥只放服务端。

public-base-url 是外部可访问的网关基础地址,可以带 /api,不含 /admin/sys-file/oss/file。现有的图片/API 基础地址继续由各端原有配置决定;allowed-origins / ALLOWED_ORIGINS 只列确实属于本系统的历史域名。上线前必须逐环境核对域名、基础路径、appId、kid 和密钥。相对 /api/admin/sys-file/oss/file 按当前文件基础地址解析,绝对历史地址保留其原有域名和部署路径。

Java 不会自动读取仓库里的 .file-access。 本地运行可以设置:

export SPRING_CONFIG_ADDITIONAL_LOCATION=file:/absolute/path/pj_service_cloud/.file-access/config.yml

通过现有 Nacos 配置体系加载这套 file-access 配置,或把外部配置挂载到各服务并设置上述变量。实际部署路径与公网基础地址应对应目标机器。首次生成默认为 observe,已有配置保留当前模式;显式 --mode 可更新本地生成配置的模式。先检查配置源优先级;config.example.yml 中的环境变量占位符不能代替实际密钥。严格模式缺少任一客户端的有效活动密钥或公网基础地址会启动失败;运行中错误配置会返回 500,不会降级匿名访问。即使在观察模式,版本签发和服务间读取也需要配置 package/service 密钥。

协议

请求查询参数为 appId、kid、ts、sign、fileName。签名原文按 LF 连接,无末尾换行:

v1
oss-file-read
appId
kid
ts
Base64URL无填充编码的UTF-8文件名

ts 是 Unix 秒;sign 是 HMAC-SHA256 的 64 位小写十六进制。fileName 只 URL 解码一次,始终放在最后。后端生成的 APK URL 因此仍以 .apk 结尾,兼容旧终端的扩展名解析。

默认普通签名有效 1800 秒,package 有效 86400 秒,允许签发时间领先最多 300 秒。GET /admin/sys-file/oss/access-info 匿名返回 R.data.serverTime(毫秒)和 ttlSeconds,不包含密钥。各端校准时差并在签名剩余不足 5 分钟时续期;校时失败保留上次时差,不能退回裸链接。传输 URL 缓存在内存里,普通渲染不会每次产生新签名。

重复关键参数、缺少字段、非规范时间戳或非规范签名为 400;未知客户端/密钥、错误签名、过期、时钟越界为 403;文件不存在为 404,数据库或存储读取失败为 500。GET 和 HEAD 共用校验。普通登录 Token 不会绕过校验。文件响应最多私有缓存 5 分钟且不超过剩余签名寿命;错误响应禁止缓存。已经输出部分字节后的网络/存储中断无法可靠改写 HTTP 状态,应按中断响应处理。

block-browser-navigation 默认为 true。严格模式下,浏览器携带 Sec-Fetch-Mode: navigate 或 Sec-Fetch-Dest: document/iframe/frame 的文件请求返回 403,即使签名正确也不放行;覆盖地址栏粘贴、新窗口打开和直接嵌入文件页面。图片、音视频以及 fetch/XHR 下载继续校验签名后访问。响应的 Vary 包含 Sec-Fetch-Mode 和 Sec-Fetch-Dest,防止浏览器把图片缓存复用为文件页面。观察模式仅记录 browser_navigation,不拦截。

后台会截获本系统文件 <a> 链接,通过已有 Blob 下载工具处理;H5/小程序富文本文件链接改走图片预览或下载后打开本地文件。复制出来的接口地址不再承诺可直接在浏览器打开。Flutter 原生下载、Feign 等没有浏览器 Fetch Metadata 的请求仍执行签名/旧 Dart 规则;不支持这些请求头的旧浏览器同样不能靠本规则识别。这项规则用于限制普通浏览器导航,不能防止脚本携带有效签名并模拟请求,也不能阻止已经获准显示的图片被保存。

入口与数据约束

部分 接入位置与行为
Java 文件入口 FileAccessGuard,SysFileController,SysFileServiceImpl;租户跳过只作用于文件查询并恢复原状态
Java 内部读取 RemoteFileService 保留 getFile 调用,通过编码桥接和专属 Feign 拦截器签名,保留文件名中的字面 %xx;钱包、硬件导出/日志/取证通过相同签名读取;票据 URLConnection 单独适配
APK 终端 /app-version/ad/page 和 /app/last-version 返回对象使用 package 签名,后台列表/编辑和数据库仍为原始值;独立静态 APK 地址保持不变
后台 src/utils/file-access*.ts;FileImage 保留已加载画面,未加载的懒图片继续续期,打开/切换预览时取有效签名;失败重试仅刷新当前文件,不清空全局签名缓存;上传回显、素材库、头像、视频、版本复制和下载均已接入;KKFileView 在编码之前签名源地址
后台编辑器 只更新编辑器 DOM 的资源属性,不替换整篇文档;输出 HTML 剥离临时签名,续期不进入编辑历史
H5 和小程序 src/sheep/utils/file-access*.ts、filePath/parseHtml、共享预览下载工具及各页面资源属性;公共算法不依赖 DOM/window;富文本图片样式保留
Flutter FileAccess、AppNetworkTool、SignedNetworkImage;主副屏独立初始化,缓存键用规范化原始地址,传输 URL 用签名
Flutter APK 重试 原生下载收到 403 时重新查询相同 ID、版本码、平台和应用的已发布版本,最多重试一次,不能换成另一个版本继续安装

业务对象、表单、上传返回值、SQLite 人脸 imagePath、失败记录和 referenceImageUrl 继续保留原始引用。本次没有数据库迁移、SQLite 升级或清空人脸库。新业务展示统一调用文件工具,不要在 API 响应拦截器里递归改写业务字符串。

第三方 URL、静态资源、data:、blob:、本地临时文件不加本协议签名。复制访问链接是临时访问地址;编辑、存储文件引用使用原始值。

旧终端兼容只接受完整 Dart/<版本> (dart:io) UA,且扩展名在 jpg/jpeg/png/gif/webp/bmp/svg/ico 内。完全没有签名参数才可走兼容分支;错误或过期签名不会回退。APK、日志、文档不凭 Dart UA 放行。

发布和回滚

  1. 在配置中心准备匹配的环境配置,保持 mode: observe 和 legacy-dart.enabled: true。发布 UPMS、integrated,以及包含文件调用的 wallet、hardware、canteen 等服务和统一公共依赖。只更新 UPMS 会遗漏内部读取。
  2. 发布后台、H5、小程序,并提供携带生产配置的新 Flutter 包。小程序保留原文件域名,真机检查 request/downloadFile 域名配置、图片预览和打开文档。
  3. 至少观察一个完整业务日,确认登录前 Logo/背景、人员照片、商品/菜品、富文本、附件下载、导出、票据和旧设备更新链路。旧设备必须实测图片 UA 和 Android 原生 APK 下载。
  4. 通过验收后设置 mode: enforce。按当前配置中心机制刷新或重启实例,逐个确认配置已生效;旧网页需刷新,小程序需加载新版本。
  5. 根据设备版本台账确认覆盖后再关 legacy-dart.enabled。没有自动失效日,短期没有旧请求不能证明设备已全部升级。
  6. 遗漏影响业务时切回 observe 并保留诊断;这会重新允许普通未签名请求。无需数据库回滚,也无需四端同时回退。

代理/CDN 必须把文件路径送到后端校验,保留完整查询参数,不得忽略查询参数缓存文件、强制公共缓存或通过其他公开存储入口绕过该接口。校时接口也必须匿名可达。不要在访问日志中记录完整签名 URL、密钥或登录 Token;可按路径、来源类别和结果计数。

监控 file.access.requests(Prometheus 通常呈现为 file_access_requests_total),标签 outcome/app/mode。结果包括 signed、missing_signature、malformed_signature、invalid_signature、expired、clock_skew、legacy_dart、unknown_client、unknown_key、configuration_error 等。文件查询/存储失败另计 file.access.read.failures。拒绝/兼容日志按结果采样,计数器保留完整次数。

轮换时先在服务端增加新 kid,同时保留旧 kid,再更新客户端构建;服务端主动签发使用 active-kid。Flutter 的旧 kid 需等设备覆盖后才移除。不要用重新运行生成脚本代替轮换流程。

本地验证命令

三个前端分别执行:

node --test tests/file-access.test.cjs

后台执行 pnpm build:prod,H5 执行 pnpm build:h5,小程序执行 pnpm build:mp-weixin。跨语言固定向量在各项目 test-vectors/file-access.json,其中的密钥是公开测试数据,不能用于部署。

Java 定向回归:

mvn -pl pjcloud-upms/pjcloud-upms-biz,pjcloud-integrated/pjcloud-integrated-biz,pjcloud-wallet/pjcloud-wallet-biz,pjcloud-hardware/pjcloud-hardware-biz,pjcloud-canteen/pjcloud-canteen-biz \
  -am test \
  -Dtest=FileAccessTest,FileReadTest,AppVersionFileAccessTest,WalletManageBatchExportTest,WalletLedgerBatchExportTest,HardBatchExportFileTest,HardBatchExportQueryTest \
  -Dsurefire.failIfNoSpecifiedTests=false

Flutter 使用项目 FVM 3.27.4:

fvm flutter test --no-pub --dart-define-from-file=.file-access/config.json \
  test/file_access_test.dart test/package_download_test.dart \
  test/face_sync_plan_test.dart test/face_sync_pipeline_test.dart \
  test/face_sync_failure_store_test.dart test/face_readiness_cache_test.dart \
  test/customer_display_balance_test.dart test/customer_display_balance_widget_test.dart
fvm flutter build apk --release --no-pub --dart-define-from-file=.file-access/config.json

验收结果与待现场验证项见 verification.md。不要把本地构建成功或源码中的 Dart UA 推断视为旧安装包验收。