黑名单中间件必须手动读取原始body,不能依赖bodyparser;需用c.request().body()或c.body()获取字节,解析关键字段后及时setbodyraw保留给下游,命中时立即返回403并return阻断链路。

黑名单中间件必须手动读取原始 body,不能依赖 BodyParser
微信/支付宝回调、某些风控接口的原始请求体是 XML 或 raw form,Fiber 默认的 BodyLimit 和自动解析逻辑会提前消费 c.Body(),导致后续黑名单校验拿不到原始字节。黑名单判断往往需要完整请求体(比如验签、提取 client_id、解析 XML 中的 appid),所以中间件里得用 c.Request().Body() 或 c.Body() 直接读,且只能读一次。
常见错误:在黑名单中间件里调 c.BodyParser(&v) → 解析失败或 body 被清空 → 后续 handler 拿不到数据;或者没做 defer c.Request().ResetBody() 导致复用时 body 为空。
- 正确做法:用
c.Body()拿原始字节,自己用xml.Unmarshal或url.ParseQuery解析关键字段 - 若需保留 body 给下游 handler(如日志中间件),读完后调
c.Request().SetBodyRaw(bodyBytes) - 别在中间件里调
c.Next()前做耗时操作(如查 Redis)—— 黑名单匹配必须快,否则拖慢所有请求
IP + User-Agent + 请求路径组合成黑名单 key,避免单点误杀
只封 IP 太粗暴,NAT 网关后一堆用户共享一个出口 IP;只封 User-Agent 容易被伪造;只封路径又太窄。生产环境建议三者拼接,例如 "blacklist:" + c.IP() + ":" + c.Get("User-Agent", "") + ":" + c.Path(),再查本地缓存或 Redis。
注意:c.IP() 在反向代理后可能返回内网地址,优先用 c.Get("X-Real-IP"),fallback 到 c.Get("X-Forwarded-For") 取第一个非空值,再 fallback 到 c.IP()。
- Key 命名加前缀(如
blacklist:)方便批量清理或监控 - 用
sync.Map缓存高频访问的黑白名单,避免每次查 Redis;但注意它不支持 TTL,长期不变的规则才适合放这里 - Redis 方案推荐用
EXISTS blacklist:1.2.3.4:curl/8.0:/api/login,原子判断,不拉回全量数据
拦截后必须立即返回且 return,不能调 c.Next()
黑名单中间件一旦命中,就要终止整个链路。Fiber 不像 Express 那样靠 next('route') 跳过,而是靠“不调 c.Next()”来阻断。如果写了 c.Status(403).SendString("blocked") 却没 return,后续 handler 仍会执行,可能重复写响应或泄露敏感信息。
- 标准写法:
c.Status(403).SendString("blocked"); return - 别用
c.JSON()返回结构体 —— 黑名单响应要极简,减少序列化开销,403 + 纯文本最快 - 如果用了自定义错误处理器(
Config.ErrorHandler),确保它不覆盖黑名单中间件已写的 status 和 body
路由级绑定黑名单中间件,避免全局污染
不是所有接口都需要黑名单控制,比如健康检查 /health、静态资源 /static/*。用 app.Use() 全局挂载会拖慢所有请求;应该按需绑定到具体路由或路由组。
例如:登录接口和支付回调最常被攻击,就单独加;而公开文档页不需要。
- 单路由:
app.Post("/login", blacklistMiddleware, loginHandler) - 路由组:
api := app.Group("/api/v1", blacklistMiddleware); api.Post("/pay", payHandler) - 千万别把黑名单中间件写在
app.Use("/api", ...)里 —— 它会对/api下所有方法(GET/POST/PUT/DELETE)生效,包括你不想拦的 OPTIONS 预检请求











