小程序接口安全攻防:签名、防重放、防刷与数据加密实战

小程序接口安全攻防:签名、防重放、防刷与数据加密实战

admin
2026-06-13 / 0 评论 / 243 阅读

小程序接口安全攻防:签名、防重放、防刷与数据加密实战

小程序代码可以被反编译、请求可以被抓包、接口可以被脚本刷——这是做接口安全设计的前提假设。这篇按"攻击者视角"过一遍小程序接口的常见攻防手段,从请求签名到风控埋点,给出一套可落地的纵深防御方案。

一、先明确攻击面

拿到一个小程序包(unwxapkg 解包是公开技术),攻击者能做什么:

  1. 读源码:所有前端逻辑可见,包括写死的密钥、接口地址、加密逻辑
  2. 改请求:Charles/mitmproxy 抓包改包,绕过前端校验直接打接口
  3. 重放请求:把合法请求原样重发(领券接口重发 100 次)
  4. 脚本刷量:用抓到的协议写脚本,批量注册、批量抢券、爬数据

核心结论:任何只放在前端的安全逻辑都等于明文。前端安全措施的定位是"提高破解成本",真正的防线必须在服务端。

二、第一层:请求签名

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+幂等令牌。四层各司其职。

五、第四层:服务端业务兜底

前面所有手段都可能被绕过(逆向能力强的大厂黑产团队有的是),服务端业务校验是最后防线:

  1. 价格不信任前端:下单只传商品 ID 和数量,价格从数据库取——这条老规矩依然有人犯错
  2. 库存原子扣减UPDATE stock SET n = n - 1 WHERE id = ? AND n > 0,affected rows = 0 就售罄
  3. 风控规则引擎:单用户单日领券上限、同设备指纹多账号识别、异常时段集中下单告警
  4. 敏感操作二次核验:提现、改手机号等操作要求短信/生物识别二次验证

六、纵深防御全景图

┌─ 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

评论 (0)

取消
0:00