软件卡密授权怎么做:时间卡、次数卡、一卡一机的选型与接入指南
给软件加授权这件事,绝大多数人第一次做都会踩同一批坑:卡密类型一开始就选错,导致后期加续费要改数据结构;机器码绑定做得太死,用户换台电脑就得找你人工解绑;卡密明文躺在数据库里,一次拖库全泄露。这篇按「选型 → 绑定策略 → 接口设计 → 安全落地」四段讲清,能直接照着做。
先想清楚:你要防的是谁
授权系统不是防破解的银弹。它真正能解决的是三件事:把卡密发出去、知道谁在用、能远程停掉。把这三件事做好,商业上已经足够;指望客户端里一段校验代码挡住逆向,是把力气用错了地方。
所以设计的重心应该放在服务端:卡密状态、到期时间、剩余次数、绑定关系全部以服务端为准,客户端每次启动或定时向服务端问一次「我还能用吗」。客户端被打补丁,最多是这一台机器绕过;服务端一封,全网失效。
卡密类型怎么选:时间卡 / 次数卡 / 混合卡
三条路的适用场景差别很大,选错了后面很难补。
| 类型 | 怎么算 | 适合什么 | 容易踩的坑 |
|---|---|---|---|
| 时间卡 | 从激活那一刻起,加 N 天/月/年,到期失效 | 订阅制软件、年费工具、按月收费的服务 | 「激活时开始计时」还是「生成时开始计时」必须一开始定死;生成时计时会让囤卡的用户吃亏 |
| 次数卡 | 每次核销扣 1,扣到 0 停 | 按次计费:导出、跑批、生成图片、调用模型 | 并发核销会超扣,扣减必须是服务端原子操作,不能客户端报数 |
| 混合卡 | 有效期与次数同时存在,任一先到就拦 | 「一年内可用 500 次」这类套餐 | 续费时两个维度要一起加,只加一个会造成「续了钱却用不了」 |
关键决策:计时起点
时间卡有两种起点,选错会直接影响退款率:
- 生成即计时:发卡商友好,用户囤卡会亏。适合线下即时交付。
- 首次激活计时(推荐):用户拿到卡,第一次在软件里用才开始算。体验好,也符合大多数人的直觉。
无论选哪种,剩余时长要能实时算出来给用户看,而不是只在数据库里存一个到期时间戳就完事——用户最常问的问题就是「我这张卡还剩几天」。
要不要一开始就做「产品套餐」
如果你打算卖不止一种规格(月卡 / 季卡 / 年卡 / 500 次卡),建议第一天就把「套餐」这层抽出来:套餐定义时长、次数、解绑策略、限频,卡密只是套餐的一个实例。这样后面加规格、调价格、按套餐批量发卡都不用改表结构。等到有几百张卡再补这层,迁移会很痛。
一卡一机与机器码绑定:三种策略的取舍
「一卡一机」听着简单,实际是整套系统里最容易引起客服工单的地方。三种常见策略:
| 策略 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 不绑定 | 卡密任何机器都能用 | 零客服、实现最简单 | 一张卡能被无限台机器共用 |
| 首绑锁定 | 第一次验卡时把机器码写进卡,之后必须一致 | 挡住大部分共用 | 用户换机/重装系统就得解绑 |
| 首绑 + 自助解绑 | 锁定,但允许用户自己解绑 N 次,两次之间设冷却 | 既挡住共用,又不产生工单 | 实现要多一套解绑状态机 |
机器码取什么
别只用 MAC 地址——虚拟机会变、USB 网卡会换。稳妥做法是多因子拼一个指纹再哈希:
machine_code = sha256(
cpu_id + motherboard_serial + system_volume_serial + machine_guid
).slice(0, 32)
拼完只上传哈希,不上传原始硬件信息,省得被质疑收集隐私。哈希后长度固定,也方便在日志里做脱敏展示。
换机解绑:自助解绑的三道闸
如果做自助解绑,这三个参数必须可配,缺一个都会出问题:
- 解绑次数上限:一张卡总共能解绑几次。不设上限等于没绑定。
- 冷却时间:两次解绑之间至少隔多久。挡住「解绑 → 给朋友 → 再解绑」的循环共用。
- 是否需要原设备确认:设备还在手上时,让原设备点确认;设备已经坏了,走后台人工。
解绑动作一定要写审计日志:谁、什么时候、哪张卡、从哪个机器码解到哪个。这类日志平时没人看,一旦有纠纷就是唯一凭据。
验卡接口怎么设计
一个够用的验卡接口只要三件事:认身份(api_key)、认卡(card_key)、认机器(machine_code)。
POST /api/v1/open/use_card
Content-Type: application/json
{
"api_key": "YOUR_API_KEY",
"card_key": "月咒-1234-5678-90AB",
"machine_code": "8f3c1a9e5b7d2046"
}
← 200 OK
{
"success": true,
"code": "OK",
"remain_days": 27,
"remain_count": 460,
"expires_at": "2026-11-25 14:03:00"
}
失败要把原因说清楚
别只返回 false。客户端要能区分「卡号错了」和「卡过期了」,前者提示用户检查输入,后者提示去续费。一套够用的原因码大概长这样:
CARD_NOT_FOUND卡号不存在CARD_EXPIRED已到期CARD_DISABLED被停用/作废NO_REMAIN次数用完DEVICE_MISMATCH机器码不匹配UNBIND_LIMIT解绑次数已用完TOO_FREQUENT请求太频繁(限流)
幂等与并发
次数卡的扣减要防重复提交:客户端网络抖动重发一次,不能扣两次。做法是让客户端带一个 request_id,服务端在短时间窗口内做去重;或者干脆把「扣减」放在一个独立的确认接口,验卡接口只做查询。
卡密怎么存、怎么传才不裸奔
这一节是被低估最多的地方。见过太多系统卡密直接明文存数据库,一次备份泄露,全部卡密被薅光。
落库:加密存,索引留哈希
- 卡密本体用对称加密(AES-256-GCM)存,密钥不放数据库里,放环境变量或 KMS。
- 为了查询,额外存一列 HMAC 哈希,用哈希列做等值查询,不用解密全表。
- 再存一列掩码(如
YZ**************66),后台列表直接展示掩码,避免为了显示而解密。
-- 表里存这三列,不存明文
card_cipher TEXT -- v1.salt.iv.tag.cipher(AES-256-GCM)
card_hash TEXT -- HMAC-SHA256(pepper, card)
card_mask TEXT -- YZ**************66
传输:要不要上参数加密
HTTPS 已经挡住了链路窃听,参数加密解决的是另一个问题:客户端被逆向、密钥被提取,攻击者直接构造请求刷你接口。这时候把请求体做一层 AES 加密(密钥由服务端下发并定期轮换),配合时间戳与随机串防重放,能显著抬高成本。
其它几条便宜的加固
- 开放接口按 API Key 分族限流,超了直接返回 429 并带
Retry-After。 - 后台登录上 TOTP 双因素,别只靠密码。
- 后台权限做细:谁能看卡密、谁能删卡、谁能看日志,分开授权,服务端逐路由校验。
- 审计日志里不要记完整卡密,记掩码就够。
续费要不要一开始就留好
要。用户到期当天最想干的事就是续费,如果这时候你只能「再发一张新卡」,等于把用户推去重新激活、重新绑定,体验很差。
续费的设计要点:
- 时间卡续费时,起点取
max(now, 原到期时间),避免用户提前续费反而亏掉剩余天数。 - 次数卡续费把次数加到剩余次数上,同时把总次数一起加,否则「已用 / 总次数」会算错。
- 记录续费次数与最后一次续费时间,客服看这两个字段就能判断是不是老客户。
上线前检查清单
- 计时起点定了吗?(生成即计时 / 首次激活计时)
- 卡密是加密落库的吗?查询走哈希列还是全表解密?
- 次数扣减是服务端原子操作吗?重复提交会扣两次吗?
- 机器码变了有自助解绑路径吗?解绑次数与冷却设了吗?
- 失败原因码齐了吗?客户端能区分「过期」和「输错」吗?
- 开放接口有按 Key 限流吗?
- 后台登录有双因素吗?权限是逐路由校验还是只在前端藏按钮?
- 审计日志里有没有漏出完整卡密?
- 续费加时长时,起点是
max(now, 到期时间)吗? - 有没有一个「还剩几天到期」的视图,能提前一周看到快到期的卡?
月咒授权系统把这些都做完了:卡密加密落库、一卡一机、自助解绑、续费、到期预警、开放接口与参数加密。
进入控制台 →