外观
文件管理
字数
6397 字
预计阅读
25 分钟
用户在后台上传的文件,比如头像、富文本里的图片、表单附件和审批附件,都由"文件管理"统一保存。它负责三件事:文件存到哪里、谁能看、上传的是不是安全的文件。
后台有两个页面:
| 页面 | 用途 |
|---|---|
| 系统管理 → 文件管理 → 文件列表 | 系统里所有上传过的文件:搜索、预览、下载、复制链接、导出、删除 |
| 系统管理 → 文件管理 → 存储配置 | 文件存放的位置:本地磁盘或 S3 兼容存储,以及新文件存到哪一个 |
谁能看到这些菜单
初始化后只有超级管理员能用这两个页面。其他角色要在 系统管理 → 角色管理 里分配菜单和按钮权限,见权限一览。
存储配置
一条存储配置就是"一个存放文件的地方"。可以添加多条,但同一时间只有一条是主配置,新上传的文件都存到主配置里。
初始化数据时会创建一条名为"本地存储"的本地磁盘配置,并把它设为主配置。所以不做任何设置,上传功能就能用。
列表显示名称、驱动、主配置、启用状态、创建时间和备注,可以按名称、驱动和启用状态搜索。列表里不显示服务地址、密钥这类连接信息,点"编辑"才能看到。
新增时先选驱动,也就是存储的类型。添加以后驱动不能再改。
本地磁盘
文件保存在服务器自己的硬盘上。
- 保存目录由服务端的环境变量
STORAGE_LOCAL_ROOT决定,默认是./data/upload(相对于apps/server目录)。这个目录不能在页面上修改,所有本地磁盘配置共用它; - 目录下分成
public(公开文件)和private(私有文件)两个子目录。只有public里的文件可以不登录直接访问; - 页面上只有一项公开访问前缀:公开文件的地址以它开头。留空时是
/files。如果前面加了 CDN(内容分发网络,用来加速图片等静态文件),可以填 CDN 的地址,比如https://cdn.example.com。
S3 兼容存储
S3 原本是亚马逊云的对象存储服务,它的接口已经成为行业通用的标准。阿里云 OSS、腾讯云 COS、Cloudflare R2、MinIO 等都兼容这套接口,都可以用这个驱动。
| 字段 | 说明 |
|---|---|
| 服务地址(Endpoint) | 存储服务的地址,以 http:// 或 https:// 开头 |
| 区域 | 比如 us-east-1,只能是小写字母、数字和连字符 |
| 存储桶 | 存放文件的"桶"的名称,3–63 个字符,只能是小写字母、数字、点和连字符 |
| 访问密钥 ID | 服务商提供的 AccessKey ID |
| 访问密钥 | 服务商提供的 AccessKey Secret,加密保存,见访问密钥加密保存 |
| 路径风格访问 | 打开后,文件地址的形式是"服务地址/存储桶/文件";关闭时是"存储桶.服务地址/文件"。服务不支持存储桶子域名时(自建的 MinIO 常见)要打开 |
| 公开访问域名 | 公开文件的地址前缀,可以填 CDN 或存储桶的公开域名。留空时使用存储桶自己的地址 |
文件在存储桶里按前缀分开存放:public/ 放公开文件,private/ 放私有文件,staging/ 是浏览器直传时的临时区(见浏览器直传 S3)。
存储桶只能公开 public/
请在服务商那里设置存储桶的访问策略(或者 CDN 的回源规则),只允许匿名读取 public/ 下的文件。private/ 和 staging/ 必须保持私有,否则私有文件的权限检查就没有意义了。
主配置
点某一行的设为主配置,之后新上传的文件就存到这里。
- 只有启用的配置能设为主配置;
- 原来的主配置保持启用。已经上传的文件不会搬家,仍然留在原来的位置,照常可以访问。文件列表的"存储"列显示每个文件存在哪里;
- 主配置不能停用,也不能删除。要停用或删除它,先把别的配置设为主配置。
测试连接
编辑一条配置时,对话框底部有测试连接按钮。它使用已经保存的设置,所以改完要先保存,再打开编辑测试。
- 本地磁盘:检查保存目录能不能写入;
- S3 兼容存储:用保存的密钥访问存储桶,等待超过 5 秒算失败。
结果只有"连接成功"和"连接失败"两种。失败时不说明原因(地址被拒绝、超时、密钥错误还是存储桶不存在),也不显示对方返回的内容。这是有意的:如果页面会显示对方的回应,就可能被人用来探测内网,见安全基线 · SSRF。
访问密钥加密保存
- 访问密钥用 AES-256-GCM 加密后存入数据库。加密用的密钥由环境变量
APP_SECRET派生出来; - 访问密钥从不发给浏览器。编辑时这一栏是空的,提示"未修改:输入新的密钥即可替换"。留空保存,原来的密钥不变;
- 更换
APP_SECRET以后,已经保存的密钥无法解密,要重新填写; - 删除一个 S3 配置后,等它上面的文件全部清理完(见删除和清理),保存的密钥也会被清除。
只能连接公网地址
存储服务的地址是管理员在页面上填写的。如果不加限制,有人就可以填一个内网地址,让服务器替他去访问内网的机器。这种攻击叫 SSRF(服务端请求伪造)。所以:
- 服务地址只能指向公网地址。内网地址、本机地址和云厂商的元数据服务地址都会被拒绝。直接写成内网 IP 或
localhost的,保存时提示"不允许使用该服务地址(内网地址或未登记的端口)";写成域名的,每次连接时都会检查它解析出来的地址; - 端口默认只允许 80 和 443。自建的服务用了别的端口(比如 MinIO 常用的 9000),要由部署的人在服务端环境变量里登记:
OUTBOUND_S3_PORTS=9000,多个端口用逗号或空格分隔。这份端口名单只能写在环境变量里,页面上改不了; ALLOW_PRIVATE_ENDPOINTS=true会取消地址和端口检查,只用于本地测试,正式环境要保持false。
这几个变量的说明见环境变量 · 安全。
停用和删除
- 停用的配置不能设为主配置。已经存在它上面的文件不受影响,照常可以访问;
- 配置上还有文件时不能删除,提示"数据正在被使用,不能删除"。已经删除、正在等待清理的文件不算;
- 可以勾选多行批量删除。其中有一条不能删,就一条都不删。
公开文件和私有文件
每次上传都带一个业务标签,说明这个文件是做什么用的。标签由页面决定(开发者写在代码里),用户不用选。标签也决定了文件是公开的还是私有的:
| 业务标签 | 公开 / 私有 | 用在哪里 |
|---|---|---|
头像 avatar | 公开 | 个人中心的"更换头像"。服务端把图片裁成 256×256 的 WebP 图片;换了新头像,旧头像自动删除 |
封面 cover | 公开 | 表单里的图片字段,比如 系统工具 → 生成示例 → 图书 的封面 |
富文本图片 richtext | 公开 | 富文本编辑器里插入或粘贴的图片,比如通知公告和邮件模板的正文 |
附件 attachment | 私有 | 文件上传字段的默认标签;文件列表的"上传文件"按钮也用它 |
导入文件 import | 私有 | 留给"先上传文件、再导入"这类功能。模板自带的 Excel 导入不经过文件管理,不使用它 |
流程附件 wf.attachment | 私有 | 审批表单里的"附件"组件,也就是表单设计器拖出来的附件 |
公开文件:
- 只能是图片:PNG、JPEG、GIF、WebP。类型按文件内容判断,扩展名也必须在名单里并且和内容一致(比如内容是 JPEG、扩展名却是 .png 的封面图会被拒绝)。头像例外:只看内容、不看原来的扩展名,因为服务端会把它转成 WebP 再保存;
- 有一个固定的地址,不用登录就能打开,可以直接显示在页面上。本地磁盘上的地址形如
/files/年/月/日/随机编号.webp。拿到地址的任何人都能打开,所以不要把内部资料当作公开图片上传。
私有文件:
- 没有公开地址,必须登录后通过下载接口读取;
- 能下载的人:上传人本人、超级管理员、有文件列表"查看"权限(
storage.object.view)的人。业务模块还可以为自己的标签加一条规则,比如流程附件是"能查看这个流程实例、并且能看到这个附件字段的人",见工作流 · 附件。其他人下载时提示没有权限; - 下载时浏览器总是把它当作文件保存,不会当成网页打开(只有图片可以在页面里预览)。这样即使有人上传了伪装过的文件,也没法在别人的浏览器里运行;
- 文件存在 S3 时,服务端检查完权限,把浏览器转到一个 60 秒内有效的临时链接,由浏览器直接从 S3 下载。
文件列表
在 系统管理 → 文件管理 → 文件列表 中,可以看到系统里所有上传过的文件,默认最新的在前面。
- 列:文件名、文件类型、文件大小、业务标签、是否公开、存储、上传人、上传时间。可以按文件大小和上传时间排序;
- 搜索:文件名(包含即可)、业务标签、是否公开、上传时间范围;
- "文件类型"是服务端根据文件内容判断出来的,不是浏览器报上来的。
打开、预览和复制链接
- 点文件名:公开文件在新标签页打开;私有文件带着登录状态下载,保存为原来的文件名;
- 预览:只有图片有这个按钮,在页面上放大查看。私有图片也能预览,但同样要有下载权限;
- 复制链接:只有公开文件有,复制完整的访问地址。
上传
工具栏的上传文件可以一次选多个文件。从这里上传的文件都是私有的"附件",同样要经过上传检查。
导出
导出按钮把当前筛选条件下的文件导出为 Excel,列和页面上的相同。
删除
每次删除一个文件。确认框会提醒两点:
- 删除后文件立即停止访问,公开文件的地址也随即失效;
- 系统不记录文件在哪里被使用。还在引用它的页面、记录或消息(比如某个人的头像、某篇公告里的图片)将无法显示这个文件。
删除后页面上无法恢复。文件本身还会保留一段时间,见删除和清理。
前面有 CDN 时
公开文件如果经过 CDN,CDN 上已经缓存的副本要等缓存过期才会失效,这由 CDN 服务商控制。
上传检查
不管从哪个页面上传,服务端对每个文件都做下面的检查:
- 大小:不能超过参数
storage.max_size_mb,超过时提示"请求内容过大"; - 扩展名:必须在参数
storage.allowed_exts的名单里; - 内容和扩展名一致:服务端读取文件开头的一段字节,判断它到底是什么文件。每种格式的开头都有固定的标记,叫"文件头"(也叫"魔数")。把
.exe改名成.pdf这样的文件会被拒绝,提示"不支持的文件类型(.pdf),或文件内容与扩展名不符"; - 用途和类型匹配:公开标签只接受 PNG、JPEG、GIF、WebP 图片;
- 随机名称保存:保存路径是"年/月/日/随机编号.扩展名"。原来的文件名只用来显示和下载时命名,不参与保存路径,所以没法用
../这类文件名把文件写到别的目录(这种攻击叫"路径穿越")。
Office 文件还要多查一步:docx、xlsx、pptx 其实是 zip 压缩包,服务端会确认包里真的是 Word、Excel 或 PowerPoint 文档,而不是一个改了扩展名的普通压缩包;老格式的 doc、xls、ppt 也会确认里面有对应的文档内容。
两个参数
在 系统管理 → 参数设置 中修改,改完服务端立即按新值检查;页面上的上传组件要刷新页面后才跟着变(见其他限制):
| 参数键 | 参数名称 | 默认值 | 说明 |
|---|---|---|---|
storage.max_size_mb | 上传文件大小上限(MB) | 20 | 1–2048 之间的整数,填写无效时按 20 处理。登录前可读 |
storage.allowed_exts | 允许上传的扩展名 | jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip | 用逗号或空格分隔,不区分大小写;留空时使用默认名单。头像保存前会转成 WebP,名单里要保留 webp,否则无法更换头像 |
大小上限是登录前可读的参数,不登录也能读到它的值,页面上的上传组件靠它提前检查文件大小(见其他限制)。扩展名名单不公开,只在服务端检查。两个都是内置参数,页面上不能修改"登录前可读",见系统管理 · 参数的几种标记。
有些扩展名加进名单也没用
html、htm、xhtml、xht、svg、svgz、xml、js、mjs永远不允许上传,写进名单也会被忽略。浏览器会把这些文件当作网页或脚本运行,可以用来做跨站脚本攻击(XSS);- 服务端靠文件头判断类型,而纯文本文件没有文件头。所以除了
txt以外,csv、md、json这类纯文本格式即使加进名单也传不上去。
其他限制
- 每个 IP 每分钟最多上传 60 次(更换头像是 10 次);3 秒内重复提交同一个文件会被拒绝,防止连点;
- 页面上的上传组件(包括表单设计器的"附件")在发送前也会检查文件数量和大小,这只是为了尽快提示,真正的限制以服务端为准:
- 大小上限就是参数
storage.max_size_mb,组件下方的提示(比如"最多 10 个文件,单个不超过 20 MB")显示的也是它; - 网页加载后只读一次这个参数,在后台里切换菜单不会重新读,所以改了参数以后,已经打开的网页要刷新浏览器;
- 读不到参数时(比如网络出错),按 20 MB 检查,下次刷新时再读;
- 开发者在组件上指定了大小上限时,以组件的为准,见开发指南 · 文件上传。表单设计器的"附件"组件填了"单个文件上限(MB)"时也以它为准,不填就跟随参数;
- 大小上限就是参数
- 文件列表的上传文件按钮、富文本编辑器里插入或粘贴的图片,都不在页面上检查大小,太大的文件由服务端拒绝,提示"请求内容过大"。
浏览器直传 S3
普通上传的路线是"浏览器 → 服务器 → 存储",文件要经过服务器一次。主配置是 S3 时,还可以让浏览器直接把文件传到 S3。上传时文件不经过服务器,适合大文件(确认时服务端一般只读取文件开头;Office 文件要从 S3 完整读一遍,但不会整个放进内存)。过程分三步:
- 浏览器先把文件名、大小和类型告诉服务端。服务端按上传检查的规则先查扩展名、大小和用途,通过后给浏览器一个 10 分钟内有效的上传链接。这个链接叫"预签名链接":服务端用密钥签过名,只能用来上传这一个文件,密钥本身不会交给浏览器;
- 浏览器用这个链接把文件传到存储桶的临时区
staging/; - 浏览器通知服务端"传完了"。服务端重新读取存好的文件:大小必须和申请时一样,类型按文件头重新判断(Office 文件会完整读一遍)。不符合就删掉,提示"上传的文件与申请直传时的大小或类型不符,已删除";符合才移到正式位置,出现在文件列表里。
只有申请链接的人本人能完成第 3 步,而且只能完成一次。
直传要由开发者打开
直传是上传组件的一个开关,默认关闭。后台没有对应的设置项,模板自带的页面也都没有打开它。需要时由开发者在组件上打开,见开发指南 · 文件上传。主配置是本地磁盘时,打开了直传也只会提示"当前主存储不支持直传,请经服务器上传"。
使用 S3 时的部署设置
只要有私有文件存在 S3 配置上(不论这条配置现在是不是主配置),不管有没有打开直传,这些文件的下载和预览都是浏览器直接从 S3 取的(就是上文的 60 秒临时链接)。所以部署时要做这几件事:
- 构建前端和打印 nginx 的 CSP 时,都用
CSP_CONNECT_SRC登记 S3 的地址,再把打印出的值填进 nginx 的Content-Security-Policy,否则浏览器的内容安全策略(CSP)会拦下这些请求; - 在存储桶的跨域规则(CORS)里允许本站发出的
PUT和GET请求; - 给
staging/设置一条生命周期规则(比如 1 天后自动删除),清理传了但没有确认的临时文件; - 自建服务用了 80、443 以外的端口时,登记
OUTBOUND_S3_PORTS。
具体做法见部署 · 使用 S3 存储时。公开图片按普通图片加载,不需要登记。
删除和清理
- 在文件列表里删除时,先把记录标记为"已删除",文件马上停止访问。公开文件会同时从公开区移到私有区,所以它的地址立即失效;
- 文件本身保留到保留期满,再由内置定时任务"清理过期日志"(默认每天 03:30 执行)连同记录一起彻底删除。保留天数是参数
audit.retention_days,默认 180 天,和各种日志共用,见定时任务 · 内置任务; - 彻底删除时如果失败(比如 S3 暂时连不上),记录会留下,下次清理时再试。
没人用的文件不会自动清理
系统不记录一个文件被哪些地方使用。审批附件是个例外:发起或办理审批时,上传的附件会记下它属于哪个流程实例,但这只用来判断谁能下载。下面这些文件会一直留在存储里:
- 上传了,但表单最后没有保存;
- 在上传组件里点了文件旁边的"移除"按钮(× 或垃圾桶图标)。这只是把文件从表单里移除,文件本身不会被删除;
- 富文本里插入后又删掉的图片。
目前没有自动清理这类"孤儿文件"(没有任何地方使用的文件)的功能,这项功能计划中、暂未实现。需要时可以在文件列表里按业务标签、上传时间筛选后手工删除,但删除前要确认它确实没有被使用。
换头像是个例外:新头像保存成功后,旧头像会自动删除。
权限一览
| 页面 | 按钮权限 | 权限码 | 说明 |
|---|---|---|---|
| 文件列表 | 浏览 | storage.object.browse | 打开页面、搜索 |
| 查看 | storage.object.view | 下载和预览任何人的私有文件 | |
| 导出 | storage.object.export | ||
| 删除 | storage.object.remove | ||
| 存储配置 | 浏览 | storage.config.browse | 打开页面、搜索(列表不含连接信息) |
| 查看 | storage.config.view | 读取一条配置的连接信息(访问密钥除外) | |
| 新增 | storage.config.create | ||
| 修改 | storage.config.modify | 编辑(同时需要"查看")、启用和停用、设为主配置、测试连接 | |
| 删除 | storage.config.remove | 删除、批量删除 |
上传本身不需要权限:任何登录用户都可以在带上传字段的页面上传文件。文件列表的"上传文件"按钮也一样,能打开这个页面就能用。
谨慎授予这两个权限
- 文件列表的"浏览"能看到系统里所有文件的文件名、上传人等信息,不按数据范围过滤;
- 文件列表的"查看"能下载任何人的私有文件,包括所有审批附件。
文件的上传、导出、删除,以及存储配置的新增、修改、启用停用、设为主配置、测试连接和删除,都会记入 系统管理 → 日志管理 → 操作日志。
开发指南
- 文件上传:在表单里加图片或附件字段、组件的属性(包括直传开关和大小上限)、私有文件的下载规则、把文件绑定到业务记录;
- 参数设置:在代码里读取后台可以修改的参数;
- 部署:环境变量(
STORAGE_LOCAL_ROOT、OUTBOUND_S3_PORTS、ALLOW_PRIVATE_ENDPOINTS、CSP_CONNECT_SRC)、部署 · 使用 S3 存储时; - 相关功能:安全基线(文件上传、SSRF)、工作流 · 附件、表单设计器、定时任务、登录与账号 · 基本资料与头像。