概述
在 ToB 软件交付或一些收费的桌面软件(如 IDEA、Navicat)中,我们经常会接触到 License(许可证)。
通俗的讲,License 就是软件的“驾驶证”。当你购买了软件,厂商发给你一个 License 文件(或者激活码),软件读取这个文件,验证你是否有权使用,以及可以使用多久、可以使用哪些功能。
虽然 SaaS 模式(账号登录验证)越来越流行,但在很多私有化部署、离线环境或对数据隐私要求极高的场景下,License 依然是目前最主流的授权保护方式。
今天我们就来聊聊 License 是怎么设计出来的,以及它是如何一步步升级来对抗破解者的。
License 的核心能力与要求
设计一个合格的 License 系统,通常需要满足以下几个核心诉求:
- 完整性(防篡改):用户不能随意修改 License 里的内容(比如把过期时间从 2025 年改成 2099 年)。
- 唯一性(防复制/机器绑定):A 客户购买的 License,不能直接拷贝给 B 客户使用。
- 时效性(过期控制):能够精确控制授权的开始和结束时间。
- 功能控制(按需授权):可以限制用户只能使用标准版功能,或者解锁高级版功能。
为了实现这些目标,我们的 License 实现方案经历了几个版本的迭代。
迭代一:明文配置文件(裸奔版)
这是最原始的思路。我们定义一个 JSON 文件作为 License,里面记录授权信息。
License 文件内容:
{
"company": "字节跳动",
"expireTime": "2025-12-31",
"modules": ["user", "order"]
}
验证逻辑:
程序读取文件,解析 JSON,判断 CurrentTime > expireTime,如果是则停止服务。
存在的问题:
这就好比把家里的钥匙放在大门口的地毯下。任何懂一点电脑的用户,用记事本打开这个文件,把 2025 改成 2999,破解就完成了。
结论: 毫无安全性,只能防君子不能防小人。
迭代二:对称加密(虽然加密了,但钥匙在锁上)
为了防止用户修改文件,我们决定对文件内容进行加密。使用 AES 或 DES 等对称加密算法。
生成逻辑(厂商端):
- 准备一个密钥
SecretKey = "123456"。 - 使用该密钥将 JSON 内容加密成乱码字符串。
License 文件内容:
U2FsdGVkX1+... (一串看不懂的密文)
验证逻辑(客户端):
- 代码中硬编码写死密钥
SecretKey = "123456"。 - 软件启动时,用密钥解密 License 文件。
- 解密成功则校验时间,解密失败则认为 License 非法。
存在的问题:
对称加密的核心缺陷在于:加密和解密用的是同一个密钥。
为了让软件能运行,你必须把密钥写在代码里。破解者只需要反编译你的 Jar 包或 exe 文件,全局搜索一下 "AES" 或者 "Key" 相关的字符串,就能拿到密钥。拿到密钥后,他就可以自己生成任意有效期的 License 了。
结论: 安全性略有提升,但对于稍有经验的逆向工程师来说,形同虚设。
迭代三:非对称加密 + 数字签名(行业标准版)
为了解决“密钥必须下发给客户端”的问题,我们需要引入 非对称加密(RSA) 和 数字签名 技术。这是目前大多数 License 系统的核心原理。
原理:
非对称加密有一对密钥:私钥(Private Key) 和 公钥(Public Key)。
- 私钥:保存在厂商手里,绝不泄露,用于签名。
- 公钥:埋在客户端代码里,用于验签。
生成逻辑(厂商端):
- 准备明文授权信息(JSON)。
- 对 JSON 内容进行 Hash 计算(如 MD5 或 SHA256),得到摘要。
- 使用 私钥 对摘要进行加密,生成 “数字签名”。
- 将
明文信息 + 数字签名打包发给用户,这就是 License 文件。
验证逻辑(客户端):
- 软件读取 License 文件,拆解出
明文信息和数字签名。 - 客户端使用代码里内置的 公钥 解密
数字签名,得到摘要A(如果解密失败,说明签名被篡改)。 - 客户端对
明文信息进行同样的 Hash 计算,得到摘要B。 - 对比
摘要A和摘要B。- 如果相等:说明明文没有被篡改,且确实是由拥有私钥的厂商签发的。
- 如果不等:说明被篡改了。
是如何解决之前的问题的?
破解者手里只有公钥。公钥只能用来解密(验证),不能用来加密(生成签名)。所以破解者即使改了明文里的过期时间,因为他没有私钥,无法生成对应的新签名,软件校验时就会发现 摘要A != 摘要B,从而拒绝启动。
新的问题:
虽然防篡改解决了,但防复制没解决。A 公司买了一个 License,把它发给 B 公司,B 公司也能通过验证(因为 License 本身是合法的)。
迭代四:机器特征绑定(进阶版)
为了解决“一证多用”的问题,我们需要把 License 和运行软件的机器硬件绑定。
生成逻辑(厂商端):
- 要求用户在部署服务器上运行一个脚本,获取服务器的唯一指纹(Machine ID)。指纹通常由
CPU序列号 + 网卡MAC地址 + 主板序列号组合而成。 - 用户将 Machine ID 发给厂商。
- 厂商将 Machine ID 写入到 License 的明文 JSON 中。
{ "expireTime": "2025-12-31", "machineId": "CPU-BF31-MAC-8821" // 绑定机器 } - 使用私钥生成签名。
验证逻辑(客户端):
- 使用公钥验签(确保文件没被改过)。
- 新增步骤:软件获取当前服务器的硬件信息,计算出本地的
Local Machine ID。 - 将
Local Machine ID与 License 文件中的machineId对比。如果不一致,说明 License 是从别的机器拷贝过来的,拒绝启动。
最终版本的局限性:为什么还是防不住?
到了“迭代四”,我们已经拥有了一个包含 RSA 签名校验 + 机器绑定 + 有效期控制 的完善 License 系统。这已经能防住绝大部分普通用户和初级黑客。
但是,它依然不是绝对安全的。为什么?
因为代码最终是在用户的机器上运行的,客户端是没有秘密的。
-
公钥替换攻击:
破解者虽然拿不到你的私钥,但他可以生成一对自己的“私钥B”和“公钥B”。他修改你的客户端程序(比如修改 Jar 包),把你内置的“公钥A”换成他的“公钥B”。然后他就可以用“私钥B”随意签发 License 了。 -
暴力修改判断逻辑(爆破):
最终的代码里,总会有类似这样的一行判断:if (license.verify()) { run(); } else { exit(); }破解者不需要搞懂你的加密算法,他只需要通过反编译工具(如 Javassist)找到这行代码,把它改成:
if (true) { // 强制为真 run(); }或者直接删除
else分支。这就是所谓的“暴力破解”。
应对方案(且战且退):
为了对抗上述攻击,厂商通常会引入 代码混淆(Obfuscation) 和 加壳 技术,增加反编译的难度,或者在程序中埋入多个隐蔽的校验点。但这本质上是一场“猫鼠游戏”,只能增加破解成本,无法从根本上杜绝破解。
总结
License 的本质不是为了实现“绝对安全”,而是为了提高破解门槛。
一个成熟的 License 方案(RSA + 签名 + 机器绑定)足以挡住 99% 的普通用户和非专业人士,保障厂商的商业利益。对于那 1% 精通逆向工程的高手,单纯靠技术手段是防不住的,这时候就需要法律手段(律师函警告)来补位了。