Fiber默认不防刷验签,因无内置签名验证与限流机制;必须显式注册中间件:先验签(用hmac.Equal恒定比对,校验timestamp/nonce/body+密钥),再限流(基于X-App-ID的自定义keyGenerator),顺序不可颠倒。

为什么直接用 fiber.New() 无法防刷验签
因为 Fiber 默认不处理请求签名验证和频率控制,所有中间件都是按需加载的。没显式注册验签或限流逻辑,ctx.Request().URI() 就直接进业务 handler 了——攻击者能反复发相同参数的请求,服务端毫无感知。
验签和防刷必须拆成两个独立环节:先验签(确认来源合法),再限流(限制合法来源调用频次)。顺序不能反,否则恶意请求可能绕过签名直接打爆限流器。
- 验签失败应返回
401 Unauthorized,不计入限流统计 - 签名有效期建议设为 5–10 分钟,用
time.Now().Unix()+nonce防重放 - 密钥不要硬编码在代码里,从环境变量读取,比如
os.Getenv("API_SECRET")
怎么写一个轻量级验签中间件
核心是比对客户端传来的 sign 参数是否由约定字段(如 method、path、timestamp、nonce、body)+ 密钥拼接后 HMAC-SHA256 得到。注意 body 必须只读一次,所以得用 ctx.Body() 提前取出并缓存。
func SignVerify() fiber.Handler {
secret := os.Getenv("API_SECRET")
return func(c *fiber.Ctx) error {
timestamp := c.Get("X-Timestamp")
nonce := c.Get("X-Nonce")
sign := c.Get("X-Sign")
if timestamp == "" || nonce == "" || sign == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "missing sign headers"})
}
// 防重放:10分钟窗口
if t, _ := strconv.ParseInt(timestamp, 10, 64); time.Now().Unix()-t > 600 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "timestamp expired"})
}
// 读 body(仅一次)
body, _ := c.Body()
raw := fmt.Sprintf("%s%s%s%s%s", c.Method(), c.Path(), timestamp, nonce, string(body))
expected := fmt.Sprintf("%x", hmac.Sum256([]byte(raw+secret)))
if !hmac.Equal([]byte(sign), []byte(expected)) {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid sign"})
}
return c.Next()
}
}
- 务必用
hmac.Equal()做恒定时间比对,防止时序攻击 -
c.Body()会消费原始 body,后续 handler 再调c.Body()就是空的,需要手动重置:用c.Request().SetBodyRaw(body) - 如果接口允许 GET 不带 body,验签时 body 应为空字符串,别漏判
怎么配合 fiber.Limiter 实现防刷
fiber.Limiter 默认基于 IP 限流,但 API 场景更需要按 app_id 或 client_id 限流——这得靠自定义 keyGenerator。同时要注意:验签中间件必须在限流中间件之前,否则非法请求也会被计数。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
limiter := fiber.Limiter{
NewLimitFunc: func(ctx *fiber.Ctx) (int, time.Duration, error) {
// 每个合法 app_id 每分钟最多 100 次
return 100, time.Minute, nil
},
KeyGenerator: func(c *fiber.Ctx) string {
// 从已通过验签的 header 中取标识,不是 IP
return c.Get("X-App-ID")
},
}
- 如果验签失败,
X-App-ID可能不存在,此时KeyGenerator返回空字符串会导致所有非法请求挤占同一个桶,必须前置拦截 - 高并发下,
fiber.Limiter默认使用内存存储,重启即清零;生产环境建议换redis后端,用github.com/gofiber/fiber/v2/middleware/limiter的 RedisStore - 不要把限流阈值设太死,例如写成
return 5, time.Second, nil,容易误杀正常重试
上线前必须检查的三个坑
很多团队本地测试全通,一上预发就 401 或限流误触发,问题往往出在细节。
- 客户端生成 sign 时用了错误的 body 编码:比如前端用
JSON.stringify()后没去除空格,而 Go 后端用json.Marshal()默认带缩进——二者哈希值必然不同 - 时间戳用的是客户端本地时间,且没校准 NTP,导致频繁过期;应强制要求客户端用服务端时间(首次握手返回
X-Server-Time) - Fiber 的
c.Get()对 header 名大小写不敏感,但某些代理(如 Nginx)会把X-Sign改成x-sign再转发,实际仍可读取,但如果你写了c.Request().Header.Peek("X-Sign")就会失败
验签和防刷的边界很清晰:签名管“是不是你”,限流管“你能不能这么刷”。两者叠加时,任何一环的松动都会让整个防护形同虚设。

















