添加license
This commit is contained in:
@@ -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 校验通过");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user