Spring-Boot-License
背景与需求
在软件开发和商业化过程中,许可证控制是一个不可避免的技术需求。无论是企业级管理系统、桌面应用软件,还是SaaS服务,都需要对软件的使用范围、功能权限和时间限制进行有效管控。
许可证系统的核心价值在于:
- 保护知识产权: 防止软件被非法复制和分发
- 商业模式支撑: 支持按功能、按时间的差异化定价
- 用户管理: 精确控制授权用户和使用范围
- 合规要求: 满足企业对软件资产管理的需求
本文将介绍一个基于Spring Boot + RSA2048非对称加密的许可证控制系统实现方案,具备硬件绑定、功能权限控制等。
设计思路
技术设计
许可证系统采用非对称加密的设计思路:厂商使用私钥对许可证信息进行数字签名,客户端使用对应的公钥验证签名的真实性。这种架构的优势在于:
- 安全性高: 私钥由厂商严格保管,公钥可以随软件分发,即使公钥泄露也无法伪造许可证
- 部署简单: 无需额外的许可证服务器,支持离线验证
- 扩展性强: 可以灵活添加各种验证规则和 权限控制
技术选型
后端技术栈:
- Spring Boot 3.x: 提供完整的Web服务框架和依赖注入
- Java Security API: 利用JDK内置的RSA加密算法实现
- Jackson: 处理JSON序列化和反序列化
前端技术栈
- 原生JavaScript: 无框架依赖,保持轻量级
- TailwindCSS: 快速构建现代化UI界面
- RESTful API: 标准化的前后端交互
加密算法:
- RSA2048: 足够安全的非对称加密强度
- SHA256withRSA: 数字签名算法
- Base64: 签名结果编码格式
核心功能实现
硬件指纹获取
硬件绑定是许可证系统的重要安全特性,通过获取主板序列号实现设备唯一性 识别。
@Component
public class HardwareUtil {
private static final Logger logger = LoggerFactory.getLogger(HardwareUtil.class);
/**
* 获取主板序列号,支持Windows和Linux系统
*/
public String getMotherboardSerial() {
String os = System.getProperty("os.name").toLowerCase();
try {
if (os.contains("windows")) {
return getWindowsMotherboardSerial();
} elseif (os.contains("linux")) {
return getLinuxMotherboardSerial();
} else {
logger.warn("不支持的操作系统: {}", os);
return"UNKNOWN";
}
} catch (Exception e) {
logger.error("获取主板序列号失败", e);
return "UNKNOWN";
}
}
/**
* Windows系统通过WMI命令获取主板序列号
*/
private String getWindowsMotherboardSerial() throws Exception {
Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && !line.equals("SerialNumber")) {
logger.debug("Windows主板序列号: {}", line);
return line;
}
}
reader.close();
process.waitFor();
return "UNKNOWN";
}
/**
* Linux系统通过dmidecode命令获取主板序列号
*/
private String getLinuxMotherboardSerial() throws Exception {
try {
// 优先使用dmidecode命令
Process process = Runtime.getRuntime().exec("sudo dmidecode -s baseboard-serial-number");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
String line = reader.readLine();
reader.close();
process.waitFor();
if (line != null && !line.trim().isEmpty() && !line.contains("Not Specified")) {
logger.debug("Linux主板序列号: {}", line.trim());
return line.trim();
}
// 备选方案:读取系统文件
return getLinuxMotherboardFromSys();
} catch (Exception e) {
logger.error("dmidecode命令执行失败", e);
return getLinuxMotherboardFromSys();
}
}
/**
* 从/sys/class/dmi/id/board_serial文件读取主板序列号
*/
private String getLinuxMotherboardFromSys() {
try {
Process process = Runtime.getRuntime().exec("cat /sys/class/dmi/id/board_serial");
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
String line = reader.readLine();
reader.close();
process.waitFor();
if (line != null && !line.trim().isEmpty()) {
logger.debug("Linux主板序列号(从sys读取): {}", line.trim());
return line.trim();
}
} catch (Exception e) {
logger.warn("从/sys文件读取失败", e);
}
return "UNKNOWN";
}
/**
* 获取系统信息摘要,用于调试和展示
*/
public String getSystemInfo() {
return String.format("操作系统: %s %s, 架构: %s, 主板序列号: %s",
System.getProperty("os.name"),
System.getProperty("os.version"),
System.getProperty("os.arch"),
getMotherboardSerial()
);
}
}
这个实现的关键点:
- 异常处理: 获取失败时返回"UNKNOWN"而不是抛出异常,保证程序稳定性
- 多重备选: Linux下优先使用dmidecode,失败时尝试读取sys文件
- 编码处理: 统一使用UTF-8编码避免乱码问题
- 日志记录: 详细记录获取过程,便于问题排查
RSA加密工具类
RSA加密是整个系统的安全基石,需要提供密钥生成、签名、验签等完整功能。
@Component
public class RSAUtil {
private static final Logger logger = LoggerFactory.getLogger(RSAUtil.class);
private static final String ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final int KEY_SIZE = 2048;
/**
* 生成RSA密钥对
*/
public KeyPair generateKeyPair() throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM);
keyGen.initialize(KEY_SIZE);
KeyPair keyPair = keyGen.generateKeyPair();
logger.info("RSA密钥对生成成功,密钥长度: {} bits", KEY_SIZE);
return keyPair;
}
/**
* 使用私钥对数据进行数字签名
*/
public String sign(String data, PrivateKey privateKey) throws Exception {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signedBytes = signature.sign();
String result = Base64.getEncoder().encodeToString(signedBytes);
logger.debug("数据签名完成,原始数据长度: {}, 签名长度: {}", data.length(), result.length());
return result;
}
/**
* 使用公钥验证数字签名
*/
public boolean verify(String data, String signatureBase64, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicKey);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
boolean isValid = signature.verify(signatureBytes);
logger.debug("签名验证结果: {}", isValid ? "通过" : "失败");
return isValid;
}
/**
* 将私钥转换为PEM格式字符串
*/
public String privateKeyToPem(PrivateKey privateKey) {
String encoded = Base64.getEncoder().encodeToString(privateKey.getEncoded());
return"-----BEGIN PRIVATE KEY-----\n" +
formatBase64String(encoded) +
"\n-----END PRIVATE KEY-----";
}
/**
* 将公钥转换为PEM格式字符串
*/
public String publicKeyToPem(PublicKey publicKey) {
String encoded = Base64.getEncoder().encodeToString(publicKey.getEncoded());
return"-----BEGIN PUBLIC KEY-----\n" +
formatBase64String(encoded) +
"\n-----END PUBLIC KEY-----";
}
/**
* 从PEM格式字符串加载私钥
*/
public PrivateKey loadPrivateKeyFromPem(String pemContent) throws Exception {
String privateKeyPEM = pemContent
.replaceAll("-----\\w+ PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(privateKeyPEM);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePrivate(spec);
}
/**
* 从PEM格式字符串加载公钥
*/
public PublicKey loadPublicKeyFromPem(String pemContent) throws Exception {
String publicKeyPEM = pemContent
.replaceAll("-----\\w+ PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM);
return keyFactory.generatePublic(spec);
}
/**
* 格式化Base64字符串,每64个字符换行
*/
private String formatBase64String(String base64) {
StringBuilder formatted = new StringBuilder();
for (int i = 0; i < base64.length(); i += 64) {
formatted.append(base64, i, Math.min(i + 64, base64.length())).append("\n");
}
return formatted.toString().trim();
}
}