API 客户端响应处理
非浏览器客户端被拦截 / 挑战 / 限流时收到什么:X-Mo-Guard 头、JSON 结构、PoW 兜底与「配 API 画像免挑战」。
墨守拦截或挑战一个请求时,通过 HTTP 状态码 + X-Mo-Guard 响应头 + 响应体告诉你发生了什么。浏览器会收到好看的 HTML 页;非浏览器客户端(Accept 不含 text/html)一律收到机器可读的 JSON,方便在代码里分支处理。这一页写给微信小程序、App、fetch / XHR、服务端调用的开发者。
#一眼判断:X-Mo-Guard 响应头
只要响应带了 X-Mo-Guard 头,就说明是 WAF 的处置,不是你后端返回的。先看有没有这个头,有就按下表处理;没有就是你后端的正常响应。
| 状态码 | `X-Mo-Guard` | 含义 | 客户端建议 |
|---|---|---|---|
| 403 | blocked | 命中攻击检测(SQLi / XSS / RCE…) | 提示「请求被安全策略拦截」,把 ref 一并显示 |
| 403 | denied | IP 黑名单 / 地理围栏 / UA 黑名单 | 同上 |
| 403 | bot-blocked | 判定为自动化 / 异常客户端,或签名校验未通过 | 同上;若是正常小程序,去配 API 画像 |
| 413 | body-too-large | 请求体超限 | 减小上传体积 |
| 401 | challenge-api | 需要人机验证(API 模式) | 见下文,或配 API 画像直接免掉 |
| 429 | rate-limited | 触发限流 / CC 防御 | 退避重试,有 Retry-After 就按它来 |
| 503 | overloaded / waiting-room | 过载卸载 / 排队中 | 稍后重试 |
#拦截响应(403)的 JSON 结构
{
"error": "request_blocked",
"action": "blocked", // blocked | denied | bot-blocked | body-too-large
"reason": "检测到潜在的攻击特征", // 中文原因,可直接展示
"ref": "A1B2C3D4E5", // 事件编号,给管理员排查误拦用
"by": "mo-guard"
}小程序里的处理示例:
wx.request({
url, method, data,
success(res) {
const g = res.header['X-Mo-Guard'] || res.header['x-mo-guard']
if (g && res.statusCode === 403) {
const d = res.data || {}
wx.showModal({
title: '访问被安全网关拦截',
content: (d.reason || '请求被拦截') + ',事件编号:' + (d.ref || '-'),
showCancel: false,
})
return
}
// …正常业务处理…
}
})#人机验证(401 challenge-api)
收到 401 + X-Mo-Guard: challenge-api 时,响应形如:
HTTP/1.1 401 Unauthorized
X-Mo-Guard: challenge-api
WWW-Authenticate: MoGuardPoW realm="bot", challenge="<token>", difficulty=16
Content-Type: application/json
{
"error": "bot_challenge_required",
"algorithm": "sha256-leading-zero-bits",
"challenge": "<token>",
"difficulty": 16,
"verify_url": "/__moguard/verify"
}方案 A(强烈推荐):配 API 画像,根本不被挑战
小程序解 PoW 很麻烦。正确做法是让它压根不被挑战——在控制台配 API 画像,见下一节。配好后合法请求直接放行,客户端不需要写任何挑战处理代码。
方案 B:自行解工作量证明
- 1取参数从响应体拿
challenge、difficulty、verify_url。 - 2求解从 0 开始枚举整数
answer,直到sha256(challenge + ":" + answer)的二进制前导零位数 ≥ difficulty(默认 16,平均约 6.5 万次哈希)。 - 3提交
POST {verify_url},body{"c": challenge, "answer": "<answer>"};成功返回200 {"ok":"true","pass":"<pass>"}。 - 4带上通行证之后每个请求都带
X-Moguard-Pass: <pass>头即可放行(默认有效期 30 分钟,过期后再收到 401 时重解)。
失败返回:400 格式错 / 403 解错或过期 / 409 重放(同一个 challenge 只能用一次)。
Python 兜底求解示例
import hashlib
def _leading_zero_bits(digest: bytes) -> int:
bits = 0
for byte in digest:
if byte == 0:
bits += 8
continue
for m in range(7, -1, -1): # 遇到第一个 1 bit 即停
if byte & (1 << m):
return bits
bits += 1
break
return bits
def solve_pow(challenge: str, difficulty: int) -> str:
n = 0
while True:
h = hashlib.sha256(f"{challenge}:{n}".encode()).digest()
if _leading_zero_bits(h) >= difficulty:
return str(n)
n += 1JS SDK 已内置整套 PoW 兜底与 pass 复用,JS 客户端无需自己写。纯机器对接(服务端↔服务端)通常只需签名,很少会被挑战。
#彻底免挑战:给小程序配 API 画像
控制台 → 人机验证 → API 画像 → 添加:
| 字段 | 填什么 |
|---|---|
| 路由前缀 | 小程序实际调用的 API 前缀,如 /api/ |
| Referer 域 | servicewechat.com(微信强制设置、不可伪造) |
| UA 必含 | 留空(小程序 UA 不稳定) |
| 漂移处置 | 挑战(或拦截) |
含义:该前缀下带 servicewechat.com Referer 的就是真小程序 → 直接放行,不评分、不挑战;不符合的(浏览器直调、Postman 伪造)→ 按处置挑战或拦截。同一个前缀可以配多条画像,命中任意一条即算合法客户端——比如小程序一条、原生 App 一条。
画像不等于防重放画像挡的是「换个客户端来调」,挡不住原样重放。要防抓包重放,请叠加 API 请求签名。
#给客户端的统一处理骨架
function handleWafResponse(res) {
const g = res.header['X-Mo-Guard'] || res.header['x-mo-guard'] || ''
if (!g) return false // 非 WAF 响应,交给正常业务
switch (res.statusCode) {
case 401: /* challenge-api:配了 API 画像就不会走到这;否则解 PoW */ break
case 403: /* 被拦截:展示 res.data.reason + res.data.ref */ break
case 413: /* 体积超限 */ break
case 429: /* 限流:退避后重试 */ break
case 503: /* 过载或排队:稍后重试 */ break
}
return true // 已由 WAF 处置
}- 判断是否 WAF 处置,只看
X-Mo-Guard头是否存在,别去猜状态码。 - 403 的 body 是 JSON,直接取
reason/ref展示。 - 小程序优先配 API 画像免挑战,而不是在前端解 PoW。
- 别把 401 一律当成「登录过期」——先看
X-Mo-Guard头,否则会把用户莫名其妙踢下线。
相关:API 请求签名与 SDK · 人机验证 · 访客页面。
