因为fiber.Limiter默认Filter只允许GET请求,而/login是POST接口;必须自定义Filter放开POST方法,并按IP+用户名拼key、用Redis存储实现防爆破的动态限流。

为什么 fiber.Limiter 默认不拦 POST /login
因为 fiber.Limiter 默认只对 GET 请求生效,而登录接口通常是 POST。直接套用官方限流中间件,你会发现请求完全没被限制——这不是 bug,是设计如此:fiber.Limiter 的默认 Filter 函数内部写了 c.Method() == "GET" 判断。
实操建议:
- 必须显式传入自定义
Filter,放开 POST 方法,例如:fiber.Limiter{ Filter: func(c *fiber.Ctx) bool { return c.Path() == "/login" && c.Method() == "POST" }, Max: 5, Expiration: 1 * time.Minute, } - 别把
Filter写成闭包里捕获变量(比如从 config 动态读取路径),Fiber 中间件初始化时就执行一次,后续不会重求值 - 注意路径匹配是严格字符串比对,
/login不会匹配/login/或/api/login
如何按 IP + 用户名双重维度限流
单纯按 IP 限流容易误伤(同一出口 NAT 后多人共用),只按用户名又无法防爆破(攻击者轮换账号)。Fiber 原生 Limiter 只支持单 key,得自己拼 key。
实操建议:
- 在
Filter返回 true 后、实际限流前,用c.Locals注入组合 key:c.Locals("limiter_key", c.IP()+":"+c.FormValue("username")) - 配合自定义
Store(如fiber.NewMemoryStore())并重写Get/Set方法,在 key 中嵌入该 locals 值 - 更稳妥的做法是绕过
fiber.Limiter,直接用redis.Client+INCR+EXPIRE手写逻辑,key 设为login:ip:username,避免内存 store 在多实例下失效
登录失败后自动触发更强限流(防暴力破解)
常规限流是“固定窗口”,但真实攻击常在失败后立刻换账号重试。需要动态升级策略:单 IP 累计 3 次失败,后续所有登录请求降为 1次/5分钟。
实操建议:
- 用 Redis 记录失败计数,key 如
failed_login:<ip></ip>,每次失败INCR并设EXPIRE 15m - 在限流中间件中先查这个 key:
if cnt, _ := rdb.Get(ctx, "failed_login:"+c.IP()).Int(); cnt >= 3 { // 启用严苛限流:1次/5分钟 } - 成功登录后,务必
DEL failed_login:<ip></ip>,否则用户正常登录一次就永久被锁 - 别依赖内存计数——多进程部署时各实例数据不共享,必须走 Redis 或其他外部存储
前端提示语和状态码怎么配才不露馅
返回 429 Too Many Requests 是标准做法,但直接暴露“你还剩 X 次机会”等于帮攻击者校准节奏;返回通用错误又让合法用户困惑。
实操建议:
- 始终返回
401 Unauthorized或400 Bad Request,和普通登录失败一致,不区分是否因限流拒绝 - 日志里单独记录限流事件,字段包含
reason: "rate_limited",方便后台审计 - 前端统一提示“账号或密码错误”,不要加“请稍后再试”之类暗示性文案——那等于告诉对方“你快撞对了”
- 如果真要给用户友好提示,改用异步方式:登录失败后,前端静默发个
GET /login/status查询当前 IP 是否处于惩罚期,再决定是否显示“操作频繁,请 1 分钟后重试”
Fiber 的限流不是开箱即用的防护盾,它更像一块可塑的铁坯——IP 维度、账号维度、失败反馈、响应伪装,每个环节都得亲手锻打。最容易被忽略的是多实例部署下的状态同步,以及把限流逻辑和业务逻辑耦合在同一个 handler 里导致的延迟判断。


















