网页被爬还能靠前端混淆挡一挡,APP 和小程序的接口一旦被抓包,爬虫就能绕开页面直接拿数据。本文用 9 个问答讲清接口被爬的定位方法、签名与防重放的正确设计、网关限速配置,代码可直接复制。
Q:APP和小程序的接口为什么比网页更容易被爬?
因为接口把数据做成了「开箱即用」的形态,爬虫的边际成本几乎为零。
- 接口即数据:一次请求返回结构化 JSON,不用解析 HTML 和动态渲染,抓取效率是网页的几十倍。
- 无 Cookie 无验证码:很多接口只认一个 token,token 拿到就能批量刷。
- 密钥在客户端:签名逻辑打包在 APP 或小程序包里,反编译即可拿到算法与密钥。
- 抓包门槛低:服务端证书校验缺失时,一个代理工具就能看到全部接口与参数。
Q:接口被爬和网页被爬,危害差别在哪?
- 数据全量外流:价格、房源、题库、用户资料可以一次性被拖走,直接被竞品复用。
- 直接压数据库:接口是「贵」资源,每个请求都要查库,被刷时数据库压力远高于页面抓取。
- 计费与对账失真:接口调用量虚高,容量规划和流量对账都会被带偏。
- 账号体系受牵连:有些接口返回用户维度信息,抓取的同时可能伴随越权试探。
Q:怎么定位是哪些接口在被刷?
先把接口清单和调用量对齐,再按「接口维度 + IP 维度」交叉筛。
# 1. 调用量最高的接口(去掉查询串,聚焦接口本身)
grep " 200 " /var/log/nginx/api.log | awk '{print $7}' \
| sed 's/?.*//' | sort | uniq -c | sort -rn | head -20
# 2. 单接口下的请求来源集中度(前 10 个接口逐一看)
awk '{print $7}' /var/log/nginx/api.log | sed 's/?.*//' \
| sort | uniq -c | sort -rn | head -5
# 3. 某个列表接口里,请求数最高的 IP(超过 N 次即重点怀疑)
awk '$7 ~ /\/api\/v1\/list/ {print $1}' /var/log/nginx/api.log \
| sort | uniq -c | sort -rn | head -20
经验阈值:正常用户在单个列表接口上一天几十次以内;单 IP 超过几百次、且只调数据接口不调其他接口,基本可以定性为采集。第一版先记录,别急着封 —— 先把可疑 IP 集合固化下来,再看它的行为模式。
Q:接口签名应该怎么设计?
核心原则是「服务端可独立复算、客户端无法只改参数就通过」。最小可用方案:时间戳 + 随机串 + 参数排序 + HMAC。
# 签名串 = 方法 + 路径 + 排序后的参数 + 时间戳 + 随机串
# 再与服务端保管的密钥做 HMAC-SHA256,放进请求头
sign_str = method + "\n" + path + "\n" + "&".join(
"%s=%s" % (k, params[k]) for k in sorted(params)
) + "\n" + str(ts) + "\n" + nonce
sign = hmac.new(secret.encode(), sign_str.encode(), hashlib.sha256).hexdigest()
# 请求头
X-Ts: 1758700000
X-Nonce: 8f3c1a92
X-Sign: 3d1f...e9
四个必做细节:参数必须排序后拼接否则客户端可以乱序绕过;时间戳和随机串必须进签名串否则无法防重放;空值参数也要参与否则可以增删字段;服务端比对用常量时间函数避免时序侧信道。
Q:签名被反编译破解了怎么办?
这是常态 —— 只要密钥在客户端,就一定能被拿到。破解之后的加固方向是让「一次破解」不能长期复用。
- 密钥不下发到客户端:登录后由服务端下发短期 token(1 到 2 小时有效),token 与设备绑定,过期必须重新登录。
- 敏感接口走服务端转发:客户端只带登录态访问自己的 BFF 层,真正的业务密钥只在服务端之间流转。
- 关键接口二次校验:登录态 + 设备指纹 + 风险评分,三项都通过才返回完整数据。
- 缩短签名有效期:把时间窗压到 5 分钟以内,破解释放的价值随时间快速衰减。
要点是不要指望前端混淆。混淆只能抬高门槛,不能提供安全边界。
Q:防重放为什么必须做?怎么做?
因为抓包拿到的是完整可重放的请求。只校验签名的话,攻击方把请求原样重发就能通过 —— 签名是对的,参数也是对的。
# 1) 时间窗校验:超出 300 秒一律拒绝
now = int(time.time())
if abs(now - ts) > 300:
return 401, "sign expired"
# 2) nonce 一次性:Redis SET NX EX,重复即判重放
key = "api:nonce:" + nonce
if not redis.set(key, "1", nx=True, ex=600):
return 401, "replay detected"
两个注意点:nonce 的过期时间要大于时间窗,否则窗口内仍可重放;Redis 要独立实例或独立 DB,别和业务缓存抢资源,被刷时不能连带失效。
Q:怎么识别爬虫的参数枚举行为?
参数规律是接口类爬虫最硬的指纹,比请求频率更可靠。
- ID 连续递增:正常用户点开的是随机 ID,爬虫按 1、2、3 顺序遍历。
- 分页越界:正常用户翻不了几十页,爬虫会翻到几百上千页。
- 参数越界:一次性把时间范围、地区、价格拉成全集,普通用户不会这么查。
- 调用链缺失:正常用户先调详情再调评论,爬虫只调数据接口,从不调次要接口。
# 抓 ID 连续枚举:连续 20 个递增参数即输出片段
awk -F'id=' '{split($2,a,"&"); print a[1]}' /var/log/nginx/api.log \
| awk 'NR==1{p=$1;n=1;next} {
if ($1==p+1) {n++; if (n==20) print "seq-attack around", p}
else n=1;
p=$1 }'
识别出来之后按指数退避处置:先降速,再加验证码,最后才封 IP。直接封会误伤,尤其是共用出口 IP 的企业用户和运营商 NAT 用户。
Q:网关层怎么做限速与封禁?
接口限速和页面限速要分开建 zone,阈值完全不同。动态接口的业务价值高、成本高,阈值必须更严。
# 接口专用限速区:按 IP 10 r/s,突发 20
limit_req_zone $binary_remote_addr zone=api_rl:20m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=api_conn:20m;
location /api/ {
limit_req zone=api_rl burst=20 nodelay;
limit_req_status 429;
limit_conn api_conn 20;
add_header Cache-Control "no-store";
proxy_pass http://backend;
}
# 登录/发码类接口单独一条,阈值压到 1 r/s、突发 3
limit_req_zone $binary_remote_addr zone=login_rl:10m rate=1r/s;
三条经验:不要在 if 块里写 limit_req(Nginx 不支持,会直接报错);429 响应不要设 cache,否则被缓存后正常用户也被挡;白名单接口要显式排除,比如健康检查、回调通知。
Q:还有什么补充建议?
- 返回数据先做减法:分页上限(比如最多 50 条)、字段按角色下发、手机号等敏感字段脱敏。少给一分数据,抓取的价值就低一分。
- 把接口清单管起来:内部维护接口表(路径、鉴权方式、限速阈值、负责人),无鉴权的旧接口最容易成为入口。
- 监控三个指标:单接口 QPS、单 token 日请求数、单 IP 参数枚举次数。前两个做容量告警,第三个做安全告警。
- 与 CDN/WAF 联动:把接口路径单独建防护规则,静态资源仍走缓存,两条链路互不干扰,改动风险最小。
- 定期用抓包工具自测:自己模拟一次抓包,看看能拿到哪些接口和参数。能自己复现的问题,别指望攻击者发现不了。