安全三件套:限流状态外置 Redis + 公开接口反爬(轻量)+ 边缘层加固(todo #1/#2/#3)
2026-07-05 定稿 · 执行真相源为 plan .md,本页为阅读版 · 状态:已批准,本地实现完成待发版
Context
- 三条安全 todo 一次做完。现状:限流桶(
internal/middleware/ratelimit.go keyedLimiter)与登录失败锁(internal/service/auth.go:45-120 loginLimiter)全是进程内存态,重启清零、无法多实例;商品公开接口 public_id 是 UUIDv4 不可枚举,但店铺列表接口(shop_code 可猜,S001…)是真实爬取面;ali 安全组还留着 3389 对全网开放的系统默认残留,nginx 只有 limit_req 无 limit_conn。原 todo #1 写的 AWS Shield/EC2 已过时(EC2 已下线),按阿里云现状重定义。#3 用户拍板轻量版(日配额+UA 拦截+防盗链;不做滑块/签名/登录墙)。
- 代码顺序 #2 → #3 → #1(#3 日配额复用 #2 的 Counter;#1 纯运维)。所有生产变更(发版 / ssh ali / 安全组 CLI)单独设批准点,用户明示后才执行;本地改码+测试先行。机器仅 1.8GiB RAM(mysql+jiu+payd+nginx 在跑),redis 内存上限克制。
阶段 #2 · 限流/登录锁外置 Redis
- 外置:per-IP/per-shop 限流桶、登录失败计数与锁、(为 #3 预留)日配额 Counter。不动:web ticket / 会话(已在 MySQL)、login_attempts 审计表。
- ☑新包
backend/internal/ratelimit:store.go(接口+Init(addr)/Default(),addr 空或 Ping 失败→内存并 warn,Default() 未 Init 惰性内存——现有测试零改动的关键)、memory.go(keyedLimiter+loginLimiter 原样迁入,janitor 一起)、redis.go(限流用 go-redis/redis_rate/v10 GCRA 对齐 rate+burst;登录锁 INCR+EXPIRE 30m、达阈值 SET locked: EX 并清计数,三行 Lua 保原子;键前缀 jiu:rl:)、fallback.go(redis 报错逐调用降级内存+限频 warn,恢复自动回 redis)。接口:Limiter.Allow(key)→{Allowed,RetryAfter}、Counter.Incr(key,ttl)→int64、FailLocker.Locked/RecordFailure(key,max,lockFor)/Reset
- ☑调用方改造:
middleware/ratelimit.go 改用 ratelimit.Default().NewLimiter(n,per,burst),429 用 Result.RetryAfter;RateLimitByIP/ByShop 签名改 (n,per,burst),router.go:56-61 六处同步;Enabled=false 全放行、keyFn 空串放行语义不变。service/auth.go 删 loginLimiter(~75 行)改 ratelimit.Default().FailLocker(),lockFor 传 config.C.Session.LockMinutes。main.go 在 config.Load 后 ratelimit.Init(config.C.Redis.Addr)。config/config.go 加 Redis.Addr + BindEnv("redis.addr","REDIS_ADDR"),默认空=内存模式
- ☑基础设施:go.mod 加 go-redis/v9、redis_rate/v10、miniredis/v2(test)。
deploy/docker-compose.jiu.yml 加 jiu-redis:redis:7-alpine、127.0.0.1:6379、--maxmemory 64mb --maxmemory-policy noeviction --appendonly no --save "" --rename-command CONFIG ""、无 volume(刻意无持久化:redis 重启丢限流计数可接受,noeviction 防锁键被逐出,回环无密码 payd 容器够不着)。production.env.template 加 REDIS_ADDR=127.0.0.1:6379(非密钥,deploy-server.sh 漂移检查正好当上线核对项)
- ☑测试:现有 ratelimit/auth 测试零改动通过(内存路径);
memory_test.go 迁移对等用例;redis_test.go(miniredis:rate+burst、锁定+FastForward 过期解锁、Counter TTL);fallback_test.go(Close 后不 500、降级仍限流);Init("坏地址") 不 panic。DoD:go build/vet/test ./... 全绿、零真实外部依赖
阶段 #3 · 公开接口反爬(轻量版)
- ☑日配额二级闸:
middleware/dailyquota.go——DailyQuota(scope, limit),key=dq:<scope>:<yyyymmdd>:<ip>,Counter().Incr(key, 26h),超限 429+Retry-After(到午夜);Counter 出错放行(不误伤)、Enabled=false/limit=0 放行。配置默认:daily_product_per_ip=1000(单品 API+OG 页同池)、daily_shoplist_per_ip=300。挂载:/product/:public_id、public/products/:public_id、public/shops/:shop_code/products;release/errors/pay-callback 不挂。内存版 Counter 带 TTL janitor(防随机 IP 灌爆)
- ☑店铺列表收紧:
handler/public.go:235 page_size 上限 50→20(已核实唯一调用方 client 固定传 20,无破坏);public_test.go 加断言:公开两接口响应 JSON 不含 cost/purchase_price 等敏感字段名(防未来回归泄露,参考 pricing_fields_test.go 写法)
- ☑测试:dailyquota 小配额打满 429、跨 scope 独立、关闭放行、Counter 错误放行;page_size 夹到 20
阶段 #1 · 边缘层加固
- ☑nginx(
deploy/nginx-jiu-ali.conf,CI 随 server 发版下发):
- - 新 zone:
jiu_shoplist 2r/s(列表专属,location ~ ^/api/v1/public/shops/ 须放现有 (public|auth) 正则之前)、limit_conn_zone jiu_conn、jiu_dl
- - UA map ×2:
$jiu_bad_ua(空 UA + python-requests/scrapy/go-http-client/aiohttp/okhttp 等抓取库)用于 API 公开路径;$jiu_bad_ua_lib(同表去掉空 UA)用于 /product/、/app/product/ OG 页——社交分享爬虫 UA 不可控,OG 路径不拦空 UA;不封 curl/wget(运维自用+攻击者改 UA 零成本)
- - 公开 API location 加
limit_conn jiu_conn 20(shops 路径 10);/downloads/ 加 limit_conn jiu_dl 3 + limit_rate_after 10m; limit_rate 4m(上线前查实例带宽再定值)
- -
/images/ 防盗链:valid_referers none blocked server_names 51yanmei.com *.51yanmei.com,invalid→403(none 放行空 Referer——微信 webview/App 直连必须放行;只挡第三方网页热链)
- ☑安全组(aliyun CLI,批准点):Describe 留档 →
RevokeSecurityGroup 删 3389/0.0.0.0/0 → 复核;22/443/80/ICMP 不动 → baize 台账记变更
- ☑DDoS runbook:新建
docs/runbooks/ddos-response.html(沿用项目深色 HTML 家族样式)+ 登记 docs/index.html。内容:判定指标(带宽/QPS/CPU steal/控制台 DDoS 事件页)、基础防护黑洞机制(约 5G 阈值、黑洞 2.5-24h)、决策树(CC→调紧 zone+封 IP;容量型→按量高防 ¥2-3k/月起 CNAME 切换 30min,或套 CDN/ESA,回源 182.92.213.171:443)、DNS TTL 平时保持 ≤600s、黑洞期客户公告话术。明确决策:不预购高防/CDN,接受极端容量攻击下的黑洞期不可用
上线顺序(每个【批准点】单独等用户点头)
- 本地全部改码+测试全绿 → 分阶段 commit(feat(backend) ×2 + devops ×1 + docs ×1)
- 【批准点】server 发版走 CI:后端二进制(线上无 REDIS_ADDR → 仍内存模式,行为与现状一致的安全中间态)+ 新 nginx conf 同时带出 → 验收 nginx 项(UA 403 / 防盗链 403 / 微信分享卡片不误伤 / hey 并发出 429)
- 【批准点】ssh ali:
free -m 核内存(余量 <150MB 则 maxmemory 降 32mb)→ 部署 compose 拉起 jiu_redis → ping 通 → production.env 加 REDIS_ADDR → systemctl restart jiu → 验收:触发限流看 429、redis-cli --scan 'jiu:rl:*' 有键、restart jiu 后键仍在(跨重启达成)、docker stop jiu_redis 降级不 500 再 start
- 【批准点】aliyun CLI 删 3389 → baize 记账
- 回滚路径:nginx=上一 tag 重发;redis=env 删 REDIS_ADDR 重启(回内存模式);安全组=Authorize 原参数回加
- 收尾:todo #1/#2/#3 标 done 等验收;plan 定稿 HTML 阅读版落
docs/、登记 index(全局双产物规则)
明确不做(写入 runbook/commit message)
- 签名链接(QR 已印刷不可过期、UUIDv4 已不可枚举)|滑块/验证码(过重,杀零门槛分享体验)|登录后可见(杀死扫码分享功能)|预购高防/CDN(成本不匹配体量,转 runbook 预案)|封 curl/wget/泛"bot" UA(误伤运维与社交分享爬虫)
风险
- 1.8GiB 内存紧:redis 实占 <80MB,上线前实测 free -m
- REDIS_ADDR「模板有、线上无」窗口期 deploy-server.sh 会告警一次(步骤 2→3 之间),预期内
- shops 路径从 jiu_pub zone 移入 jiu_shoplist 后不再占原配额,行为变化在预期内