public-sdk
v2.8.0
Published
Node.js 服务端通用工具库 - 提供零依赖微信小程序 SDK、DEDS 签名防重放守卫、统一响应封装、Token 服务、通用 HTTP 客户端、常用工具方法等
Maintainers
Readme
公共服务 SDK (public-sdk)
版本: 2.7.0 | Node.js: >= 18.0.0
Node.js 服务端工具库,提供零依赖微信与抖音小程序 SDK、DEDS 动态接口签名守卫、统一响应封装、Token 登录态管理服务、原生 Http 客户端及常用通用工具方法。
特性
- 零外部依赖:基于 Node.js 18+ 原生驱动实现
Http客户端,无第三方运行时依赖。 - 双端小程序 SDK:提供微信(WX)与抖音(DY)小程序 OpenAPI 完整封装,支持凭证自愈重试、多环境自适应与实例级超时配置。
- 接口签名与防重放:提供 DEDS 动态接口签名守卫与恒定时间安全比对(
timingSafeEqualSign),支持动态密钥派生、魔数过滤与基于 Redis 的防重放中间件。 - 密码学安全:基于 Node.js
crypto模块实现安全的随机数、UUID 与 Token 签名。 - 测试覆盖:包含异步单元测试套件,覆盖核心模块与极端边界场景。
- 模块支持:支持 ES Module 与 CommonJS 双规范导出。
交流讨论
QQ群:666713758
前后端协同机制
本项目与前端 SDK public-client-sdk 提供对应的接口设计与协同机制:
| 能力维度 | 服务端 (public-sdk) | 客户端 (public-client-sdk) | 机制说明 |
| :--- | :--- | :--- | :--- |
| 接口防篡改与防重放 | SignatureGuard (Express 中间件) | DEDSSigner (加签发生器) | 动态时间片 KDF、参数排序混淆、4 字节魔数初筛与 Redis 单次消费防重放 |
| 统一数据响应与解包 | Send.success / Send.fail | UniRequestClient / Fetch / Axios | 统一响应结构与状态码,客户端自动解包 body.data 并处理登录失效拦截 |
| 跨端排他路由锁 | - | UniversalRouter (小程序/Web/App) | 排他锁控制,防止跳转未完成时的并发连点与页面栈异常 |
| 统一多端持久化缓存 | - | UniversalStorage (多端统一 Storage) | 统一多端本地存储接口,支持指定秒级/毫秒级过期时间(TTL) |
| 实时长连接消息推送 | ws-service / Redis PubSub | SocketClient (WebSocket 客户端) | 心跳探活、指数退避重连与离线消息缓冲队列 |
| 时间漂移校准 | /common/timestamp | setServerTimeOffset | 服务端时间戳校准,消除客户端设备时钟漂移 |
安装
npm install public-sdk快速开始
微信小程序 SDK
import { WX } from "public-sdk";
const wx = new WX({
appid: "your-appid",
secret: "your-secret",
botOpenid: "your-bot-openid", // 可选:保活机器人 openid,非小程序端调用安全接口时用于兜底
env_version: "develop", // develop | trial | release(默认 release);与 wechatEnv 等价
});
// 获取 access_token
const token = await wx.getAccessToken();
// 获取微信 openid
const openid = await wx.getOpenid(code);
// 获取小程序码
const qrBuffer = await wx.getUnlimitedQRCode({
page: "pages/index/index",
scene: "user=123",
width: 430,
});
// 1. 文本内容安全识别 (微信 msg_sec_check 2.0)
// 支持小程序端传入 openid,或在 Web/APP 端省略 openid(自动使用机器人兜底)
const secResult = await wx.checkSensitiveWords("待检测文本内容", {
scene: 2, // 1资料, 2评论(默认), 3论坛, 4社交日志
nickname: "用户昵称",
});
// secResult: { bool: false, message: '未包含敏感词', suggest: 'pass' }
// 也可显式传入 openid(若用户长期未活跃触发 61010,底层将自动借用机器人重试):
// await wx.checkSensitiveWords("待检测文本内容", userOpenid, { scene: 2 });
// 2. 异步多媒体内容安全识别 (微信 media_check_async 2.0,支持图片/音频)
const mediaResult = await wx.checkMediaAsync({
media_url: "https://example.com/sample.jpg",
media_type: 2, // 1 音频, 2 图片
scene: 1, // 1 资料, 2 评论, 3 论坛, 4 社交日志
// openid, // 可选,未传或用户未活跃时自动走 botOpenid 兜底
});
// mediaResult: { success: true, trace_id: 'xxxx', errcode: 0, errmsg: 'ok' }
// 获取手机号
const phoneInfo = await wx.getPhoneNumber(code);发送订阅消息(2.5.0+)
import { WX } from "public-sdk";
const wx = new WX({ appid: "xxx", secret: "xxx", env_version: "release" });
// 自动截断超长 thing* 字段,并根据配置注入 miniprogram_state
const result = await wx.sendSubscribeMessage({
touser: "openid_xxx",
templateId: "TEMPLATE_ID",
data: {
thing2: { value: "今日学习提醒" }, // 超长内容自动截断至 20 字符
thing6: { value: "请及时完成今日任务" },
time3: { value: "2026-07-21 09:00" },
},
page: "pages/index/index",
// miniprogramState: "formal", // 可选,覆盖默认配置
});
if (result.success) {
console.log("发送成功");
} else {
console.error("失败:", result.errmsg);
}抖音小程序 SDK(2.7.0+)
零外部依赖对接抖音开放平台 OpenAPI(官方服务端根域名 https://open.douyin.com),支持凭证自动缓存、40001/40002/40014 自愈重试与实例级超时配置:
import { DY } from "public-sdk";
const dy = new DY({
appid: "your-client-key",
secret: "your-client-secret",
env_version: "release", // develop | trial | release(与 douyinEnv 等价)
timeout: 8000, // 可选:自定义请求超时时长(默认 8000ms)
});
// 1. 获取 access_token(自动缓存至过期前 5 分钟)
const accessToken = await dy.getAccessToken();
// 2. 登录凭证校验 (code2session)
const session = await dy.code2Session("login_code_xxx");
// session: { openid: 'xxx', session_key: 'xxx', unionid: 'xxx', anonymous_openid: 'xxx' }
// 3. 生成小程序码 (createQRCode)
const qrBuffer = await dy.createQRCode({
path: "pages/index/index",
width: 430,
});
// 4. 内容安全文本反垃圾检测 (checkSensitiveWords)
const secResult = await dy.checkSensitiveWords("待检测文本内容");
// secResult: { bool: false, message: '未包含敏感词' }
// 5. 获取手机号 (getPhoneNumber)
const phoneInfo = await dy.getPhoneNumber("phone_code_xxx");
// 6. 发送订阅消息 (sendSubscribeMessage)
const msgResult = await dy.sendSubscribeMessage({
to_user_id: "openid_xxx",
template_id: "TEMPLATE_ID",
data: {
thing1: "系统通知", // 自动截断超长字段
time2: "2026-09-18 15:00",
},
page: "pages/index/index",
});DEDS 接口签名与防重放守卫(2.6.0+)
基于动态密钥派生、参数排序签名、魔数过滤与 Redis 防重放机制,为 Express 服务端提供接口安全校验中间件:
import express from 'express'
import { SignatureGuard } from 'public-sdk'
import redis from './utils/redis'
const app = express()
// 1. 初始化签名守卫实例
const signGuard = new SignatureGuard({
kernelSeed: 0x5f3759df, // 项目专属掩码种子
redis, // Redis 实例 (用于单次消费防重放)
timeWindowMs: 300000, // 5 分钟有效容差窗口
whitelist: ['/pay/wx/notify', '/health', '/common/timestamp']
})
// 2. 挂载中间件
app.use('/api', signGuard.middleware())原生 Http 客户端(2.6.0+)
基于 Node.js 原生驱动封装,无第三方依赖,提供简洁的请求接口与拦截器机制:
import { Http } from 'public-sdk'
// 1. 快捷请求
const res = await Http.get('https://api.example.com/users')
console.log(res.data)
// 2. 实例模式
const client = Http.create({
baseURL: 'https://api.weixin.qq.com',
timeout: 5000,
headers: { 'X-Custom-Header': 'value' }
})
// 3. 拦截器支持
client.interceptors.request.use((config) => {
config.headers['Authorization'] = 'Bearer token'
return config
})
const result = await client.post('/cgi-bin/stable_token', { appid: 'xxx', secret: 'xxx' })静态工具:截断微信 thing 字段
WX.truncateThing("很长的内容..."); // 截断到 20 字符内响应数据统一封装
import { Send } from "public-sdk";
// 自定义成功响应 code(默认为 0)
Send.successCode = 200;
// 成功响应
Send.success(data); // { code: 0, data: {}, message: 'success', timestamp: 1694524800000 }
// 错误响应
Send.fail("操作失败", 400); // { code: 400, message: '操作失败', timestamp: 1694524800000 }
// 格式化错误信息
Send.formatError(error); // { code: 500, message: '服务器错误', timestamp: 1694524800000 }
// 请求过于频繁响应
Send.tooManyRequests(); // { code: 429, message: '请求过于频繁', timestamp: 1694524800000 } 工具类 Utils
import { Utils } from "public-sdk";UUID 生成
使用 crypto.randomUUID() 生成符合 RFC 4122 标准的 UUID v4。
const uuid = Utils.uuid();
// 示例输出: "550e8400-e29b-41d4-a716-446655440000"特性:
- 使用加密安全的随机数生成器
- 保证唯一性
- 标准 UUID 格式(36 字符)
格式化方法
formatAccount(account)
格式化账号:转小写并去除首尾空格。
Utils.formatAccount(" [email protected] ");
// 返回: "[email protected]"
Utils.formatAccount(null); // 返回: ""
Utils.formatAccount(undefined); // 返回: ""
Utils.formatAccount(12345); // 返回: "12345"formatPassword(password)
格式化密码:去除首尾空格。
Utils.formatPassword(" myPassword123 ");
// 返回: "myPassword123"
Utils.formatPassword(null); // 返回: ""
Utils.formatPassword(undefined); // 返回: ""随机字符串生成
randomStr(len?, prefix?, charset?)
生成加密安全的随机字符串。
参数:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| len | number | 8 | 生成的字符串长度 |
| prefix | string | '' | 前缀 |
| charset | string | 'alphanumeric' | 字符集类型 |
字符集选项:
| 值 | 说明 | 包含字符 |
|----|------|----------|
| 'alphanumeric' | 字母数字混合(默认) | a-z, A-Z, 0-9 |
| 'alpha' | 纯字母 | a-z, A-Z |
| 'numeric' | 纯数字 | 0-9 |
示例:
// 默认:8 位字母数字混合
Utils.randomStr();
// 示例: "aB3xK9mP"
// 自定义长度
Utils.randomStr(16);
// 示例: "qR7tY2wE8pL1kO3j"
// 带前缀
Utils.randomStr(8, "token_");
// 示例: "token_xKmPqR"
// 纯字母
Utils.randomStr(20, "", "alpha");
// 示例: "aBcDeFgHiJkLmNoPqRsT"
// 纯数字
Utils.randomStr(6, "", "numeric");
// 示例: "385921"特性:
- 使用
crypto.randomBytes()生成密码学安全的随机数 - 支持自定义字符集
- 适用于生成 Token、验证码、临时凭证等场景
校验方法
isEmail(email)
校验邮箱格式是否符合标准。
Utils.isEmail("[email protected]"); // true
Utils.isEmail("[email protected]"); // true
Utils.isEmail("invalid-email"); // false
Utils.isEmail(null); // false
Utils.isEmail(""); // false支持格式:
- 标准格式:
[email protected] - 子域名:
[email protected] - 特殊字符:
[email protected] - 多级顶级域:
[email protected]
isPhone(phone, strict?)
校验手机号格式。
参数:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| phone | string \| number | - | 手机号 |
| strict | boolean | true | 是否使用严格模式 |
严格模式(默认):
- 支持中国大陆手机号
- 规则:11 位数字,1 开头,第二位为 3-9
Utils.isPhone("13812345678"); // true
Utils.isPhone("19900001111"); // true
Utils.isPhone("12345678901"); // false (号段无效)
Utils.isPhone("1381234567"); // false (位数不足)
Utils.isPhone(13812345678); // true (支持数字类型)宽松模式:
- 支持国际手机号格式
- 规则:可选
+前缀,7-15 位数字
Utils.isPhone("+8613812345678", false); // true
Utils.isPhone("+11234567890", false); // true
Utils.isPhone("8613812345678", false); // trueisUrl(url)
校验 URL 格式是否有效。
Utils.isUrl("https://www.example.com"); // true
Utils.isUrl("http://test.org/path?query=1"); // true
Utils.isUrl("ftp://files.server.com"); // true
Utils.isUrl("not-a-url"); // false
Utils.isUrl("http://"); // false
Utils.isUrl(null); // false特性:
- 使用原生
URL构造函数验证 - 支持 http、https、ftp 等常见协议
- 正确识别各类型合法 URL 结构
数据转换
list2tree(items, prop?)
将扁平列表转换为多级树形结构。
参数:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| items | Array | - | 扁平数据列表 |
| prop | Object | {} | 属性名映射配置 |
默认配置:
{
id: 'id', // ID 字段名
parent_id: 'parent_id', // 父级 ID 字段名
children: 'children', // 子节点字段名
label: 'label', // 显示文本字段名
value: 'value' // 值字段名
}基本用法:
const items = [
{ id: 1, name: "根节点1", parent_id: 0 },
{ id: 2, name: "子节点1-1", parent_id: 1 },
{ id: 3, name: "子节点1-2", parent_id: 1 },
{ id: 4, name: "根节点2", parent_id: 0 },
{ id: 5, name: "子节点2-1", parent_id: 4 },
];
const tree = Utils.list2tree(items, { label: "name" });
// 输出:
[
{
id: 1,
name: "根节点1",
label: "根节点1",
value: 1,
children: [
{
id: 2,
name: "子节点1-1",
label: "子节点1-1",
value: 2,
parent_name: "根节点1",
children: []
},
// ...
]
},
// ...
]自定义属性名:
const items = [
{ itemId: 1, title: "根节点", parentId: null },
{ itemId: 2, title: "子节点", parentId: 1 },
];
const tree = Utils.list2tree(items, {
id: "itemId",
label: "title",
value: "itemId",
parent_id: "parentId"
});Sequelize 兼容支持:
自动识别 Sequelize 模型实例并提取 dataValues:
const sequelizeItems = await Model.findAll();
const tree = Utils.list2tree(sequelizeItems);特性:
- 支持多级嵌套解析
- 自动跳过重复 ID 的项并输出警告
- 兼容父级 ID 为 null/undefined 的根节点判定
- 自动维护
parent_name字段 - 输入空数组或非数组时安全返回空数组
路由匹配
isInRoutes(route, routes)
检查指定的 API 路由是否在匹配列表中(支持路由路径参数)。
参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| route | { method: string, path: string } | 待匹配的路由 |
| routes | Array<{ method: string, path: string }> | 路由匹配列表 |
示例:
const routes = [
{ method: "GET", path: "/api/users" },
{ method: "GET", path: "/api/users/:id" },
{ method: "POST", path: "/api/users" },
];
// 精确匹配
Utils.isInRoutes({ method: "GET", path: "/api/users" }, routes);
// true
// 路径参数匹配
Utils.isInRoutes({ method: "GET", path: "/api/users/123" }, routes);
// true
// HTTP 方法忽略大小写
Utils.isInRoutes({ method: "get", path: "/api/users" }, routes);
// true
// 不匹配
Utils.isInRoutes({ method: "POST", path: "/api/posts" }, routes);
// false特性:
- 支持动态路径参数(如
/users/:id) - HTTP 请求方法忽略大小写
- 请求路径忽略大小写
- 安全处理 null 或非法输入
IP 地址获取
clientIp(req)
从 HTTP 请求对象中获取客户端真实 IP 地址。
参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| req | Object | Express / Koa 等 HTTP 请求对象 |
IP 获取优先级:
X-Forwarded-For头(取首个有效 IP)X-Real-IP头req.ipreq.connection.remoteAddressreq.socket.remoteAddress
示例:
// Express 路由中使用
app.get("/api/data", (req, res) => {
const ip = Utils.clientIp(req);
console.log(ip); // "192.168.1.1"
});特性:
- 兼容代理服务器的多级转发场景
- 自动移除 IPv6 映射前缀 (
::ffff:) - 将 IPv6 本地回环 (
::1) 规范化为127.0.0.1 - 支持逗号分隔的多 IP 解析
- 兼容数组类型的 HTTP header
- 空请求对象返回空字符串
典型应用配置:
// Nginx 代理配置示例:
// proxy_set_header X-Real-IP $remote_address;
// proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
app.use((req, res, next) => {
req.clientIp = Utils.clientIp(req);
next();
});字符串截断(2.5.0+)
通用字符串长度截断工具,适用于微信 thing 字段等长度受限场景。
Utils.truncate("很长的内容...", 20); // 截断到 20 字符,超长添加 '...'
Utils.truncate("很长的内容...", 10, '…'); // 自定义省略符
Utils.truncate(null); // 返回空字符串(安全不抛错)Token 服务(2.5.0+)
通用的 Token 登录态管理服务,支持单设备互斥、多端登录与滑动续期。
import { TokenService } from "public-sdk";
// 注入 Redis 客户端(兼容 ioredis / node-redis / 任意实现 get/set/del/expire 的对象)
const token = new TokenService(redis, {
prefix: "my-app", // 键名前缀,默认 'token'
expire: 86400, // 过期时间(秒),默认 24 小时
singleDevice: true, // 单设备互斥,默认 true
});
// 保存登录态
const t = await token.save(userId, { userId, role: "user" });
// 校验 Token(验证失败抛出 Error,statusCode = 401)
const info = await token.verify(t);
// 滑动续期
await token.refresh(t);
// 注销
await token.revoke(t);
// 强制下线(按 userId 维度)
await token.revokeByUserId(userId);特性:
- 采用三键映射设计(token-account / account-token / account-info),保障快速校验
- 单设备互斥模式:新 Token 自动使旧 Token 失效
- 多设备模式(
singleDevice: false):按 Token 维度隔离存储用户信息 - 兼容多种 Redis 客户端(ioredis、node-redis 或 Mock 对象)
- 鉴权异常附带
statusCode = 401,便于中间件统一捕获
测试
运行测试套件:
npm test
# 或
node test.js测试覆盖:
- UUID 生成(3 个用例)
- 账号格式化方法(4 个用例)
- 密码格式化方法(3 个用例)
- 随机字符串(6 个用例)
- 邮箱校验(3 个用例)
- 手机号校验(5 个用例)
- URL 校验(3 个用例)
- 列表转树形结构(7 个用例)
- 路由匹配(5 个用例)
- 客户端 IP 获取(8 个用例)
- 字符串截断(5 个用例)
- WX 静态工具(3 个用例)
- WX 构造与 wechatEnv(6 个用例)
- TokenService(6 个用例)
- Http 客户端(2 个用例)
- DEDS 签名与防重放守卫(3 个用例)
- DY 抖音小程序 SDK(6 个用例)
- 深度优化与健壮性专项(8 个用例)
总计:86 个测试用例全部通过
更新日志
[2.7.0] - 2026-09-18
新增
- DY 抖音小程序 SDK:新增
DY模块(./utils/applet/dy),提供凭证获取(含自动重试与缓存)、登录校验(code2Session)、小程序码生成、文本内容安全检测、手机号解密与订阅消息推送等完整能力。 - 实例级超时配置:
WX与DY构造函数新增timeout参数,支持为各 SDK 实例独立配置 HTTP 请求超时时长。 - 恒定时间签名比对:导出
timingSafeEqualSign工具函数,防范针对 API 签名的时序侧信道攻击(Timing Attack)。
修复与增强
- Send.formatError 循环引用防御:增强错误格式化鲁棒性,针对包含循环引用的复杂对象增加安全序列化保护,杜绝二次崩溃。
- list2tree 拓扑死循环防御:新增多节点相互循环引用拓扑检测,遇脏数据环路时自动阻断挂载并降级为根节点,避免内存溢出与 JSON 序列化崩溃。
- truncate 边界截取防护:修复在
max <= suffix.length极端场景下截断后字符串长度超出上限的问题。 - TokenService 多设备全量清理:多设备登录模式下维护 Redis 活跃 Token 集合,支持通过
revokeByUserId一键注销该用户全量在线设备。 - 测试框架异步调度升级:重构
test.js为支持async/await的异步队列调度执行器,消除异步断言提前通过隐患。 - 环境要求规范:修正 Node.js 运行引擎要求为
>=18.0.0,与底层原生fetch与crypto原生特性对齐。
[2.6.0] - 2026-08-21
新增
- Http 客户端:基于 Node.js 原生驱动实现,支持拦截器、实例创建(
Http.create)、超时中断与 Buffer 数据接收。 - DEDS 接口签名守卫 (SignatureGuard):实现基于动态密钥派生、魔数初筛与 Redis 单次防重放的 Express 中间件。
优化
- 零依赖改造:清空 package.json 中的 dependencies 依赖项,改用内置模块实现。
- 微信 SDK 驱动重构:
WX模块切换至原生Http驱动,并完善 Buffer 处理机制。 - 配置规范统一:移除历史环境变量
WECHAT_MINIPROGRAM_STATE,统一支持构造函数传参(env_version/wechatEnv)。 - 测试用例补充:测试用例扩充至 72 个。
[2.5.1] - 2026-08-05
修复
- WX.getAccessToken 改用稳定版接口
/cgi-bin/stable_token:采用稳定版接口替代旧接口,默认force_refresh=false,消除多进程/多实例并发刷新相互挤掉 token (40001) 的问题。 - 新增
_withToken凭证失效自动重试:微信接口返回40001/42001(access_token 失效)时,自动以force_refresh=true刷新并重试一次。 - 修复
getUnlimitedQRCode错误响应识别:增加对 Node 环境下返回 Buffer 的判定与解析,异常时提取errcode/errmsg抛出。 - 提升
checkSensitiveWords容错:增加errcode状态校验与空详情数组兜底。
优化
- 缓存字段语义调整:
expires_in调整为绝对时间戳expiresAt,过期判定采用Date.now() >= expiresAt。 - 文档与类型声明同步更新。
重要说明
- 本版本保持向后兼容,API 接口无破坏性变更。
- 多进程或集群部署推荐保持默认
force_refresh=false。
[2.5.0] - 2026-07-21
新增
- WX.sendSubscribeMessage():支持发送订阅消息,自动处理字段长度限制与环境参数。
- TokenService:通用 Token 管理服务,支持单设备互斥、多设备登录与滑动续期。
- Utils.truncate():通用字符串截断工具。
- WX.truncateThing():微信 thing 字段专用截断方法。
修复
- 修复多设备登录场景下
account-info覆盖问题(非单设备模式下按 Token 维度隔离)。
优化
- package.json 新增
./utils/Token导出路径。 - 完善订阅消息与 TokenService 文档示例。
- 测试用例扩充至 65 个。
[2.4.0] - 2026-05-28
新增
- 随机数生成改用 Node.js
crypto模块。 randomStr()支持charset自定义字符集。isPhone()支持strict宽松模式校验国际号码。
修复
- 修复
uuid()进制转换问题。 - 修复
list2tree()重复 ID 节点重复添加问题。 - 修复
isInRoutes()传入 null 时的空指针异常。
优化
- 优化邮箱与手机号校验正则。
- URL 校验改用原生
URL构造函数。 - 完善
list2tree()边界处理。 Send统一响应格式补充timestamp字段。
测试
- 新增单元测试套件,覆盖核心公共方法。
[2.3.0] - 2025-12
文档
- 重写 README.md,补充 API 接口与使用示例。
配置
- 完善 package.json 元数据配置与 LICENSE 声明。
新增
isPhone()支持国际号码格式校验。
修复
- 修复
uuid()算法缺陷。 - 修复
list2tree()树形转换重复节点问题。 - 修复
isInRoutes()边界解构异常。
优化
- 优化参数校验与异常处理机制。
[1.0.1] - 2025-09
- 初始版本发布。
许可证
参与贡献
欢迎提交 Issue 与 Pull Request。
问题反馈
如遇问题或有功能建议,请提交至 Issue 列表。
