小程序请求不带 Referer,需用微信签名机制(sign/timestamp/nonce)校验+Redis 按 openid 限流+idempotency_key 幂等+ Nginx 兜底防护,且时间单位须统一。

为什么直接用 $_SERVER['HTTP_REFERER'] 拦不住小程序请求
小程序发起的 wx.request 请求默认不带 Referer,服务端读到的 $_SERVER['HTTP_REFERER'] 是空或不可信值。靠它做来源校验等于没设防。
真正有效的识别依据是微信签名机制——每次请求都应携带 sign、timestamp、nonce,且服务端需用约定密钥重算签名比对。
-
前端调用前必须用
md5(timestamp + nonce + secret)生成sign -
后端收到后先校验
timestamp是否在 5 分钟有效窗口内(防重放) - 再用相同逻辑重算
sign,严格区分大小写和拼接顺序 - 验证通过才进入后续逻辑,否则统一返回
401 Unauthorized
用 Redis 实现用户级 QPS 限流(非 IP 级)
小程序用户登录态是 openid,不是 IP。按 IP 限流会误伤同一 WiFi 下多个用户;按 openid 限流才合理。
推荐使用 Redis 的 INCR + EXPIRE 原子组合,避免竞态条件:
立即学习“PHP免费学习笔记(深入)”;
// 示例:每分钟最多 30 次接口调用
$openid = $this->getOpenidFromToken(); // 从 Authorization header 或 POST 中提取
$key = "rate_limit:{$openid}:api_v1_submit";
$redis->incr($key);
$redis->expire($key, 60);
if ($redis->get($key) > 30) {
throw new Exception('Request limit exceeded', 429);
}
- 务必用
$redis->expire()而非SET key val EX 60,因INCR可能创建 key 导致过期失效 - 若用 phpredis 扩展,确保版本 ≥ 5.3.0,老版本
expire()对不存在 key 返回 false 易漏判 - 不要在限流前查
GET再INCR,这会产生竞态,必须依赖原子操作
接口幂等性必须由客户端传 idempotency_key
防止恶意脚本反复提交(比如抽奖、下单),光靠频率限制不够,得靠业务层幂等控制。
要求小程序在请求头或参数中带上唯一 idempotency_key(如 UUID v4),服务端用它作为 Redis 键存处理状态:
- 收到请求先
SETNX idempotency_key:xxx processing 300(5 分钟过期) - 若设置成功,执行业务逻辑并最终写入结果到
idempotency_key:xxx:result - 若设置失败,直接返回缓存的结果(或查 DB 确认是否已成功)
- 注意:不能只依赖数据库唯一索引,因为网络超时后客户端可能重试,而 DB 插入可能已成功但响应未达
别忽略 Nginx 层的基础防护兜底
PHP 层限流失效时(比如 FPM 崩溃、代码绕过),Nginx 的 limit_req 是最后一道防线。
配置示例(按 $http_x_wx_openid 限流,需小程序主动透传):
map $http_x_wx_openid $openid_key {
default $http_x_wx_openid;
}
limit_req_zone $openid_key zone=api_per_user:10m rate=30r/m;
location /api/ {
limit_req zone=api_per_user burst=5 nodelay;
fastcgi_pass php-fpm;
}
- 必须用
map提取 header,不能直接用$http_x_wx_openid做 zone key,否则空值会打满一个桶 -
burst=5允许短时突发,但nodelay表示不延迟排队,超了直接 503,避免请求堆积拖垮 PHP - 这个配置无法替代 PHP 层签名和幂等校验,只是防流量冲击的辅助手段
最易被忽略的是时间窗口一致性:Redis 过期时间、签名 timestamp 容差、Nginx limit_req 的 rate 单位,三者必须统一用秒或分钟,混用会导致限流形同虚设。











