go-uploads
基于 Go + Gin 的高性能文件接收服务,支持单文件与大文件分片上传, 通过 HMAC 签名保证安全,为 Electron / Web / 微信小程序 / 移动 App 提供统一上传后端。
ARCHITECTURE
系统架构
任意客户端 ────▶ PHP 后端 (签发签名) ────▶ go-uploads (存储文件) │ ▲ │ │ │ │ │ ├─ PUT /upload │ │ │ ├─ POST /upload/complete │ │ │ └─ GET /health │ │ │ │ └── 返回签名配置 ─┘ │ └── PUT 预签名 URL + 文件流 ─────────────────▶ go-uploads
核心思路:客户端不持有密钥。
PHP 后端负责鉴权 + 签发带签名的临时上传 URL,客户端拿到 URL 后直接向 go-uploads 传输文件。分片上传完成后 PHP 通知 go-uploads 合并。
UPLOAD FLOW
上传流程
单文件上传
- 1.客户端 → PHP uploadConfig() 获取 { isChunk:false, putUrl, key }
- 2.客户端 → PUT {putUrl} 二进制流
分片上传
- 1.客户端 → PHP uploadConfig() 获取 { isChunk:true, chunkUrls[], uploadId, chunkSize, notifyUrl, key }
- 2.客户端 → PUT {chunkUrls[i]} 逐片上传
- 3.全部传完 → 客户端 POST {notifyUrl} 通知合并
- 4.PHP uploadNotify() → go-uploads POST /upload/complete 合并 + 清理
分片大小 2MB(2048KB),由 PHP 端 $CHUNK_SIZE 常量控制。
API
接口规范
/upload?signature={hex}&options={url_encoded_json}
文件上传入口,支持单文件模式和分片模式。请求体为二进制文件流,Content-Type: application/octet-stream。
options JSON 字段
成功 (200)
{
"code": 200,
"data": { "key": "20240101/example.jpg", "size": 1048576 }
}
错误响应
{"code": 403, "message": "invalid signature"}
{"code": 400, "message": "key escapes upload directory"}
{"code": 413, "message": "File exceeds maximum size"}
/upload/complete?signature={hex}&options={url_encoded_json}
分片合并接口,由 PHP 的 uploadNotify() 代理调用,客户端不直接请求。options 字段:Key + UploadId。
- 1.验证签名
- 2.读取 {upload.dir}/.chunks/{UploadId}/part_N
- 3.按编号升序合并
- 4.校验总大小 ≤ maxSize
- 5.删除临时目录 → 返回
// 成功
{"code":200,"data":{"key":"20240101/example.mp4","size":52428800}}
// 404 UploadId 不存在 / 无分片
{"code":404,"message":"UploadId not found"}
/upload/delete?signature={hex}&options={url_encoded_json}
批量删除文件,由 PHP 或可信后端调用。options 建议为 {"Action":"delete"},请求体为 { "keys": ["20240101/file1.jpg", ...] }。
- •每个 Key 经过 safePath 路径穿越校验
- •仅删除文件,不删除目录
- •Key 不存在返回 not found,不影响其他删除
{
"code": 200,
"data": {
"deleted": ["20240101/file1.jpg"],
"failed": [{"key": "20240101/ghost.png", "reason": "not found"}]
}
}
/health
无需签名
{"status":"ok","timestamp":"2026-08-04T12:00:00Z"}
SIGNATURE
签名机制
生成(PHP 端)
$optionsJson = json_encode($options); // JSON 序列化
$signature = hash_hmac('sha256', $optionsJson, $apikey);
$url = $baseUrl . '/upload?signature=' . $signature
. '&options=' . urlencode($optionsJson);
密钥:cH1rC************5xm6bp9k(PHP 与 config.yaml 必须一致)
验证(Go 端)
rawSignature := c.Query("signature")
rawOptions := c.Query("options") // 直接对原始 options 计算 HMAC
mac := hmac.New(sha256.New, []byte(config.ApiKey))
mac.Write([]byte(rawOptions))
expectedSig := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(rawSignature), []byte(expectedSig)) {
return 403 // 恒定时间比较,防时序攻击
}
- •直接对 URL Query 原始 options 做 HMAC,避免 JSON 键顺序不一致
- •客户端不持有 apikey,密钥仅存在于 PHP 与 go-uploads 两端
CONFIG
配置参考
apikey: "cH1rC************5xm6bp9k" server: port: 9000 readTimeout: 3600 writeTimeout: 3600 upload: maxSize: 107374182400 # 100GB dir: "./uploads" allowedFormats: [] # 空 = 不限制 chunkTtl: 86400 # 分片存活 24h
缺失字段使用默认值,无需完整写出。最小配置:apikey + upload.dir。
HMAC-SHA256 签名密钥,与 PHP 端 $up_apikey 一致。
HTTP 监听端口。
HTTP 读写超时,大文件上传需较大值。
单文件(含合并后)最大字节数,通过 io.LimitReader 流式限流,不依赖 Content-Length。
文件存储根目录,Key 相对路径拼接至此。
扩展名白名单,如 ["jpg","png","mp4","pdf"],不区分大小写。
未完成分片目录存活时间,后台 goroutine 自动清理,设 0 关闭。
CLIENTS
客户端接入
PHP 端是上传流程的核心调度层,负责用户鉴权、签发签名 URL、代理合并请求。完整参考实现见仓库根目录 php生成配置.php。
uploadConfig() — 签发上传配置
POST /upload/uploadConfig
x-api-key: cH1rC************5xm6bp9k
{ "mime": "image/jpeg", "filename": "photo.jpg",
"size": 5242880 }
// 响应(单文件)
{ "isChunk": false, "key": "20240101/photo.jpg",
"putUrl": "http://112.132.215.28:9000/upload?...",
"chunkUrls": [], "chunkSize": 0 }
// 响应(分片)
{ "isChunk": 1, "chunkUrls": [...], "chunkSize": 2097152,
"key": "20240101/video.mp4", "uploadId": "abc123def456",
"notifyUrl": "https://your-server.com/upload/uploadNotify" }
uploadNotify() — 通知合并
$options = ['Key' => $key, 'UploadId' => $uploadId];
$optionsJson = json_encode($options);
$signature = hash_hmac('sha256', $optionsJson, $this->up_apikey);
$url = $this->up_apiurl . '/upload/complete?signature='
. $signature . '&options=' . urlencode($optionsJson);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
SECURITY
安全模型
| 攻击 | 向量 | 防御 |
|---|---|---|
| 非授权上传 / 删除 | 无签名请求 | HMAC 签名验证 → 403 |
| 签名重放 | 复用旧签名 URL | 签名含 Key + UploadId,无法跨文件复用 |
| 路径穿越写文件 | Key=/etc/passwd | 三层路径校验 → 400 |
| 路径穿越删除 | Key=../ | safePath 三层校验 → 400 |
| 磁盘写满 | 超大文件 | io.LimitReader 限流 + maxSize → 413 |
| 上传恶意文件 | .php、.exe | allowedFormats 白名单 → 400 |
| 时序攻击猜解签名 | 逐字节比较响应时间 | hmac.Equal 恒定时间比较 |
路径穿越三层防御
① filepath.IsAbs → 拒绝绝对路径 ② strings.Contains(clean,"..") ③ HasPrefix(fullPath, absDir+sep)
大小限制
单文件: io.LimitReader(maxSize+1) 分片合并: 合并后校验总 size 超限删除 不依赖 Content-Length header
孤儿分片清理
后台 goroutine 每小时扫描 目录 mtime > chunkTtl(24h) 则清理 合并成功在 complete 中即时删除
运维建议
- •生产加 nginx 反代,只暴露 /upload 与 /health,屏蔽 /upload/complete(仅 PHP 内网调用)
- •定期轮换 apikey,同步更新 PHP 与 config.yaml
- •监控 upload.dir 磁盘使用量,配置 allowedFormats 白名单
ERROR CODES
错误码汇总
| HTTP | code | 说明 |
|---|---|---|
| 200 | 200 | 成功 |
| 400 | 400 | 参数错误(路径非法、格式不允许) |
| 403 | 403 | 签名无效或缺少签名参数 |
| 404 | 404 | UploadId 不存在或无分片 |
| 413 | 413 | 文件超过 maxSize |
| 500 | 500 | 服务端错误 |