小程序接口安全攻防:签名、防重放、防刷与数据加密实战
小程序代码可以被反编译、请求可以被抓包、接口可以被脚本刷——这是做接口安全设计的前提假设。这篇按"攻击者视角"过一遍小程序接口的常见攻防手段,从请求签名到风控埋点,给出一套可落地的纵深防御方案。
一、先明确攻击面
拿到一个小程序包(unwxapkg 解包是公开技术),攻击者能做什么:
- 读源码:所有前端逻辑可见,包括写死的密钥、接口地址、加密逻辑
- 改请求:Charles/mitmproxy 抓包改包,绕过前端校验直接打接口
- 重放请求:把合法请求原样重发(领券接口重发 100 次)
- 脚本刷量:用抓到的协议写脚本,批量注册、批量抢券、爬数据
核心结论:任何只放在前端的安全逻辑都等于明文。前端安全措施的定位是"提高破解成本",真正的防线必须在服务端。
二、第一层:请求签名
2.1 签名方案
sign = HMAC-SHA256(secret, method + path + timestamp + nonce + body摘要)问题来了:secret 放哪?放前端代码里会被反编译提取。务实的做法是不追求绝对保密,而是动态化:
// 登录后,服务端按设备指纹+时间窗口下发动态签名密钥
// 前端存内存(不落盘),每次请求计算签名
const crypto = require('./crypto') // 轻量 hmac 实现
function signRequest(method, path, body) {
const timestamp = Date.now().toString()
const nonce = randomStr(16)
const message = [method, path, timestamp, nonce, hashBody(body)].join('\n')
const sign = crypto.hmacSha256(getSessionSecret(), message)
return { timestamp, nonce, sign }
}2.2 服务端验签
// Node.js 中间件
async function verifySign(req, res, next) {
const { 'x-timestamp': ts, 'x-nonce': nonce, 'x-sign': sign } = req.headers
// 1. 时间窗口校验(±5分钟),防长期重放
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
return res.status(401).json({ code: 'SIGN_EXPIRED' })
}
// 2. nonce 一次性校验:redis SETNX + TTL
const isNew = await redis.set(`nonce:${nonce}`, 1, 'NX', 'EX', 300)
if (!isNew) {
return res.status(401).json({ code: 'REPLAY_ATTACK' })
}
// 3. 按用户会话密钥重算签名比对
const expected = hmacSha256(req.sessionSecret, buildMessage(req))
if (expected !== sign) {
return res.status(401).json({ code: 'SIGN_INVALID' })
}
next()
}密钥下发设计:签名密钥在登录时由服务端生成(绑定 openid + 设备 + 过期时间),存 Redis。密钥本身不下发到可持久化的存储,前端只存内存变量,冷启动重新走登录换取。这样即使被反编译拿到加密算法,拿不到实时密钥。
三、第二层:防重放与防刷
3.1 时间戳 + nonce 双保险
只校验时间戳:5 分钟窗口内照样可以重放。只校验 nonce:nonce 表无限膨胀。两者结合:时间窗口内的 nonce 记 Redis(TTL = 窗口长度),窗口外的直接拒绝。
3.2 幂等令牌(业务防重)
关键操作(下单、领券、提现)加幂等令牌:
进入下单页 → POST /idempotent-token → 服务端生成 token 存 Redis(5分钟有效,一次性)
提交订单 → 携带 token → 服务端 GETDEL 原子消费
→ 消费成功:处理业务
→ 消费失败(token 不存在/已用):拒绝,返回"请勿重复提交"const token = await redis.getdel(`idem:${clientToken}`)
if (!token) return res.status(409).json({ code: 'DUPLICATE' })
// token 消费成功才继续业务比签名更硬:幂等令牌在业务层防重放,签名防不了"同一用户不同 nonce 重放同一业务"。
3.3 频率限制
多维度限流(Redis + 令牌桶/滑动窗口):
async function rateLimit(req) {
const rules = [
{ key: `rl:ip:${req.ip}`, limit: 100, window: 60 }, // IP 维度
{ key: `rl:uid:${req.openid}`, limit: 30, window: 60 }, // 用户维度
{ key: `rl:uid-api:${req.openid}:${req.path}`, limit: 5, window: 60 } // 用户+接口
]
for (const r of rules) {
const cnt = await redis.incr(r.key)
if (cnt === 1) await redis.expire(r.key, r.window)
if (cnt > r.limit) return { blocked: true, rule: r.key }
}
return { blocked: false }
}梯度惩罚:超限不直接封禁(误伤共享 IP),而是返回 429 + 递增惩罚时长(30s → 5min → 1h),并在风控后台打标。
3.4 行为验证
高频价值接口(抢券、秒杀)前置行为验证:
- 微信提供的
wx.checkSession只是登录态检查,不是人机验证 - 自建方案:滑块/点选验证码,配合行为埋点(操作时长、滑动轨迹的贝塞尔曲线特征、页面停留时间)综合评分
- 简单有效的一招:接口必须携带页面埋点产生的行为链 ID,脚本直调接口没有行为链,直接拒绝
四、第三层:数据加密传输
HTTPS 之外再加一层应用层加密,主要防抓包工具明文查看(HTTPS 抓包在用户信任证书的前提下是明文的):
// 前端:AES 加密业务数据
const key = getSessionKey() // 登录协商的会话密钥
const encrypted = aesEncrypt(JSON.stringify(data), key)
wx.request({
url: api,
data: { payload: encrypted }, // 密文外层再走签名
// ...
})
// 后端:对称解密
const data = JSON.parse(aesDecrypt(req.body.payload, sessionKey))会话密钥协商:登录成功后服务端生成 32 字节随机密钥,用 openid 绑定存 Redis,返回给前端存内存。抓包者拿到的是密文+无法解密的密钥。
加密的边界认知:这层防的是"偷看数据",防不了"拿会话重放"——重放要靠前面的签名+nonce+幂等令牌。四层各司其职。
五、第四层:服务端业务兜底
前面所有手段都可能被绕过(逆向能力强的大厂黑产团队有的是),服务端业务校验是最后防线:
- 价格不信任前端:下单只传商品 ID 和数量,价格从数据库取——这条老规矩依然有人犯错
- 库存原子扣减:
UPDATE stock SET n = n - 1 WHERE id = ? AND n > 0,affected rows = 0 就售罄 - 风控规则引擎:单用户单日领券上限、同设备指纹多账号识别、异常时段集中下单告警
- 敏感操作二次核验:提现、改手机号等操作要求短信/生物识别二次验证
六、纵深防御全景图
┌─ L1 传输层:HTTPS + 证书锁定(防被动窃听)
├─ L2 签名层:动态密钥 + timestamp + nonce(防篡改、防重放)
├─ L3 业务层:幂等令牌 + 限流 + 行为验证(防刷量)
├─ L4 数据层:应用层 AES(防抓包明文)
└─ L5 兜底层:服务端价格/库存/权限校验 + 风控(防一切绕过)攻击者要攻破你的系统,需要逐层突破;而每加一层,攻击成本都是指数级上升。绝大多数刷单攻击在 L2/L3 就被挡掉了,坚持打到 L5 的对手,靠风控人工处置。
七、避坑清单
| 坑 | 现象 | 解法 |
|---|---|---|
| secret 硬编码在前端 | 反编译直接提取 | 登录后动态下发,内存持有 |
| 只校验时间戳不校验 nonce | 窗口期内重放成功 | 双校验 + Redis SETNX |
| 前端传价格/库存 | 改包 0.01 元下单 | 服务端查库取价 |
| 限流只按 IP | 公司/校园网集体误伤 | IP+用户+设备指纹多维 |
| 加密了没验签 | 抓包改密文照样打 | 加密和签名是两件事,都要 |
| 幂等令牌可复用 | 并发重复提交 | GETDEL 原子消费 |
| 429 直接封 IP | 黑产换 IP 池绕过 | 梯度惩罚 + 设备指纹识别 |
写在最后
接口安全没有银弹,本质是一场成本博弈:你的防御成本要让攻击者的破解成本高于收益。普通业务做好"动态密钥签名 + 防重放 + 服务端兜底校验"三板斧,就能挡掉 99% 的脚本小子;剩余 1% 的职业黑产,交给风控规则和人工运营。别追求理论上的绝对安全,追求性价比足够高的纵深防御。
评论 (0)