添加license

This commit is contained in:
mass
2026-05-18 09:21:57 +08:00
15 changed files with 887 additions and 9 deletions
@@ -0,0 +1,19 @@
package com.ruoyi.common.utils.license;
/**
* License 授权异常。
*/
public class LicenseException extends Exception
{
private static final long serialVersionUID = 1L;
public LicenseException(String message)
{
super(message);
}
public LicenseException(String message, Throwable cause)
{
super(message, cause);
}
}
@@ -0,0 +1,97 @@
package com.ruoyi.common.utils.license;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Scanner;
/**
* License 生成工具(独立运行)。
*
* <p>使用方法:
* <ol>
* <li>IDEA 开发环境:右键 main 方法 → Run,在 Program arguments 中填写路径,如 D:\license\license.lic</li>
* <li>服务器环境:java -cp ruoyi.jar com.ruoyi.common.utils.license.LicenseGenerator [路径]</li>
* <li>无参数运行:交互式输入路径</li>
* </ol>
*
* <p>注意:此工具必须在目标服务器上运行,不能在开发机上生成后拷贝。
*/
public class LicenseGenerator
{
public static void main(String[] args)
{
System.out.println("========================================");
System.out.println(" License 授权文件生成工具");
System.out.println("========================================");
System.out.println();
try
{
String fingerprint = LicenseUtils.collectMachineFingerprint();
System.out.println("当前服务器机器指纹(MAC 地址):");
System.out.println(fingerprint);
System.out.println();
String licenseCode = LicenseUtils.sign(fingerprint);
System.out.println("生成的授权码:");
System.out.println(licenseCode);
System.out.println();
String outputPath;
if (args != null && args.length > 0 && !args[0].trim().isEmpty())
{
outputPath = args[0].trim();
System.out.println("使用参数指定路径:" + outputPath);
}
else
{
Scanner scanner = new Scanner(System.in);
System.out.print("请输入 license.lic 文件保存路径(如 D:\\license\\license.lic 或 /opt/license/license.lic):");
outputPath = scanner.nextLine().trim();
}
if (outputPath.isEmpty())
{
System.err.println("错误:路径不能为空");
System.exit(1);
}
File outputFile = new File(outputPath);
File parentDir = outputFile.getParentFile();
if (parentDir != null && !parentDir.exists())
{
if (!parentDir.mkdirs())
{
System.err.println("错误:无法创建目录 " + parentDir.getAbsolutePath());
System.exit(1);
}
}
Files.write(Paths.get(outputPath), licenseCode.getBytes(StandardCharsets.UTF_8));
System.out.println();
System.out.println("========================================");
System.out.println(" License 文件生成成功!");
System.out.println("========================================");
System.out.println("文件路径:" + outputFile.getAbsolutePath());
System.out.println();
System.out.println("下一步操作:");
System.out.println("1. 将此路径配置到 application.yml 的 af.license.path 参数中");
System.out.println("2. 启动应用程序,系统会自动校验 License");
System.out.println();
}
catch (LicenseException e)
{
System.err.println("错误:" + e.getMessage());
System.exit(1);
}
catch (Exception e)
{
System.err.println("生成 License 文件失败:" + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}
@@ -0,0 +1,185 @@
package com.ruoyi.common.utils.license;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.NetworkInterface;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
/**
* License 授权工具类。
*
* <p>授权原理:
* <ol>
* <li>采集当前服务器所有物理网卡的 MAC 地址,排序后拼接为指纹字符串。</li>
* <li>用内置密钥对指纹做 HMAC-SHA256 签名,得到授权码。</li>
* <li>将授权码写入 license.lic 文件(由开发者在目标机器上生成后交付)。</li>
* <li>程序启动时重新采集 MAC 指纹,重新计算签名,与 license.lic 内容比对。</li>
* </ol>
*
* <p>防篡改:签名密钥硬编码在 jar 内,甲方无法自行伪造 license.lic。
*/
public class LicenseUtils
{
private static final Logger log = LoggerFactory.getLogger(LicenseUtils.class);
/**
* HMAC 签名密钥,打包后不可见,修改此值会使所有已颁发的 License 失效。
* 生产部署前请替换为你自己的随机字符串。
*/
private static final String HMAC_SECRET = "D0uY!n@R3ch@rge#S3cr3tK3y$2026";
private static final String HMAC_ALGORITHM = "HmacSHA256";
/**
* 采集当前机器所有物理网卡的 MAC 地址,排序后拼接为机器指纹。
*
* @return 机器指纹字符串,格式如 "AA:BB:CC:DD:EE:FF|11:22:33:44:55:66"
* @throws LicenseException 无法获取任何 MAC 地址时抛出
*/
public static String collectMachineFingerprint() throws LicenseException
{
List<String> macList = new ArrayList<>();
try
{
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
if (interfaces == null)
{
throw new LicenseException("无法枚举网络接口");
}
while (interfaces.hasMoreElements())
{
NetworkInterface ni = interfaces.nextElement();
if (ni.isLoopback() || ni.isVirtual() || !ni.isUp())
{
continue;
}
byte[] mac = ni.getHardwareAddress();
if (mac == null || mac.length == 0)
{
continue;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++)
{
if (i > 0)
{
sb.append(":");
}
sb.append(byteToHex(mac[i]).toUpperCase());
}
macList.add(sb.toString());
}
}
catch (LicenseException e)
{
throw e;
}
catch (Exception e)
{
throw new LicenseException("采集 MAC 地址失败:" + e.getMessage(), e);
}
if (macList.isEmpty())
{
throw new LicenseException("未找到任何物理网卡 MAC 地址,无法生成机器指纹");
}
Collections.sort(macList);
return String.join("|", macList);
}
/**
* 对机器指纹做 HMAC-SHA256 签名,生成授权码。
*
* @param fingerprint 机器指纹
* @return 十六进制授权码字符串
* @throws LicenseException 签名失败时抛出
*/
public static String sign(String fingerprint) throws LicenseException
{
try
{
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(
HMAC_SECRET.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(keySpec);
byte[] raw = mac.doFinal(fingerprint.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : raw)
{
sb.append(byteToHex(b));
}
return sb.toString();
}
catch (Exception e)
{
throw new LicenseException("License 签名失败:" + e.getMessage(), e);
}
}
/**
* 将单个字节转为两位小写十六进制字符串,不依赖 String.format。
*/
private static String byteToHex(byte b)
{
final char[] HEX = "0123456789abcdef".toCharArray();
return "" + HEX[(b >> 4) & 0xF] + HEX[b & 0xF];
}
/**
* 校验 License 文件是否与当前机器匹配。
*
* <p>校验逻辑:读取 licenseFilePath 文件内容,与当前机器指纹的签名比对。
*
* @param licenseFilePath license.lic 文件的绝对路径
* @throws LicenseException 文件不存在、内容为空或签名不匹配时抛出
*/
public static void verify(String licenseFilePath) throws LicenseException
{
log.info("License 校验开始,文件路径:{}", licenseFilePath);
if (licenseFilePath == null || licenseFilePath.trim().isEmpty())
{
throw new LicenseException("License 文件路径未配置(af.license.path)");
}
String licenseContent;
try
{
licenseContent = new String(
Files.readAllBytes(Paths.get(licenseFilePath.trim())),
StandardCharsets.UTF_8).trim();
}
catch (Exception e)
{
throw new LicenseException("读取 License 文件失败,路径:" + licenseFilePath + ",原因:" + e.getMessage(), e);
}
if (licenseContent.isEmpty())
{
throw new LicenseException("License 文件内容为空");
}
String fingerprint = collectMachineFingerprint();
log.info("当前机器指纹:{}", fingerprint);
String expected = sign(fingerprint);
if (!expected.equalsIgnoreCase(licenseContent))
{
log.error("License 校验失败,当前机器指纹与授权不匹配");
throw new LicenseException("License 授权校验失败:当前服务器未获得授权,程序拒绝启动");
}
log.info("License 校验通过");
}
}