秒杀系统高并发优化实战(C++ / Drogon):后端接口梳理与设计——GET 还是 POST,防的从来不是抓包
"为避免抓包,理论上还是所有接口都用 POST 吧。"——这句话我在设计秒杀接口时认真纠结过。直觉上 POST 把参数藏进了 body,好像比 GET 安全;但真去翻抓包工具的输出,会发现 Wireshark 抓 HTTP 明文时,URL 和 body 是一个 TCP 流里的前后两段,POST 一点便宜都占不到。
这个误区如果不澄清,接口设计会跑偏成"全 POST"——然后为这个错误的理由付出真实的代价:读接口失去缓存能力、日志里看不到请求参数排障变难、REST 语义崩坏。本文把秒杀系统的接口清单、GET/POST 的取舍逻辑、以及"防抓包"真正的解法一次性讲透。
本文是「秒杀系统(C++ / Drogon)」系列的接口设计篇。配套仓库
seckill-cpp(GitHub: https://github.com/Hespethorn/seckill-cpp),当前v0.1.x已落地/api/health与/api/seckill两个接口,路由挂在src/main.cc。
一、接口设计:是什么、坑在哪、本质一句话
- 是什么:秒杀系统的接口设计,本质是把"读操作"和"写操作"梳理清楚,让幂等的读和非幂等的写各走各的路——这决定了缓存、限流、日志三个维度的策略能否生效。
- 坑在哪:两个高频误区。一是**"全用 POST 防抓包"——POST 防不了主动抓包,只防被动泄露(历史记录、日志、Referer),把安全期望寄托在错误的手段上;二是读接口滥用 GET 的参数设计**——把 userId 塞进 URL 当查询参数,虽然方便但会在日志里裸奔,且失去"参数不进日志"的红利。
- 本质一句话:接口方法的选择标准是"读/写语义 + 幂等性",安全靠 HTTPS、签名、Token、一次性令牌,而不是靠 POST 这层窗户纸。
二、秒杀系统接口清单(当前 + 规划)
先看阶段一已经落地的两个接口,再对照规划的完整清单:
1 | # 当前 v0.1.x(已实现) |
一眼看过去,读接口全是 GET、写接口全是 POST——这不是巧合,是下面这套规则推出来的。
三、GET vs POST:判断标准是语义,不是安全
判断一个接口用 GET 还是 POST,只需要回答两个问题:
- 它会改数据吗?(写 → POST;读 → 继续下一步)
- 它是幂等、可缓存的吗?(是 → GET;否 → POST)
秒杀下单为什么必须是 POST?因为它改库存、建订单、非幂等(同一请求发两次会产生两条副作用,全靠 uk_user_sku 兜底)。登录为什么 POST?因为它创建会话。商品列表为什么 GET?因为它只读且幂等,GET 让 CDN、浏览器、Redis 全部可以参与缓存。
四、POST 防的不是抓包,是泄露
把"防抓包"这层窗户纸捅破——POST 和 GET 在主动抓包面前是平等的:
| 泄露途径 | GET(参数在 URL) | POST(参数在 body) |
|---|---|---|
| 浏览器历史记录 | ❌ 完整暴露 | ✅ 不记录 |
| 服务器访问日志 | ❌ URL 全量入日志 | ✅ body 通常不进日志 |
| 反向代理 / 网关日志 | ❌ 暴露 | ✅ 不暴露 |
| Referer 头(跳转外站) | ❌ 带参数泄露 | ✅ 只带 URL 不带 body |
| 书签 / 分享链接 | ❌ 参数跟着走 | ✅ 无法通过链接传递 |
| 主动抓包(Wireshark / Fiddler / Charles) | ❌ 明文全抓 | ❌ 照样全抓 |
最后一行才是"抓包"的字面意思,而这一行 POST 毫无优势——抓包抓的是整个 TCP 流,URL 和 body 只是流里先后两段。所以:
POST 防的是"被动泄露"(日志、历史、Referer),防不了"主动抓包"。
五、防抓包/防重放:真正的四件套
攻击者抓到包之后,真正危险的是重放——拿到一个成功的秒杀请求,脚本反复发。这需要一套和 HTTP 方法无关的防护:
| 手段 | 防什么 | 实现要点 |
|---|---|---|
| HTTPS(TLS) | 被动抓包看明文 | Wireshark 只能看到密文;中间人需客户端信任其证书 |
| 签名(timestamp + nonce + HMAC) | 篡改 + 重放 | 客户端对参数做 HMAC,服务端校验签名 + 时间窗(如 ±5min)+ nonce 去重 |
| Token 鉴权 | 越权调用 | 登录后发 Token,请求头 Authorization: Bearer <token>,服务端校验 |
| 一次性秒杀令牌 | 脚本刷单 | 秒杀前先领令牌(限量),下单时校验,用完即焚 |
签名流程示意:
1 | 客户端: params + timestamp + nonce |
这套东西在阶段四(规划 8.x:防刷、限流、秒杀资格校验)会是主角,到时候 GET 接口同样要带签名。安全从来不依赖 GET/POST 的选择。
六、阶段一接口落地:当前怎么写的
src/main.cc 里的路由注册,已经按"读 GET / 写 POST"划分:
1 | // 健康检查:只读,GET |
秒杀下单的 body 协议(SeckillController.cc):
1 | // 请求: POST /api/seckill |
关键设计点:userId / skuId 放在 body 而不是 URL——这既符合 POST 语义,也顺便获得了"日志不记录参数"的被动泄露防护。但注意,这不是安全措施,阶段四给下单接口加签名 + 一次性令牌时,body 里还要追加 timestamp/nonce/sign。
七、可运行验证步骤
1 | # 1. 启动服务(WSL) |
第 4、5 步是"方法即契约"的直接体现——接口声明了 POST,GET 就不该能调到它,这也是防"误用接口"的第一道防线。
八、设计取舍总结
| 决策点 | 选定方案 | 放弃方案 | 放弃的代价 |
|---|---|---|---|
| 读接口方法 | GET | 全 POST | 无——GET 白拿缓存/日志/语义 |
| 写接口方法 | POST | 全 GET | 无——body 藏参数 + 非幂等语义正确 |
| 敏感参数位置 | body | URL 查询串 | 排障时得看请求体(但日志也更干净) |
| 防抓包 | HTTPS + 签名 + Token + 一次性令牌 | 靠 POST | 需实现签名/令牌(阶段四做) |
| 方法越权 | Drogon 自动 405 | 应用层手动判断 | 无(框架白送) |
一句话收尾:接口方法是语义决策,不是安全决策——读用 GET 拿缓存红利,写用 POST 藏好参数,防抓包交给 HTTPS 和签名这套"真家伙"。把"全用 POST 防抓包"这个念头放下,接口设计才算真正想明白了。
配套仓库:
https://github.com/Hespethorn/seckill-cpp(当前接口见src/main.cc路由与src/controllers/SeckillController.cc协议转换)。本系列是作者个人的 C++ 秒杀系统实战记录,所有方案、代码与压测数据均为原创。

