Skip to content

OSS 文件存储与安全访问

sz-admin 将“文件存在哪里”和“谁能访问文件”分开处理:

层次职责不负责
sz-common-ossMinIO、阿里云 OSS、腾讯云 COS、七牛云等 S3 兼容存储适配业务权限
sz-common-resource场景、上传、命名、存储读写、ResourceRef、统一文件流响应具体业务记录的访问判断
业务模块固定下载/预览路由、接口权限、数据范围、记录与资源关系解析或代理调用方 URL

sys_resource 是受管资源元数据的权威来源。服务端不会根据前端提交的 URL、objectKey、sceneCode 去发起任意网络请求。

访问模式

模式适用场景accessUrl前端行为
DIRECT头像、Logo、公开富文本图片稳定公开地址浏览器直接访问
PRESIGNED私有 OSS 中无需业务权限的短时展示短时签名地址浏览器直连存储
PROTECTED教师附件、模板当前/历史、合同附件等业务文件null调用所属业务下载/预览接口

省略 serve-mode 时默认 PROTECTED。公开资源必须显式声明 DIRECTPRESIGNED 只解决存储访问时效,不等于业务授权。

PROTECTED 没有 ticket、token、bizType 或通用授权器。每次访问都由业务接口重新校验当前登录用户、接口权限、数据范围、业务状态和记录—资源关系。

以下文件无需强行改为受保护 ResourceRef

  • classpath 内置 Excel 模板;
  • 动态生成的导入模板和导出文件;
  • 公开头像、Logo、富文本图片;
  • 原本就由业务接口实时生成的文件流。

配置

yaml
sz:
  oss:
    provider: MINIO
    endpoint: your-minio-host:9000
    accessKey: your-access-key
    secretKey: your-secret-key
    bucketName: your-bucket
    domain: https://files.example.com
    scheme: https

  resource:
    root: ./data
    default-storage-type: LOCAL
    security:
      allowed-exts: [jpg, jpeg, png, gif, webp, pdf, doc, docx, xls, xlsx, txt, csv, zip]
    max-size: 50MB
    scenes:
      - code: admin.user.logo
        name: 账户头像
        type: LOCAL
        serve-mode: DIRECT
        path: logo
        base-url: /api/admin/resource/file/logo
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [svg, png, jpg, jpeg, webp, gif]
        max-size: 3

      - code: template.excel
        name: 模板文件管理 / 导入模板
        type: LOCAL
        serve-mode: PROTECTED
        path: template
        naming: ORIGINAL
        path-strategy: DATE
        exts: [xls, xlsx]
        max-size: 10

      - code: teacher.attachment
        name: 教师统计 / 附件
        type: LOCAL
        serve-mode: PROTECTED
        path: teacher-attachments
        naming: ORIGINAL
        path-strategy: BIZ_DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 10

      - code: system.protected
        name: 通用业务附件
        type: LOCAL
        serve-mode: PROTECTED
        path: protected
        naming: ORIGINAL
        path-strategy: DATE
        exts: [png, jpg, jpeg, webp, gif, pdf, doc, docx, xls, xlsx]
        max-size: 10

配置规则:

  • PROTECTED 不配置 base-url
  • DIRECTbase-url 必须是浏览器可访问地址;
  • 本地 dev/local/preview 可以通过 Java 资源端点提供 DIRECT 文件;
  • 生产环境应由 Nginx、OSS 或生产启用的受控资源服务承载 DIRECT 地址;
  • system.temp 是 DIRECT 兼容场景,新生成的业务附件默认使用 system.protected
  • 正式业务建议定义稳定、可读的 code/name,例如 contract.attachment / 合同管理 / 附件
  • 各环境必须保持同一场景编码,存储类型和公开地址可以按环境变化。

旧配置中的 serve-mode: TOKEN 必须改为 PROTECTEDexpire 仅对 PRESIGNED 有效。

上传与 ResourceRef

上传接口

http
POST /api/admin/resource/upload
Content-Type: multipart/form-data

sceneCode=teacher.attachment
pathSegments=1001
file=<binary>

成功响应中的 data 示例:

json
{
  "resourceId": "10086",
  "sceneCode": "teacher.attachment",
  "objectKey": "teacher-attachments/1001/20260904/report.pdf",
  "originName": "report.pdf",
  "contentType": "application/pdf",
  "size": 98304,
  "eTag": null,
  "accessUrl": null
}

PROTECTED 返回 accessUrl: null 是正常结果,不表示上传失败。

业务引用契约

多文件 JSON 字段统一保存 ResourceRef[]

ts
export type ResourceRef = {
  resourceId: string;
  sceneCode?: string;
  objectKey?: string;
  originName?: string;
  contentType?: string;
  accessUrl?: string | null;
};

字段含义:

  • resourceId:受管资源主标识,网络和前端按字符串处理;
  • sceneCode:业务字段允许使用的固定资源场景;
  • objectKey:存储定位键;
  • originName:展示和下载文件名;
  • contentType:后端生成响应类型的依据;
  • accessUrl:DIRECT/PRESIGNED 的只读派生地址,不入库、不参与授权。

前端不能将 resourceId 转为 number,也不能只保存 URL 或 string 数组。

保存时的可信引用

业务 Service 在 create/update 时不能原样保存客户端提交的 ResourceRef。通过 ResourceReferenceService 规范化:

java
entity.setAttachments(resourceReferenceService.normalizeForCreate(
        dto.getAttachments(), "contract.attachment", StpUtil.getLoginIdAsLong()));

entity.setAttachments(resourceReferenceService.normalizeForUpdate(
        existing.getAttachments(), dto.getAttachments(),
        "contract.attachment", StpUtil.getLoginIdAsLong()));

规范化会:

  1. 校验 resourceId 为正数且 sys_resource 存在、未删除;
  2. 校验资源 scene 与业务字段固定 scene 一致;
  3. 新增引用必须由当前用户上传;
  4. 更新时允许保留原记录已经挂接的历史引用;
  5. 使用 sys_resource 重建 objectKey、文件名和 MIME;
  6. 清空 accessUrl,按 resourceId 去重并保持提交顺序。

原记录 A/B 更新为 A/C 后,只保存 A/C。B 只是解除业务引用,不会自动删除 sys_resource 或物理文件。

受保护下载与预览

内置接口:

业务下载预览授权边界
教师统计附件POST /api/admin/teacher-statistics/{id}/resources/{resourceId}/downloadPOST /api/admin/teacher-statistics/{id}/resources/{resourceId}/preview查询权限、数据范围、记录包含资源
当前模板POST /api/admin/sys-temp-file/{id}/resources/{resourceId}/downloadPOST /api/admin/sys-temp-file/{id}/resources/{resourceId}/preview查询权限、活动记录、双字段一致
模板历史POST /api/admin/sys-temp-file-history/{id}/resources/{resourceId}/downloadPOST /api/admin/sys-temp-file-history/{id}/resources/{resourceId}/preview查询权限、活动父记录、历史记录双字段一致

业务接口先执行 validateResourceAccess(id, resourceId),再调用公共流式服务:

java
@SaCheckPermission("contract.record.query_table")
@PostMapping("/{id}/resources/{resourceId}/download")
public ResponseEntity<StreamingResponseBody> downloadResource(
        @PathVariable Long id, @PathVariable Long resourceId) {
    contractService.validateResourceAccess(id, resourceId);
    return resourceDownloadService.download(resourceId);
}

ResourceDownloadService 只负责:

  1. 根据 resourceId 查询活动的受管资源;
  2. 通过 sceneCode + objectKey 调用存储抽象读取文件流;
  3. 下载使用 attachment,预览使用 inline;
  4. 预览仅允许 PNG、JPEG、WebP、GIF、PDF;
  5. 返回安全文件名、X-Content-Type-Options: nosniffCache-Control: no-store

业务状态限制应写在 validateResourceAccess 中。例如合同只有“已发布”状态允许下载,不需要扩展公共下载服务。

WARNING

禁止新增只接收 resourceId、不校验业务记录的通用下载 Controller。否则知道资源 ID 的用户可能越权访问不属于自己的文件。

前端接入

vue
<FileDownloadList
  :files="row.attachments"
  :biz-id="String(row.id)"
  :download-api="downloadContractResourceApi"
  :preview-api="previewContractResourceApi"
/>

访问顺序:

  1. 存在 bizId + 业务 API + resourceId:调用固定业务接口并处理 Blob;
  2. 不存在业务接口,但 DIRECT/PRESIGNED 返回 accessUrl:浏览器直接访问;
  3. 两者都没有:拒绝访问,提示从已保存的业务记录进入;
  4. 不回退到服务端 URL 代理。

编辑和提交规则:

  • 保留完整 ResourceRef[]
  • 上传过程中禁止提交;
  • 提交前调用 normalizeResourceFiles(),移除本地预览状态并按 resourceId 去重;
  • 新增记录尚无业务 ID 时,新上传图片使用本地 Blob URL 预览;
  • 保存后重新进入页面,PROTECTED 文件改走业务接口;
  • Blob 预览关闭或组件卸载时释放 URL.createObjectURL() 生成的地址。

代码生成器

字段使用 List<ResourceRef> 且控件为 fileUpload/imageUpload 时,生成器会自动生成:

  • create/update 资源规范化;
  • validateResourceAccess(id, resourceId)
  • Controller 下载/预览接口;
  • 前端下载/预览 API;
  • bizIddownloadApipreviewApi 组件绑定。

生成前必须选择已注册的资源场景。system.protected 可以作为默认值,但正式模块建议定义业务专属 scene,便于文件管理和运维识别。需要额外业务状态限制时,在生成后的 validateResourceAccess 中补充。

头像、Logo、公开富文本图片使用 DIRECT/PRESIGNED,不生成受保护下载接口。

头像

头像不使用 ResourceRef,也不走受保护下载。数据库和提交保存 objectKey,专用返回 VO 提供展示 URL:

对象持久/提交字段展示字段
SysUserVOlogologoUrl
UserProfileVOavataravatarUrl
专用登录返回 VOobjectKey 字段URL 字段
BaseUserInfologo无 URL 字段

前端保存前应删除 logoUrl/avatarUrl,不能把派生 URL 回写数据库。

文件管理

文件管理页面是资源登记和排障入口,不是绕过业务权限的下载中心。列表展示文件名、用途、大小、访问方式、存储方式、创建人和登记时间:

  • DIRECT:允许直接访问或复制地址;
  • PRESIGNED:按需生成短时访问地址;
  • PROTECTED:仅提供详情,并提示从所属业务记录预览或下载;
  • system.protected:显示“通用业务附件”,不会凭资源记录猜测具体业务归属。

场景配置中的 name 用于说明用途。业务专属 scene 越清晰,文件管理列表越容易排查。

历史数据与 Liquibase

v2.1.0 没有数据库结构变化。sys_resource 沿用已有表结构。

demo/2.1.0/001_demo_resource_references.xml 只处理官方演示数据:

  • 登记 8 个教师统计演示资源;
  • 为教师统计 ID 25、26 补齐确定性的 resourceId
  • 对齐当前模板和模板历史的演示 JSON;
  • 精确前置条件不满足时 MARK_RAN,不覆盖二开数据;
  • 不扫描、不下载、不删除、不猜测自定义文件。

自定义业务数据不会自动迁移。升级前必须确保每个 PROTECTED ResourceRef.resourceId 都能命中活动的 sys_resource;无法确定原文件与资源记录的映射时,应重新上传并保存业务记录。

系统没有 OFF/AUDIT/REPAIR 迁移模式,没有 H01-H20 报告,也没有自动修复运行器。

错误语义

HTTP业务码含义
400R2003 / R2004引用无效或资源场景不匹配
403R2001 / R2005无访问权限或新增资源不属于当前用户
404R2002业务记录或活动资源不存在
415R2006文件类型不允许预览
500R2007存储读取失败

前端应对 415 提示用户改用下载;403 不提供降级访问;404 刷新业务数据。

不兼容变更

  • POST /api/admin/common/files/download 任意 URL 代理已删除;
  • ProxyDownloadDTOCommonService.urlDownload()、前端 useUrlDownloadfileDownload({ url }) 已删除;
  • serve-mode: TOKEN 改为 PROTECTED
  • 受保护组件参数是 bizId + downloadApi + previewApi,不是 accessContext
  • ResourceRef.accessUrl 不再作为持久化字段或受保护文件定位依据;
  • 前后端必须成套升级;
  • /common/download/templates 仍保留,用于受控 classpath 模板下载。

安全不变量

  • 后端不得对用户提交的 URL 调用 openStream()、HTTP Client 或跟随重定向;
  • 每次受保护下载/预览必须校验接口权限、数据范围和记录—资源关系;
  • sys_resource 是资源定位的唯一权威来源;
  • PREVIEW 不能放开 HTML、SVG 等主动内容;
  • 回滚可以关闭文件入口,但不能恢复任意 URL 代理。

相关文档