更新记录
1.0.0(2026-11-11)
首个版本。
- 九个 action:ping / check / consume / idem / lock / list / stats / clean / remove
- 固定窗口限流,条件更新原子扣减配额
- HMAC-SHA256 签名 + nonce 防重放,容差只认环境变量
- 幂等键 begin / complete / get / cancel 三段式
- 带 TTL 与释放凭据的分布式锁,owner 必填
- 记录集合自带索引,附管理页与分类型统计
平台兼容性
云端兼容性
| 支付宝云 | 腾讯云 | 阿里云 |
|---|---|---|
| × | √ | √ |
uniCloud 接口防护与幂等中间件
零第三方依赖的 uniCloud 云函数中间件:固定窗口限流、HMAC 签名与 nonce 防重放、幂等键去重、轻量分布式锁。一次 check 完成前置校验,附管理页与记录统计。
适合:订单 / 支付 / 表单重复提交、接口被刷、定时任务重复执行、需要给云函数加一道服务端防护、又不想引入 Redis 或其他外部服务的项目。
只依赖 Node 内置 crypto,不引任何 npm 包或扩展库。所有记录只写在使用者自己的服务空间。
特性
- 固定窗口限流:按业务键计数,并发下用条件更新
where(count <= limit-cost).update(inc(cost))原子扣减;返回remaining与retryAfter - 防重放:HMAC-SHA256 签名 + 时间戳容差 + nonce 唯一登记,拒绝过期请求与重复 nonce;容差只认环境变量,请求侧改不了;用
timingSafeEqual比较签名 - 幂等键:
begin → 业务执行 → complete三段式,重复请求直接返回首次结果;cancel允许业务失败后重试 - 轻量分布式锁:
acquire / release,带 TTL 自动过期与释放凭据,避免误释放别人的锁 - 组合校验:
check一次完成「签名防重放 → 限流 → 幂等状态查询」,返回统一的allowed / reason / result - 默认安全:
GUARD_TOKEN未配置时拒绝一切读写;令牌、签名密钥、集合名只从环境变量读,请求侧改不了 - 不留请求正文:签名只用 body 计算 HMAC,不落库;幂等结果有大小上限;列表接口不返回锁凭据与幂等结果
- 零依赖:只用
uniCloud.database()和 Node 内置crypto - 自带管理页:限流自检、记录列表、分类型统计、清理过期、强制删除记录
目录结构
uni_modules/aliang-cloud-guard/
├── package.json 插件配置(含定价与加密清单,只列 1 个 js)
├── pages_init.json 自动注册管理页
├── readme.md / changelog.md
├── uniCloud/
│ ├── cloudfunctions/aliang-cloud-guard/
│ │ ├── index.js 单文件云函数,八个分区
│ │ └── package.json
│ └── database/
│ ├── cloud_guard_records.schema.json
│ └── cloud_guard_records.index.json 三个索引
└── pages/guard/list.vue 管理页
插件内 uniCloud 不带 -aliyun / -tcb 后缀,HBuilderX 会虚拟合并到项目根目录的 uniCloud 中。
云函数刻意做成单文件:付费普通授权版的加密清单按文件配置,多文件结构一旦漏配某个 lib,那个文件会明文留在用户项目里且不会有任何报错。单文件让加密清单只有一项。
记录统一放在一个集合,kind 区分 quota / idem / nonce / lock,只需上传一个 Schema。
五分钟接入
第 0 步(必须先做):在 uniCloud 服务空间的环境变量里配置:
GUARD_TOKEN = 一个足够长的随机字符串
GUARD_SECRET = 另一个随机字符串 # 只有启用签名校验时才需要
未配置 GUARD_TOKEN 时,除 ping 外所有 action 返回 token-required。这是默认安全,不是故障。
- 从插件市场导入本插件到 uni-app 项目。
- 上传
uniCloud/database/cloud_guard_records.schema.json到服务空间。 - 右键
uniCloud/cloudfunctions/aliang-cloud-guard→ 上传部署。 pages_init.json会自动提示把「接口防护」页注册进pages.json,确认即可。- 管理页填入
GUARD_TOKEN,点「检查」,再点几次「模拟一次请求」看放行与拒绝。
环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
GUARD_TOKEN |
空(必填) | 所有 action 的令牌,只从环境变量读 |
GUARD_SECRET |
空 | 签名校验密钥;未配置时启用签名的请求返回 secret-not-configured |
GUARD_COLLECTION |
cloud_guard_records |
记录集合名,只从环境变量读 |
GUARD_DEFAULT_LIMIT |
60 | 默认窗口内上限 |
GUARD_DEFAULT_WINDOW_MS |
60000 | 默认窗口长度(毫秒) |
GUARD_TOLERANCE_MS |
300000 | 签名时间戳容差 |
GUARD_IDEM_TTL_MS |
86400000 | 幂等记录保留时长 |
GUARD_LOCK_TTL_MS |
30000 | 锁默认 TTL |
GUARD_MAX_RESULT |
8000 | 幂等结果序列化上限,超出只记长度标记 |
GUARD_CLEAN_BATCH / GUARD_CLEAN_MAX_BATCHES |
100 / 10 | 清理过期记录的分批参数 |
除令牌、密钥、集合名外,其余可用单次请求的 data.config 覆盖。
API 参考
云函数名 aliang-cloud-guard。统一返回 { errCode: 0 | 字符串, errMsg, data },失败错误码以 aliang-cloud-guard- 开头。
| action | 说明 | 主要参数 | 需要 token |
|---|---|---|---|
ping |
健康检查,返回配置状态(不含密钥) | — | 否 |
check |
组合前置校验 | key limit windowMs timestamp nonce signature body idemKey |
是 |
consume |
单独限流扣配额 | key limit windowMs cost |
是 |
idem |
幂等键操作 | step(begin/complete/get/cancel) idemKey ttlMs result |
是 |
lock |
分布式锁 | step(acquire/release) name owner(acquire 必填) ttlMs lockToken |
是 |
list |
记录列表(不含锁凭据与幂等结果) | kind page pageSize |
是 |
stats |
分类型计数与过期数 | — | 是 |
clean |
分批清理过期记录 | — | 是 |
remove |
按 _id 强制删除一条记录 |
id |
是 |
最简接入:限流
const res = await uniCloud.callFunction({
name: 'aliang-cloud-guard',
data: {
action: 'check', token: '你的令牌',
key: 'order:create:u1001', limit: 5, windowMs: 60000
}
})
const r = res.result.data
if (!r.allowed) {
return { errCode: 'rate-limited', errMsg: `请 ${Math.ceil(r.retryAfter / 1000)} 秒后重试` }
}
// 正常业务
完整接入:限流 + 幂等
const guard = (action, extra) => uniCloud.callFunction({
name: 'aliang-cloud-guard',
data: Object.assign({ action, token: TOKEN }, extra)
}).then(r => r.result)
const pre = await guard('check', { key: `pay:${userId}`, limit: 10, idemKey: orderNo })
if (!pre.data.allowed) {
if (pre.data.reason === 'idempotent-replay') return pre.data.result // 直接返回首次结果
return { errCode: 'busy', errMsg: pre.data.reason }
}
const begun = await guard('idem', { step: 'begin', idemKey: orderNo })
if (!begun.data.acquired) return { errCode: 'in-progress', errMsg: '处理中,请勿重复提交' }
try {
const result = await doBusiness()
await guard('idem', { step: 'complete', idemKey: orderNo, result })
return result
} catch (e) {
await guard('idem', { step: 'cancel', idemKey: orderNo }) // 失败后允许重试
throw e
}
防重放签名(服务端到服务端)
签名内容:timestamp + '\n' + nonce + '\n' + body,其中 body 为字符串原样、对象则 JSON.stringify、空则空串。算法 HMAC-SHA256,十六进制输出。
强烈建议把
body先JSON.stringify成字符串再传。 直接传对象时,服务端也会JSON.stringify,而 JS 对象的键顺序取决于属性插入顺序——两边键顺序不一致就算不出同一个签名,排查起来非常费时间。传字符串可以完全避免这个问题。时间戳容差由环境变量
GUARD_TOLERANCE_MS决定(默认 5 分钟),请求里的toleranceMs会被忽略。这是故意的:容差不在签名内容里,如果允许请求覆盖,截获一个签名请求后把它改大就能绕过 nonce 记录。
const crypto = require('crypto')
const timestamp = Date.now()
const nonce = crypto.randomBytes(12).toString('hex')
const body = JSON.stringify({ orderNo: 'NO1', amount: 100 })
const signature = crypto.createHmac('sha256', GUARD_SECRET)
.update(`${timestamp}\n${nonce}\n${body}`).digest('hex')
await uniCloud.callFunction({
name: 'aliang-cloud-guard',
data: {
action: 'check', token: GUARD_TOKEN, key: 'server:pay',
timestamp, nonce, signature, body
}
})
客户端到云函数的场景建议只用限流 + 幂等:把
GUARD_SECRET放进前端等于没有密钥。签名校验适用于服务端互调或受信任的后端。
分布式锁
const a = await guard('lock', { step: 'acquire', name: 'daily-settlement', owner: 'cron-1', ttlMs: 60000 })
if (!a.data.acquired) return { errCode: 'locked' }
try {
await runSettlement()
} finally {
await guard('lock', { step: 'release', name: 'daily-settlement', lockToken: a.data.lockToken })
}
acquire时owner必须传(例如pod-1、cron-daily)。它是重入判断与排障的依据,留空会直接返回invalid-param—— 早期版本允许留空,结果两个都不传owner的调用方能互相接管同一把锁。
token是保留的鉴权字段,业务参数请勿占用。 锁的释放凭据刻意叫lockToken,与鉴权令牌分开;若业务上确实需要一个叫token的参数,请放进body或改名,否则会覆盖鉴权令牌导致返回unauthorized。
错误码
| errCode | 含义 |
|---|---|
...-token-required |
服务空间没配 GUARD_TOKEN |
...-unauthorized |
令牌不对 |
...-invalid-param |
缺少 key / 未知 step / 参数不合法 |
...-invalid-action |
action 不认识 |
...-collection-missing |
Schema 没上传 |
...-nonce-store-failed |
nonce 登记时数据库报错(不是重放,按 message 排查) |
...-internal |
其他异常 |
check 被拒绝时 errCode 仍为 0(这是业务判定,不是调用失败),具体原因看 data.reason:
rate-limited / idempotent-replay / in-progress / secret-not-configured / invalid-timestamp / invalid-nonce / timestamp-expired / bad-signature / nonce-replay。
安全与隐私
GUARD_TOKEN/GUARD_SECRET/ 集合名只从环境变量读取,data.config覆盖不了。- 业务键只做长度与控制字符约束,不参与数据库命令拼接。
- 记录集合
permission全为false,前端无法直连,只能走云函数。 - 请求正文只用于计算 HMAC,不写入数据库;
nonce记录只存键与过期时间。 - 幂等结果有大小上限,超限只保留长度标记;
list不返回token与result字段。 - 管理页只应放在后台,不要把令牌写进前端代码或暴露给终端用户。
- 扣减与窗口切换都走条件更新(
where(条件).update()),并发下不会出现「同时判定窗口过期、各自把计数覆写成 1」导致的超发。 - 返回值里的
used/remaining是按「更新前读到的计数 + 本次 cost」估算的。并发时可能偏小一点,但判定用的是数据库条件更新,不会因此放行超额请求。需要精确剩余量请自己按limit与响应时间算。 - 固定窗口本身的边界突刺(窗口首尾各放行一次
limit)是算法特性,不是并发问题。防刷要求严格时把窗口改小或叠加网关限流。
容量与限制
- 所有记录共用一个集合,索引已放在
uniCloud/database/cloud_guard_records.index.json(kind+create_time、create_time、expire_time),随 Schema 一起上传即可,不用手动去控制台建。 -
一定要配定时清理,否则集合会一直长。给云函数加定时触发器,
config只填 cron 表达式:"triggers": [ { "name": "guardClean", "type": "timer", "config": "0 0 2 * * * *" } ]触发器里不要也不能填参数 —— 定时触发时云函数收到的
event只有Time/TriggerName/Type,传不了自定义 JSON。插件已经识别Type === 'Timer'并自动执行clean,且只放行动作clean这一个,不需要也不接受令牌。 - 幂等需要业务方在成功后显式调用
complete;如果业务进程在begin之后崩溃且没调cancel,该键会等到 TTL 过期才释放,请按业务容忍度设置GUARD_IDEM_TTL_MS。 - 同一个
key的limit/windowMs以首次请求为准,后续请求传新值不会生效(记录里存的是第一次的那套)。要改请换 key,或在管理页删掉该条记录。 - 一次
check最坏会有 3~4 次数据库往返,云函数超时已显式设为 10 秒。如果用的是腾讯云个人版/入门版套餐(2025-08-31 起超时固定 3 秒),高并发下建议只用consume或idem单动作,别把四个能力全串在一次check里。 - 本插件不提供验证码、设备指纹、WAF 等能力,定位是云函数侧的服务端中间件,不替代网关防护。
常见问题
Q:报 token-required / unauthorized。
A:没配 GUARD_TOKEN 或管理页填的不一致。用 action: 'ping' 确认配置状态。
Q:启用签名后报 secret-not-configured。
A:服务空间没配 GUARD_SECRET。不打算用签名就别传 signature / nonce / timestamp。
Q:报 nonce-replay 但我确定只发了一次。
A:同一个 nonce 只能用一次,重试请换新的 nonce;另外检查时间戳是否在容差内。
Q:check 返回 in-progress。
A:同一个 idemKey 已有一次 begin 还没 complete。等业务完成,或确认失败后调 cancel。
Q:限流偶尔多放行一次。 A:固定窗口在切换瞬间有极小竞态,属于已知取舍。需要严格限流请叠加网关策略。
Q:报 collection-missing。
A:DB Schema 没上传,上传后重新部署云函数。
Q:lock acquire 返回 invalid-param,提示必须传 owner。
A:owner 是必填的。它不是装饰字段,是用来判断「同一持有者重入」的;留空会让任意调用方都能接管同一把锁。
Q:传了 toleranceMs 但不生效。
A:容差只从环境变量 GUARD_TOLERANCE_MS 读。改成请求可控会让防重放失效,所以刻意不支持。
Q:idem begin 一直返回 in-progress,但我确认上次已经跑完(或早就超时了)。
A:1.0.0 修过这个问题。旧版本在记录过期但还没被 clean 删掉时会永久卡住;现在会条件接管过期记录。如果你的版本早于 1.0.0,删掉该条 idem: 记录即可。
Q:Schema 里 permission 全是 false,会不会导致云函数写不进去?
A:不会。uniCloud 的 DB Schema(含权限与字段校验)只在 JQL 下生效,本插件用的是云函数原生数据库接口,不受约束。上传 Schema 的目的是建集合、在控制台留字段说明。
版本要求
- HBuilderX 4.71+,uniCloud 阿里云 / 腾讯云服务空间
- JavaScript 普通云函数(CommonJS),需要 Node 内置
crypto(云函数环境默认提供) - 付费普通授权版按服务空间 spaceId 绑定,提供 7 天试用
授权
购买后按 DCloud 插件市场的标准授权条款使用,可用于自己的任意商业项目,项目数量不限。普通授权版不含源码,请勿反编译或二次分发。

收藏人数:
购买源码授权版(
导入插件并试用
赞赏(0)
下载 0
赞赏 1
下载 35291
赞赏 159
赞赏
京公网安备:11010802035340号