应使用hset而非set实现ip黑名单,因其支持存储封禁原因、操作人、解封时间戳等元数据,且可通过hgetall原子读取、expireat精准控制单条记录过期;而set仅能存简单值,无法满足复杂业务需求。

为什么不用 SET 而要用 HSET 实现 IP 黑名单
直接用 SET ip:192.168.1.100 1 EX 300 存单个 IP 看似简单,但无法携带额外元数据——比如封禁原因(rate_limit / brute_force)、操作人、解封时间戳。而哈希表能在一个 key 下存多字段:HSET ip:blacklist:192.168.1.100 reason rate_limit expires_at 1724000000 operator admin。更重要的是,HGETALL 可原子读取整条记录,避免多次往返 Redis;HEXPIRE(Redis 7.0+)或 EXPIREAT 配合 HSET 可精准控制单条记录过期,比整个 key 过期更灵活。
如何从 X-Forwarded-For 安全提取真实 IP 并校验
不能直接信任 c.ClientIP(),它默认只认 127.0.0.1,线上必然失效。必须配合可信代理配置:
- 在 Gin 初始化时调用
engine.SetTrustedProxies([]string{"10.0.0.0/8", "192.168.0.0/16"}),否则X-Forwarded-For可被伪造 - 手动解析头:
xff := c.Request.Header.Get("X-Forwarded-For"),按逗号分割后取最左非信任地址(如"203.201.10.5, 10.0.0.3"→ 取"203.201.10.5") - 若 Nginx 已设
proxy_set_header X-Real-IP $remote_addr,优先用c.Request.Header.Get("X-Real-IP"),更简洁可靠
中间件里怎么安全读 body 又不破坏后续绑定
防刷常需读 body 做内容特征判断(比如短信接口的手机号是否高频),但 c.ShouldBindJSON() 会再次读 c.Request.Body,导致 EOF 错误。正确做法是:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
ioutil.ReadAll(c.Request.Body)一次性读完 - 立即重置:
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - 注意:若 body 很大(>1MB),应加长度限制,避免 OOM;可先用
http.MaxBytesReader包裹
Redis 哈希黑名单的原子检查与写入逻辑
核心不是“查是否存在”,而是“查是否已封且未过期”。典型流程:
- 构造 key:
ip:blacklist:+realIP(如ip:blacklist:203.201.10.5) - 用
HGETALL读全部字段,检查expires_at字段是否 > 当前 Unix 时间戳 - 若已过期或不存在,用
HSET写入新记录,并立刻EXPIREAT设置过期时间(不能依赖HSET ... EX,哈希不支持原生命令) - 若存在且未过期,直接
c.AbortWithStatus(403);注意不要用DEL清旧记录再写,竞态下可能漏封
真正容易被忽略的是:Redis 连接超时或网络抖动时,不能让中间件 panic 或阻塞请求。所有 Redis 调用必须带 context timeout(如 ctx, cancel := context.WithTimeout(c.Request.Context(), 100*time.Millisecond)),失败时降级为放行,而非拒绝——宁可漏封,不可误杀。










