更新记录
1.0.0(2026-10-28)
首个版本:三段式授权码 SZL1.载荷.HMAC签名;手写 SHA-256/HMAC-SHA256(不用 javax.crypto,可与 Node crypto 逐字节比对);17 位带校验设备码(厂商/品牌/机型/设备名/ANDROID_ID/包名,去除 I/L/O/U 并用 4 位分组);有效期 + 宽限期(0-90 天,默认 7);墙钟与单调时钟双源的时间回拨检测;功能位按项开关;只读解析与设备匹配提示;凭据存应用私有目录、零权限。UTS→Kotlin 编译通过(0 error),81 项纯逻辑测试全部通过;真机未执行(测试机未连接)。
平台兼容性
uni-app
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | √ | × | × | √ | × | √ | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
uni-app x
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| × | × | × | × | × | × |
离线授权码(设备绑定 + 到期宽限)
给做私有化部署、内网运行、不能联外网的 Android App 的开发者:一套离线授权码 SDK,一台设备一个码,到期自动宽限,改系统时间立刻失效,不用架授权服务器。
授权码是 HMAC-SHA256 签名的三段式字符串,客户粘贴即可激活,改一个字符就失效。签名与 SHA-256 全部用除余运算手写实现,不引任何三方库;凭据存在应用私有目录,不需要存储权限。
为什么值得用
离线授权的难点不在加密,在四件容易做错的事:
- 设备码要能报读:客户打电话过来报设备码,字母 I / L / O / U 和数字 0 / 1 一定念错。本插件的设备码用 32 进制字母表(去掉这四个字母)+ 4 位分组 + 校验位,报错一位当场就能发现;
- 到期不能一刀切:客户续费走流程要几天,到期当天就锁死业务会直接投诉。默认 7 天宽限期(可配 0–90 天),宽限期内仍可用,返回值区分
active/grace/expired,业务侧可以只弹提醒不停业务; - 改时间是最常见的绕过手段:把手机日期调回去年,
System.currentTimeMillis()就骗过了所有基于日期的校验。这里用墙钟 + 单调时钟(elapsedRealtime)双源比对,回拨超过 5 分钟且单调时钟没跟上,直接判clock_rollback; - 客服要能核对:客户说「我的码用不了」,你要能一眼看出这张码是给哪个包名、绑了哪台设备、什么时候到期。
getLicenseInfo不验签也能解析出这些字段,并告诉你当前设备是否匹配。
快速开始
签发端(内部工具 / 服务端 / 演示页都行,只要密钥一致):
// 签发端:secret 必须 ≥16 位,且与 App 端配置的一字不差
import { issue } from '@/uni_modules/szhzy-license-guard'
issue({
v: '1', // 协议版本,固定 1
app: 'com.szhzy.mes', // 应用标识,建议用包名
dev: 'ABCDEFGHJKMNPQRS', // 绑定的设备码;留空表示不绑定设备
iat: String(Math.floor(Date.now() / 1000)),
exp: String(Math.floor(Date.now() / 1000) + 365 * 86400), // 一年;0 表示永久
g: '7', // 宽限期天数
f: 'basic,batch', // 功能位;留空视为全部
sn: 'SZHZY-0001', // 授权序号,对账用
n: '江门工厂 1 号线'
})
// => { ok: true, code: 'SUCCESS', license: 'SZL1.xxxxx.yyyyy' }
App 端(3 行接入):
import { configure, activate, check, getDeviceCode } from '@/uni_modules/szhzy-license-guard'
// 密钥 ≥16 位,且与签发端一字不差。正式项目不要在客户端硬编码,
// 由服务端下发或放进白盒/native 层;演示工程里写常量即可。
configure({ secret: getSecret() })
// 首次:把设备码发给供应商,拿到码后激活
const dev = getDeviceCode() // { deviceCode, display: 'ABCD-EFGH-JKMN-PQRS', appId }
const r = activate(inputCode) // 成功后自动落盘
if (!r.ok) uni.showToast({ title: r.message })
// 每次启动:校验一次
const s = check()
if (!s.ok) { /* s.code: EXPIRED / DEVICE_MISMATCH / CLOCK_ROLLBACK ... */ }
if (s.status === 'grace') uni.showModal({ title: '授权即将到期,请尽快续费' })
API
所有接口同步执行,返回统一信封 { ok, code, message, ... },错误用 code 表达,不抛异常。
| 接口 | 说明 |
|---|---|
configure({ secret, appId, deviceSalt }) |
必须在其它 API 之前调一次。secret 必填(≥16 位),appId 缺省取包名 |
isConfigured() |
是否已配置密钥 |
getDeviceCode() |
取设备码。返回 deviceCode(17 位)、display(4 位分组)、appId |
activate(code) |
验签 → 校验状态 → 落盘。返回体与 check() 同构 |
check() |
校验已激活的授权,每次启动调一次;成功时刷新「上次校验时间」 |
isActivated() |
本地是否存有授权码(不代表未过期) |
clear() |
清除本地授权(换机、退货、测试用) |
getLicenseInfo(code) |
只读解析,不验签。多返回 currentDevice / deviceMatched |
issue(fields) |
签发授权码。正规做法放服务端,客户端只保留校验密钥 |
checkFeature(name) |
当前授权是否包含某项功能,返回 granted |
getDefaultGraceDays() / clampGraceDays(days) |
宽限期缺省值 / 归一(0–90) |
getVersion() |
插件版本 |
返回值里的 status
| status | 含义 | 建议处理 |
|---|---|---|
active |
有效期内 | 正常放行,remainDays 可做到期提醒 |
grace |
已过期但在宽限期内 | 放行,弹续费提醒 |
expired |
已过期且超出宽限期 | 停业务 |
device_mismatch |
码绑的是别的设备 | 让客户报当前设备码 |
app_mismatch |
码不是发给这个包的 | 检查包名 |
clock_rollback |
检测到时间被回拨 | 提示校准时间 |
signature_invalid |
签名不匹配 | 确认密钥一致、码未被截断 |
format_invalid |
不是三段式授权码 | 检查是否漏粘 |
设备码与绑定策略
设备码 = SHA-256(厂商 # 品牌 # 机型 # 设备名 # ANDROID_ID # 包名),取摘要前 80 位映射到 32 进制,得 16 位主体 + 1 位加权校验位。
刻意不含 Build.FINGERPRINT 与 Build.VERSION.RELEASE:这两个值会因一次 OTA 升级而变,把它们算进指纹等于「系统一升级,已发出的授权全部失效」。
几种常见策略:
- 一设备一码(默认):
dev填客户报来的设备码,最严格; - 不绑设备:
dev留空,一张码可装多台(内部员工工具常用); - 同型号批量区分:
configure({ deviceSalt })给每台机器不同的盐,避免同型号设备撞码。
购买前必读(安全边界,如实说明)
离线校验意味着校验密钥必须在 App 里,所以这个插件的定位是:
- 能做到:防误用、防超期、防改系统时间、防把一台机器的码直接复制到另一台;
- 做不到:防逆向。拿到 APK 并脱壳反编译的人,理论上能提取密钥伪造授权。
需要更强保护的场景,两点建议:① 签发放服务端,App 只做本地校验(密钥用白盒或 native 层保护);② 关键业务逻辑配合服务端二次校验。插件 README 与市场页都按这个口径描述,不夸大。
验证状态(如实说明)
- 核心逻辑:81 项自动化测试全部通过,在 Node 环境真实执行,含 SHA-256 / HMAC-SHA256 与 Node
crypto的逐字节比对、授权码签发→篡改→验签、时间回拨、宽限期边界、设备码校验位等; - Kotlin 编译:
cli launch app-android --compile true通过,0 error; - 未执行:真机运行验证(设备指纹取值、私有目录读写、真实时间回拨)尚未在真机上跑通,测试记录里标注为「未执行」,不做真机结论。
常见错误码
| code | 原因 |
|---|---|
INVALID_SECRET |
密钥长度不足 16 位,或与签发端不一致 |
INVALID_PARAM |
configure 未传 secret |
EMPTY_VALUE |
授权码为空 |
FORMAT_INVALID |
不是 SZL1.载荷.签名 三段式 |
SIGNATURE_INVALID |
签名不匹配:密钥不同、码被改动或被截断 |
APP_MISMATCH |
app 字段与当前包名不一致 |
DEVICE_MISMATCH |
dev 字段与当前设备码不一致 |
EXPIRED |
已过期且超出宽限期 |
CLOCK_ROLLBACK |
墙钟回拨超过 5 分钟且单调时钟未跟上 |
NOT_ACTIVATED |
本地没有授权记录,需先 activate |
STORE_ERROR |
私有目录写入 / 删除失败 |
平台支持
| 平台 | 支持 |
|---|---|
| Android App(uni-app Vue3 / Vue2) | √(minSdk 23 / Android 6.0) |
| iOS / 鸿蒙 / Web / 小程序 / 快应用 | × |
许可
商用授权见 license.md。普通授权可用于商业项目;源码授权可二次开发与分发。

收藏人数:
购买源码授权版(
试用
赞赏(0)
下载 13
赞赏 0
下载 12720522
赞赏 1958
赞赏
京公网安备:11010802035340号