Java 离线 License 加密验签工具与后端启停控制实现计划
根据我们的讨论,我们将基于 纯离线物理文件传递 和 明文绝对不落库(纯内存运行) 的最高安全准则来实现这个静态 License 系统。同时,"功能启停(Feature Toggle)"将完全由后端进行拦截和控制,前端不介入权限判断。
User Review Required
!IMPORTANT
模块位置确认 :计划在
fongwell-common下新建一个专门的fongwell-common-license模块。这样可以保持与核心业务逻辑解耦,便于日后单独打包交付给运维作为生成工具。请确认是否接受新建模块。!IMPORTANT
硬件指纹采集库 :为了获取 CPU 序列号、主板序列号和 MAC 地址,Java 推荐引入
oshi-core(基于 JNA 获取底层硬件信息)。请确认是否可以引入此依赖。
离线业务流转模型
- 获取机器码 :客户在目标服务器上运行由我们提供的脱机小工具(基于
HardwareUtils提取),获取服务器的唯一机器码并提供给我方运维。 - 离线发证 :我方运维在后台使用内部生成工具,基于机器码和授权范围生成
fongwell.lic授权文件,并通过离线方式(U盘/邮件/微信等)发送给客户。 - 放置文件 :客户将
fongwell.lic放置在应用服务器指定的本地目录下。 - 自动装载与纯内存运行 :应用启动时自动读取该目录下的授权文件并在内存中完成解密和验签 ,任何明文授权状态绝对不落入数据库,从根本上杜绝客户通过修改数据库表来绕过有效期的行为。
Proposed Changes
1. 核心加密与验签组件 (LicenseCryptoUtils)
我们将采用 RSA 非对称加密 (用于防篡改签名) + AES 对称加密 (用于防偷窥加解密) 的组合方案。
核心类库:LicenseCryptoUtils
-
生成端 (License Center 侧):
- 将业务授权信息 JSON 化。
- 使用
AES密钥加密 JSON 数据。 - 使用
RSA 私钥对加密后的数据生成数字签名。 - 将加密数据和签名封装为文件流输出。
-
解析端 (License Manager 侧):
- 从服务器本地目录读取
.lic文件。 - 使用内置的
RSA 公钥验证签名是否合法。 - 签名验证通过后,使用
AES密钥解密数据,还原为 JSON 对象,并存放于内存。
- 从服务器本地目录读取
2. License 数据载体 (LicensePayload)
设计标准的 Java DTO 承载授权信息:
java
public class LicensePayload {
private String licenseId; // 许可证ID
private String enterpriseName; // 授权企业名称
private Long effectiveTime; // 生效时间戳
private Long expireTime; // 过期时间戳
private String machineCode; // 绑定的机器码(硬件指纹)
private Map<String, Boolean> features; // 功能模块开关控制
private Map<String, Long> capacities; // 容量控制(如最大节点数/用户数)
}
3. 后端功能启停控制拦截 (Feature Toggle - Backend Only)
前端不介入判断,所有访问控制收拢在后端。使用 Spring AOP + 自定义注解 的方式实现全局拦截。
NEW LicenseManager.java(file:///d:/project_space/shubao/fongwell-common/fongwell-common-license/src/main/java/com/fongwell/common/license/manager/LicenseManager.java)
- 负责在 Spring Boot 启动时加载
.lic文件。 - 将解密验签后的
LicensePayload缓存为内存中的单例对象。 - 提供定时刷新机制,如果运维在运行时替换了
.lic,可以通过定时器或者对外提供一个/refreshAPI 来重载到内存。
NEW RequireLicense.java(file:///d:/project_space/shubao/fongwell-common/fongwell-common-license/src/main/java/com/fongwell/common/license/annotation/RequireLicense.java)
创建自定义注解,标注在需要授权控制的 Controller 接口上。
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireLicense {
// 指定需要的模块名称,如不指定则只校验License是否过期
String module() default "";
}
NEW LicenseAspect.java(file:///d:/project_space/shubao/fongwell-common/fongwell-common-license/src/main/java/com/fongwell/common/license/aspect/LicenseAspect.java)
通过 AOP 切面拦截请求:
- 拦截时以极低的延迟(纳秒级)读取内存中的
LicenseManager状态。 - 机器码与有效期校验 :当前服务器机器码如果不匹配,或当前时间超出有效期,则抛出
LicenseExpiredException。 - 模块授权校验 :检查注解的
module值是否在LicensePayload.features中被设置为true。否则抛出LicenseFeatureForbiddenException。 - 全局异常拦截后,统一向前端返回特定的 HTTP 状态码(如
403 Forbidden)。
4. 硬件指纹提取工具 (HardwareUtils)
NEW HardwareUtils.java(file:///d:/project_space/shubao/fongwell-common/fongwell-common-license/src/main/java/com/fongwell/common/license/utils/HardwareUtils.java)
使用 oshi-core 获取底层硬件信息,生成强绑定的物理机器码:
- 获取物理网卡的 MAC 地址。
- 获取 CPU Processor ID。
- 获取 Baseboard (主板) 序列号。
将这三者组合并经过 MD5 Hash 生成不可伪造的MachineCode。
Verification Plan
Automated Tests
- Crypto 工具测试 :
- 测试正常生成 RSA/AES 密钥并互相匹配。
- 测试构造伪造签名的文件,断言验签必须抛出异常(防御恶意篡改)。
- 硬件提取测试 :
- 编写
HardwareUtilsTest,确保在 Windows/Linux 环境下均能获取非空的强绑定机器码。
- 编写
Manual Verification
- 启动服务,将
fongwell.lic置于指定的应用加载目录下。 - 构造包含
moduleA权限且未过期的文件,请求接口:预期成功。 - 构造未授权
moduleB的文件,请求相关接口:预期在后端切面处被拦截,抛出403 Forbidden。 - 手动修改数据库中的任何业务表,试图干预 License 状态:预期系统依然只认
.lic内存状态,不受数据库干扰。