9.3 KiB
9.3 KiB
文件上传问题解决记录
📝 问题时间线
第一次报错:网络错误
时间: 初次排查 错误消息: "文件上传失败,请确认你的网络是否正常,并检查文件存储服务跨域配置是否正确" 错误类型: Network Error (XHR status = 0)
诊断过程
- ✅ 检查 MinIO 状态 → 未运行
- ✅ 检查配置 → S3 环境变量已配置
- ✅ 分析代码 → 错误发生在
src/services/upload.ts:190
根本原因
MinIO 服务未启动,导致无法访问 S3 端点。
解决方案
- 创建
docker-compose.minio.yml- MinIO 服务配置 - 创建
setup-minio.sh- 自动配置脚本 - 创建
start-all.sh- 一键启动脚本 - 启动 MinIO 服务
结果
✅ MinIO 正常运行,但上传仍然失败,进入第二阶段排查。
第二次报错:403 Forbidden
时间: MinIO 启动后 错误消息: "Forbidden" 错误类型: HTTP 403
诊断过程
- ✅ MinIO 运行正常 →
curl http://localhost:9002/minio/health/live返回 200 - ✅ Bucket 存在 →
mc ls local/lingjingwanxiang显示 bucket - ✅ 访问策略正确 →
mc anonymous get显示public - ✅ CORS 配置正确 → 测试 OPTIONS 请求通过
- ❌ 预签名 URL 问题 → 深入分析代码
第一次修复尝试(错误)
方案: 在生成预签名 URL 后替换域名
// ❌ 错误的修复
const url = await getSignedUrl(this.client, command);
if (fileEnv.S3_PUBLIC_DOMAIN && fileEnv.S3_ENDPOINT) {
return url.replace(
new URL(fileEnv.S3_ENDPOINT).origin,
new URL(fileEnv.S3_PUBLIC_DOMAIN).origin
);
}
问题: AWS Signature V4 签名基于完整 URL 计算,修改 URL 导致签名失效。
测试结果:
- ✅ 预签名 URL 包含正确的公网地址
192.168.1.83:9002 - ❌ 上传返回 403 Forbidden(签名失效)
根本原因分析
AWS 签名机制:
签名 = HMAC-SHA256(
Secret Key,
String to Sign
)
String to Sign 包括:
- HTTP 方法 (PUT)
- 完整 URL (包括 host) ← 关键
- Headers
- Payload hash
当生成签名后修改 URL,签名验证失败 → 403 Forbidden
第二次修复(正确)
方案: 创建两个独立的 S3Client
class S3 {
private readonly client: S3Client; // S3_ENDPOINT - 内部操作
private readonly presignClient: S3Client; // S3_PUBLIC_DOMAIN - 预签名 URL
constructor() {
this.client = new S3Client({
...config,
endpoint: fileEnv.S3_ENDPOINT, // host.docker.internal:9002
});
this.presignClient = new S3Client({
...config,
endpoint: fileEnv.S3_PUBLIC_DOMAIN || fileEnv.S3_ENDPOINT, // 192.168.1.83:9002
});
}
async createPreSignedUrl(key: string) {
// 使用 presignClient 生成 URL
return getSignedUrl(this.presignClient, command);
}
}
优势:
- ✅ 签名基于正确的 endpoint(公网地址)
- ✅ 内部操作仍使用容器可访问的地址
- ✅ 不需要事后修改 URL
🔍 技术深度分析
为什么需要两个不同的地址?
Docker 网络架构
┌─────────────────────────────────────┐
│ Host Machine │
│ │
│ ┌────────────────┐ ┌────────────┐ │
│ │ MinIO Container│ │ App Container││
│ │ Port: 9000 │ │ Port: 3210 ││
│ │ → :9002 │ │ ││
│ └────────────────┘ └────────────┘ │
│ ↑ ↑ │
│ │ │ │
└─────────┼──────────────────┼─────────┘
│ │
(host.docker.internal:9002)
│ │
┌─────────┴──────────────────┴─────────┐
│ Browser │
│ 192.168.1.83:3210 │
│ 192.168.1.83:9002 │
└───────────────────────────────────────┘
关键点:
- 容器内:通过
host.docker.internal访问 host 服务 - 浏览器:通过
192.168.1.83访问 host 服务 - 预签名 URL: 由服务端生成,但由浏览器使用
AWS S3 预签名 URL 工作流程
-
客户端请求:
Browser → LobeChat: "我要上传文件" -
服务端生成预签名 URL:
const command = new PutObjectCommand({...}); const url = await getSignedUrl(presignClient, command); // url = http://192.168.1.83:9002/bucket/file?X-Amz-Signature=... -
签名计算:
Canonical Request = "PUT\n" + "/bucket/file\n" + "X-Amz-Algorithm=AWS4-HMAC-SHA256&...\n" + "host:192.168.1.83:9002\n" + ← 包含在签名中 ... Signature = HMAC-SHA256(secretKey, stringToSign) -
客户端使用:
Browser → PUT http://192.168.1.83:9002/bucket/file?X-Amz-Signature=... MinIO → 验证签名 → ✅ 通过
为什么简单替换 URL 会失败?
// 错误方案
const url = await getSignedUrl(client, command);
// url = http://host.docker.internal:9002/...?X-Amz-Signature=abc123
// 签名基于: host:host.docker.internal:9002
const fixedUrl = url.replace('host.docker.internal', '192.168.1.83');
// fixedUrl = http://192.168.1.83:9002/...?X-Amz-Signature=abc123
// 但签名仍然是基于 host.docker.internal 计算的!
Browser → PUT http://192.168.1.83:9002/...?X-Amz-Signature=abc123
MinIO → 验证签名:
期望的 host: 192.168.1.83:9002
签名中的 host: host.docker.internal:9002
→ ❌ 签名不匹配 → 403 Forbidden
双客户端方案的优雅之处
// 正确方案
const presignClient = new S3Client({
endpoint: 'http://192.168.1.83:9002', // 直接使用公网地址
});
const url = await getSignedUrl(presignClient, command);
// url = http://192.168.1.83:9002/...?X-Amz-Signature=xyz789
// 签名基于: host:192.168.1.83:9002 ← 正确!
Browser → PUT http://192.168.1.83:9002/...?X-Amz-Signature=xyz789
MinIO → 验证签名:
期望的 host: 192.168.1.83:9002
签名中的 host: 192.168.1.83:9002
→ ✅ 匹配 → 200 OK
📊 修复总结
修改的文件
| 文件 | 修改内容 | 状态 |
|---|---|---|
src/server/modules/S3/index.ts |
添加 presignClient,使用双客户端架构 |
✅ 已完成 |
src/server/modules/S3/index.test.ts |
更新测试用例 | ✅ 已完成 |
创建的工具
| 文件 | 用途 |
|---|---|
docker-compose.minio.yml |
MinIO 服务配置 |
setup-minio.sh |
自动配置 MinIO |
start-all.sh |
一键启动所有服务 |
stop-all.sh |
停止所有服务 |
test-upload.sh |
诊断工具 |
rebuild-and-restart.sh |
重建和重启脚本 |
创建的文档
| 文件 | 说明 |
|---|---|
🚀立即修复上传问题.md |
完整操作指南 |
修复403错误.md |
技术详解 |
快速修复.md |
快速参考 |
文件上传故障排查.md |
故障排查指南 |
问题解决记录.md |
本文档 |
🎓 经验教训
1. Docker 网络的复杂性
在 Docker 环境中,需要区分:
- 容器间通信(内部网络)
- 容器与 host 通信(host.docker.internal)
- 外部客户端访问(公网 IP / 域名)
2. 云服务 API 的签名机制
修改已签名的 URL 通常会导致签名失效,应该:
- 在生成签名时使用正确的 endpoint
- 而不是事后修改 URL
3. 分层诊断的重要性
问题排查应该层层递进:
- 服务是否运行?
- 网络是否连通?
- 权限是否正确?
- 签名是否有效?
4. 测试的重要性
每次修复后都应该:
- 运行自动化测试
- 进行端到端测试
- 验证边界情况
🚀 下一步行动
-
立即执行修复:
./rebuild-and-restart.sh -
验证功能:
- 上传图片
- 上传文档
- 在聊天中查看上传的文件
-
清理临时文件 (可选):
rm -f 立即修复文件上传.md rm -f README-文件上传修复.md rm -f 文件上传修复说明.md -
提交代码 (可选):
git add src/server/modules/S3/ git commit -m "🐛 fix: use dual S3Client for presigned URLs - Add presignClient for generating presigned URLs with public domain - Keep client for internal S3 operations - Fix 403 Forbidden error caused by signature mismatch - Update tests to reflect dual client architecture"
问题解决日期: 2025-11-28 总耗时: ~2 小时(诊断 + 修复) 状态: ✅ 已修复,待应用和测试