租户目录隔离的关键在于路径构造前必须完成租户上下文校验,即严格使用中间件已验证的c.mustget("tenant_id")拼接路径,禁止从表单或url直接取值,且须在路由前注册中间件并显式校验tenant_id有效性。

租户目录隔离必须在文件路径拼接前完成
文件上传路径若不带租户上下文,所有租户的文件会混存到同一目录下,后续无法做权限回收、配额统计或独立备份。关键不是“怎么存”,而是“用哪个 tenant_id 去拼路径”——这个值必须来自中间件已校验过的 c.MustGet("tenant_id"),绝不能从 c.PostForm("tenant_id") 或 URL query 里直接取。
常见错误是上传接口没走租户中间件,或者中间件执行顺序错位(比如放在了 c.Next() 之后),导致 c.Get("tenant_id") 返回 nil,最终路径变成 /uploads//avatar.jpg 这种双斜杠,或 fallback 到默认租户目录。
- 确保租户中间件注册在所有文件路由之前:
r.Use(middleware.TenantContext()) - 上传 handler 内必须显式检查:
if tid, ok := c.MustGet("tenant_id").(string); !ok { c.AbortWithStatusJSON(400, gin.H{"error": "invalid tenant context"}) } - 路径拼接示例:
uploadPath := filepath.Join("uploads", tid, "avatars"),不要用字符串拼接"uploads/" + tid + "/avatars"(Windows 路径分隔符不一致)
上传前需校验租户级存储配额与文件类型白名单
多租户场景下,单个租户上传超限或传恶意文件(如 .php、.sh)会直接影响其他租户安全。这些检查不能只靠前端限制,必须在 Gin handler 中落地。
配额检查要查数据库或 Redis 缓存,而不是内存变量;文件类型必须基于 content-type + 文件头魔数双重校验,不能只信 Content-Type header(易伪造)。
- 调用前先查租户当前已用容量:
used, err := storage.GetUsedQuota(tid),再比对本次上传大小:c.Request.ContentLength - 读取前 512 字节做魔数检测:
buf := make([]byte, 512); _, _ = c.Request.Body.Read(buf),再用http.DetectContentType(buf)辅助判断 - 白名单应配置化,例如:
allowedTypes := map[string]bool{"image/jpeg": true, "image/png": true},拒绝application/x-php等高危类型
文件名需重命名并绑定租户上下文
用户上传的原始文件名不可信,含路径遍历(../../etc/passwd)、SQL 注入(file'; DROP TABLE users; --.jpg)或编码绕过风险。重命名不是为了“好看”,是为了消除攻击面。
重命名规则必须包含租户标识和唯一性因子,否则不同租户上传同名文件会覆盖——这不是 bug,是严重数据事故。
- 推荐格式:
fmt.Sprintf("%s_%s_%d%s", tid, base64.StdEncoding.EncodeToString(hash[:4]), time.Now().UnixNano(), ext) - ext 必须从魔数检测结果中提取,而非
filepath.Ext(originalName) - 禁止使用
uuid.NewString()单独生成文件名——它不带租户信息,后续无法按租户批量清理
上传后必须同步更新租户元数据
文件写入磁盘或对象存储只是第一步。如果租户的总用量、最新文件时间、文件数量等元数据不同步更新,配额控制、审计日志、垃圾回收都会失效。
这个更新操作必须和文件写入处于同一事务(DB 场景)或至少保证最终一致性(对象存储场景)。别用 goroutine 异步更新——失败无感知,数据会长期漂移。
- DB 场景:用 GORM 的
Transaction包裹文件写入 + 元数据更新 - 对象存储场景:先写文件,再调用
storage.IncrTenantUsage(tid, size),失败时触发告警并人工核对 - 元数据字段建议至少包含:
tenant_id、file_path、size_bytes、uploaded_at、uploader_id
c.MustGet("tenant_id") 拿不到值,或重命名漏掉 tid,整个隔离就形同虚设。











