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