10 KiB
10 KiB
7.2 上传配置详解
:::tip 概述
文件上传配置定义在 config/file.php 和 .env 中,控制上传目录、访问域名、大小限制、允许的文件类型等。所有值通过 env() 函数从 .env 读取,支持多环境切换;config/file.php 中的注释明确标注了每个配置的读取方。
:::
配置文件
// config/file.php — 文件上传配置(来自项目实际代码,节选)
return [
// 文件上传根目录(相对 public),所有上传文件均存放于此
// 读取方:UploadController::uploadFile()、save_file()、save_remote_file()
'upload_dir' => env('FILE_UPLOAD_DIR', 'uploads'),
// 文件访问域名,用于拼接完整 URL;为空时返回相对路径
// 读取方:get_file_url()、save_content()、processContentFieldsOnRead()
'domain_url' => env('FILE_DOMAIN_URL', ''),
// 临时文件目录名(相对 upload_dir)
// 读取方:UploadController::uploadFile()(上传落盘目录)、save_file()(识别 temp/ 开头的临时文件后迁移到正式目录)
'temp_dir' => 'temp',
// 允许上传的图片后缀(逗号分隔)
// 安全说明:svg 已移除,因其可内嵌 JavaScript 导致 XSS 风险
'image_ext' => env('FILE_IMAGE_EXT', 'gif,jpg,jpeg,png,bmp,webp'),
// 允许上传的文件后缀(逗号分隔)
'file_ext' => env('FILE_EXT', 'gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv'),
// 单文件最大尺寸(MB),转换为字节存储
'max_size' => (int) env('FILE_MAX_SIZE', 10) * 1024 * 1024,
];
配置项详解
| 配置项 | .env 变量 | 默认值 | 说明 |
|---|---|---|---|
upload_dir |
FILE_UPLOAD_DIR |
uploads |
上传根目录(相对 public/),所有文件存储于此 |
domain_url |
FILE_DOMAIN_URL |
(空) | 文件访问域名,用于拼接完整 URL |
temp_dir |
— | temp |
临时目录名(相对 upload_dir),上传接口的落盘目录,save_file() 据此识别待迁移文件 |
image_ext |
FILE_IMAGE_EXT |
gif,jpg,jpeg,png,bmp,webp |
图片允许后缀(无 svg) |
file_ext |
FILE_EXT |
图片+文档+压缩包等 | 文件允许后缀 |
max_size |
FILE_MAX_SIZE |
10 |
单文件最大大小(MB,配置内自动转字节) |
upload_dir — 上传根目录
# .env
FILE_UPLOAD_DIR=uploads
要求:
- 本项目为相对
public/的目录,Web 可直接访问其中的静态文件 - 目录必须存在且有写入权限(Docker 镜像已创建并授权
public/uploads) - 上传文件随项目目录持久化(Docker 下即宿主机目录,不依赖命名卷)
各环境示例:
| 环境 | 值 | 实际位置 |
|---|---|---|
| 本地开发 | uploads |
项目/public/uploads/ |
| Linux 生产 | uploads(或绝对路径) |
Nginx root 下可直接访问 |
| Docker | uploads |
挂载的项目目录内 |
domain_url — 文件访问域名
# .env
FILE_DOMAIN_URL=https://cdn.example.com
要求:
- 不要以
/结尾(读取方会rtrim处理,但配置时保持规范) - 必须是前端可访问的域名
- 本地开发可留空(接口返回相对路径,由同源/Vite 代理直接访问)
各环境示例:
| 环境 | 域名示例 |
|---|---|
| 本地开发 | (留空,使用相对路径) |
| 虚拟域名开发 | http://file.laravel13.elevue |
| 测试环境 | https://test-cdn.example.com |
| 生产环境 | https://cdn.example.com 或对象存储域名 |
temp_dir — 临时目录
// config/file.php
'temp_dir' => 'temp',
说明:
- 上传接口
uploadFile把文件写入{upload_dir}/temp/Ymd/,不直接落正式目录 save_file()/save_content()依据temp/前缀把文件迁移到 Service 的fileSaveDir子目录- 正式目录未被表单引用的临时文件可按周期清理(见下方「问题 4」)
max_size — 文件大小限制
# .env(单位 MB,config 内 ×1024×1024 转字节)
FILE_MAX_SIZE=10
建议值:
| 场景 | 建议值 | 说明 |
|---|---|---|
| 头像上传 | 2 MB | 图片通常不会太大 |
| 文章封面 | 5 MB | 压缩后的图片 |
| 通用文件上传 | 10 MB | 默认值 |
| 文档上传 | 20 MB | PDF/Word 等 |
:::warning 双层限制
业务层限制生效前,PHP 层必须放行:php.ini 的 upload_max_filesize / post_max_size 需 大于等于 FILE_MAX_SIZE(建议 50M)。
:::
image_ext / file_ext — 后缀白名单
# .env
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp # uploadFile?type=image 校验
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv # uploadFile(默认 type=file)校验
说明:
image_ext用于type=image,只允许图片格式;svg 已移除(内嵌 JS 的 XSS 风险)file_ext用于type=file,允许图片、文档、压缩包等常见格式- 白名单为空数组时跳过校验(防御性兜底),生产环境务必保留非空配置
.env 配置示例
# 开发环境
FILE_UPLOAD_DIR=uploads
FILE_DOMAIN_URL=
FILE_MAX_SIZE=10
# 生产环境(示例)
FILE_UPLOAD_DIR=uploads
FILE_DOMAIN_URL=https://cdn.example.com
FILE_MAX_SIZE=10
FILE_IMAGE_EXT=gif,jpg,jpeg,png,bmp,webp
FILE_EXT=gif,jpg,jpeg,png,bmp,webp,doc,docx,xls,xlsx,pdf,zip,rar
目录结构
public/
└── {upload_dir}/ # upload_dir(默认 uploads)
├── temp/ # 临时目录(UploadController 上传落盘)
│ └── 20261002/
│ └── 123456_a1b2c3d4e5f6.jpg
├── article/ # 正式目录(由 Service 的 fileSaveDir 配置)
│ └── 20261002/
│ └── 153052_9f8e7d6c5b4a.pdf
├── example/
├── user/
│ └── avatar/
└── file_template/
目录命名规则
| 目录 | 说明 | 来源 |
|---|---|---|
temp/ |
临时文件目录(上传接口落盘) | config/file.php → temp_dir |
{module}/ |
按模块的正式目录 | Service 的 fileSaveDir(未配置按模型名推导) |
{Ymd}/ |
日期子目录 | 自动创建 |
{His}_{random}.{ext} |
时间+随机文件名 | 防冲突与可预测性 |
文件生命周期
1. 前端上传 → POST /api/upload/uploadFile
保存到: public/uploads/temp/20261002/123456_a1b2c3d4e5f6.jpg
返回: filePath + fileUrl
2. 前端提交表单 → 携带 filePath
POST /api/example/add { name: "测试", avatar: "temp/20261002/..." }
3. ExampleService::add() → processFileFieldsOnSave → save_file()
temp/ → 迁移到 fileSaveDir(example/)后,正式目录相对路径入库
(其它分支:去掉域名直接用;外部 URL → 下载保存)
4. ExampleService::detail() → processFileFieldsOnRead → get_file_url()
相对路径 → 拼接 FILE_DOMAIN_URL
example/20261002/...
→ https://cdn.example.com/example/20261002/...
与 Service 层的配合
class ArticleService extends BaseTenantService
{
use HasFileProcessing, /* 其他 Trait */;
protected array $fileFields = ['cover'];
protected string $fileSaveDir = 'article';
}
实际路径映射:
配置:
FILE_UPLOAD_DIR = uploads
FILE_DOMAIN_URL = https://cdn.example.com
fileSaveDir = article
结果:
上传文件: public/uploads/temp/20261002/xxx.jpg
Service 归一化后入库: article/20261002/xxx.jpg
前端显示: https://cdn.example.com/article/20261002/xxx.jpg
Nginx 配置
静态文件直接由 Nginx 返回
# 项目方案:public/uploads 位于站点根内,location / 由 try_files 直接返回静态文件
location / {
try_files $uri $uri/ /index.php?$query_string;
}
禁止上传目录执行 PHP
# docker/nginx.conf 已内置:仅入口 index.php 可执行,其余 .php 一律 404
location ~ \.php$ {
return 404;
}
禁止访问隐藏文件
location ~ /\. {
deny all;
}
常见问题
问题 1:上传提示"目录不可写"
原因: public/uploads 没有写入权限。
解决:
# Linux
chmod -R 755 public/uploads
chown -R www-data:www-data public/uploads
# Docker:镜像内已 chown(storage / bootstrap/cache / public/uploads)
问题 2:上传后图片不显示
原因: FILE_DOMAIN_URL 配置错误或与前端不同源。
解决:
- 检查
.env中FILE_DOMAIN_URL是否为前端可访问的地址(改后php artisan config:clear) - 留空时用相对路径——确认前端与后端同源或经代理
- 浏览器直接打开图片 URL 验证
问题 3:文件大小限制不生效
原因: PHP 层限制小于业务层。
解决:
; php.ini(修改后重启服务)
upload_max_filesize = 50M
post_max_size = 50M
max_execution_time = 300
问题 4:临时文件未清理
原因: 上传后未提交表单(或表单未引用)的文件停留在 temp/,不会被迁移。
解决: 定期清理 temp/ 中超过 1 天的文件:
# 定时任务:每天凌晨清理 1 天前的临时文件
find public/uploads/temp/ -type f -mtime +1 -delete
安全建议
| 建议 | 说明 |
|---|---|
| 扩展名白名单 | 只允许已知安全的文件类型(FILE_IMAGE_EXT/FILE_EXT) |
| 移除高危后缀 | svg 已移除;生产白名单去掉 sql/js/css 等可执行类 |
| MIME 类型检查 | 导入等场景配合 finfo 检测真实 MIME(见 7.4 数据导入导出) |
| 随机文件名 | 服务端生成(时间+随机串),防止路径遍历和文件名注入 |
| 文件大小限制 | FILE_MAX_SIZE 防止大文件 DoS |
| Nginx 禁止执行 PHP | 仅入口 index.php 可执行(Docker 配置已内置) |
| 定期清理临时文件 | temp/ 内未被引用的文件按周期清理,避免磁盘空间被占满 |