From 6c3d98eaacf079b1445efad65d6dda29353063be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 18 May 2026 10:03:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20license=E6=A0=A1=E9=AA=8C=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EasyflowLicenseBootstrapValidator.java | 24 ++- .../license/EasyflowLicenseProperties.java | 69 ++++++++ .../license/MachineIdentityCollector.java | 155 +++++++++++++++--- .../license/MachineIdentityCollectorTest.java | 43 ++++- 4 files changed, 262 insertions(+), 29 deletions(-) diff --git a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseBootstrapValidator.java b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseBootstrapValidator.java index 2f6403d..7f24025 100644 --- a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseBootstrapValidator.java +++ b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseBootstrapValidator.java @@ -22,12 +22,15 @@ public class EasyflowLicenseBootstrapValidator implements BeanFactoryPostProcess private final EasyflowLicenseVerifier easyflowLicenseVerifier; private String location; + private String machineIdFile; + private String productUuidFile; + private String macAddressFile; /** * 构造启动前校验器。 */ public EasyflowLicenseBootstrapValidator() { - this(new EasyflowLicenseVerifier(new DefaultResourceLoader(), new MachineIdentityCollector())); + this(null); } /** @@ -47,6 +50,9 @@ public class EasyflowLicenseBootstrapValidator implements BeanFactoryPostProcess @Override public void setEnvironment(Environment environment) { this.location = environment.getProperty("easyflow.license.location"); + this.machineIdFile = environment.getProperty("easyflow.license.machine-id-file"); + this.productUuidFile = environment.getProperty("easyflow.license.product-uuid-file"); + this.macAddressFile = environment.getProperty("easyflow.license.mac-address-file"); } /** @@ -58,7 +64,7 @@ public class EasyflowLicenseBootstrapValidator implements BeanFactoryPostProcess @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { - EasyflowLicenseVerificationResult result = easyflowLicenseVerifier.verify(location); + EasyflowLicenseVerificationResult result = resolveVerifier().verify(location); LOG.info("license 校验通过: location={}, licenseId={}, keyId={}, licenseType={}, expiresAt={}", result.location(), result.licenseId(), @@ -92,4 +98,18 @@ public class EasyflowLicenseBootstrapValidator implements BeanFactoryPostProcess public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } + + private EasyflowLicenseVerifier resolveVerifier() { + if (easyflowLicenseVerifier != null) { + return easyflowLicenseVerifier; + } + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + MachineIdentityCollector collector = new MachineIdentityCollector( + resourceLoader, + machineIdFile, + productUuidFile, + macAddressFile + ); + return new EasyflowLicenseVerifier(resourceLoader, collector); + } } diff --git a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseProperties.java b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseProperties.java index 056d96c..20c4732 100644 --- a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseProperties.java +++ b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/EasyflowLicenseProperties.java @@ -15,6 +15,21 @@ public class EasyflowLicenseProperties { */ private String location; + /** + * 机器 machineId 文件位置,支持 classpath: 与 file: 形式。 + */ + private String machineIdFile; + + /** + * 设备 productUuid 文件位置,支持 classpath: 与 file: 形式。 + */ + private String productUuidFile; + + /** + * MAC 地址文件位置,支持 classpath: 与 file: 形式。 + */ + private String macAddressFile; + /** * 获取 license 资源位置。 * @@ -32,4 +47,58 @@ public class EasyflowLicenseProperties { public void setLocation(String location) { this.location = location; } + + /** + * 获取 machineId 文件位置。 + * + * @return machineId 文件位置 + */ + public String getMachineIdFile() { + return machineIdFile; + } + + /** + * 设置 machineId 文件位置。 + * + * @param machineIdFile machineId 文件位置 + */ + public void setMachineIdFile(String machineIdFile) { + this.machineIdFile = machineIdFile; + } + + /** + * 获取 productUuid 文件位置。 + * + * @return productUuid 文件位置 + */ + public String getProductUuidFile() { + return productUuidFile; + } + + /** + * 设置 productUuid 文件位置。 + * + * @param productUuidFile productUuid 文件位置 + */ + public void setProductUuidFile(String productUuidFile) { + this.productUuidFile = productUuidFile; + } + + /** + * 获取 MAC 地址文件位置。 + * + * @return MAC 地址文件位置 + */ + public String getMacAddressFile() { + return macAddressFile; + } + + /** + * 设置 MAC 地址文件位置。 + * + * @param macAddressFile MAC 地址文件位置 + */ + public void setMacAddressFile(String macAddressFile) { + this.macAddressFile = macAddressFile; + } } diff --git a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/MachineIdentityCollector.java b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/MachineIdentityCollector.java index 8eebd54..21506cc 100644 --- a/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/MachineIdentityCollector.java +++ b/easyflow-modules/easyflow-module-autoconfig/src/main/java/tech/easyflow/autoconfig/license/MachineIdentityCollector.java @@ -1,5 +1,8 @@ package tech.easyflow.autoconfig.license; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -20,6 +23,36 @@ public class MachineIdentityCollector { private static final Duration COMMAND_TIMEOUT = Duration.ofSeconds(10); + private final ResourceLoader resourceLoader; + private final String machineIdFile; + private final String productUuidFile; + private final String macAddressFile; + + /** + * 构造默认机器信息采集器。 + */ + public MachineIdentityCollector() { + this(new DefaultResourceLoader(), null, null, null); + } + + /** + * 构造支持外部文件覆盖的机器信息采集器。 + * + * @param resourceLoader 资源加载器 + * @param machineIdFile machineId 文件位置 + * @param productUuidFile productUuid 文件位置 + * @param macAddressFile MAC 地址文件位置 + */ + public MachineIdentityCollector(ResourceLoader resourceLoader, + String machineIdFile, + String productUuidFile, + String macAddressFile) { + this.resourceLoader = resourceLoader; + this.machineIdFile = machineIdFile; + this.productUuidFile = productUuidFile; + this.macAddressFile = macAddressFile; + } + /** * 采集当前机器的授权标识信息。 * @@ -27,16 +60,11 @@ public class MachineIdentityCollector { */ public MachineIdentity collect() { String osName = currentOsName().toLowerCase(Locale.ROOT); - if (osName.contains("win")) { - return collectWindowsIdentity(); - } - if (osName.contains("mac") || osName.contains("darwin")) { - return collectMacIdentity(); - } - if (osName.contains("nux") || osName.contains("linux")) { - return collectLinuxIdentity(); - } - throw new IllegalStateException("不支持的操作系统: " + currentOsName()); + return switch (resolveOsType(osName)) { + case WINDOWS -> collectWindowsIdentity(); + case MAC -> collectMacIdentity(); + case LINUX -> collectLinuxIdentity(); + }; } /** @@ -54,10 +82,19 @@ public class MachineIdentityCollector { * @return 机器信息 */ protected MachineIdentity collectLinuxIdentity() { - String machineId = executeShellCommand("读取 Linux machineId", "cat /etc/machine-id"); - String productUuid = executeShellCommand("读取 Linux productUuid", "cat /sys/class/dmi/id/product_uuid"); - String macAddress = executeShellCommand("读取 Linux 默认网卡 MAC", - "cat /sys/class/net/$(ip route | awk '/default/ {print $5; exit}')/address"); + String machineId = readConfiguredValue("读取 Linux machineId 文件", machineIdFile); + if (!StringUtils.hasText(machineId)) { + machineId = executeShellCommand("读取 Linux machineId", "cat /etc/machine-id"); + } + String productUuid = readConfiguredValue("读取 Linux productUuid 文件", productUuidFile); + if (!StringUtils.hasText(productUuid)) { + productUuid = executeShellCommand("读取 Linux productUuid", "cat /sys/class/dmi/id/product_uuid"); + } + String macAddress = readConfiguredValue("读取 Linux MAC 文件", macAddressFile); + if (!StringUtils.hasText(macAddress)) { + macAddress = executeShellCommand("读取 Linux 默认网卡 MAC", + "cat /sys/class/net/$(ip route | awk '/default/ {print $5; exit}')/address"); + } return new MachineIdentity(machineId, productUuid, macAddress); } @@ -67,12 +104,21 @@ public class MachineIdentityCollector { * @return 机器信息 */ protected MachineIdentity collectMacIdentity() { - String machineId = executeShellCommand("读取 macOS machineId", - "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/ {print $(NF-1)}'"); - String productUuid = executeShellCommand("读取 macOS productUuid", - "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/ {print $(NF-1)}'"); - String macAddress = executeShellCommand("读取 macOS 默认网卡 MAC", - "networksetup -listallhardwareports | awk '/Device/ {device=$2} /Ethernet Address/ {print $3; exit}'"); + String machineId = readConfiguredValue("读取 macOS machineId 文件", machineIdFile); + if (!StringUtils.hasText(machineId)) { + machineId = executeShellCommand("读取 macOS machineId", + "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/ {print $(NF-1)}'"); + } + String productUuid = readConfiguredValue("读取 macOS productUuid 文件", productUuidFile); + if (!StringUtils.hasText(productUuid)) { + productUuid = executeShellCommand("读取 macOS productUuid", + "ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/ {print $(NF-1)}'"); + } + String macAddress = readConfiguredValue("读取 macOS MAC 文件", macAddressFile); + if (!StringUtils.hasText(macAddress)) { + macAddress = executeShellCommand("读取 macOS 默认网卡 MAC", + "networksetup -listallhardwareports | awk '/Device/ {device=$2} /Ethernet Address/ {print $3; exit}'"); + } return new MachineIdentity(machineId, productUuid, macAddress); } @@ -82,15 +128,50 @@ public class MachineIdentityCollector { * @return 机器信息 */ protected MachineIdentity collectWindowsIdentity() { - String machineId = executePowerShellCommand("读取 Windows machineId", - "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid"); - String productUuid = executePowerShellCommand("读取 Windows productUuid", - "(Get-CimInstance Win32_ComputerSystemProduct).UUID"); - String macAddress = executePowerShellCommand("读取 Windows 默认网卡 MAC", - "(Get-NetAdapter | Where-Object {$_.Status -eq 'Up' -and $_.MacAddress} | Select-Object -First 1 -ExpandProperty MacAddress)"); + String machineId = readConfiguredValue("读取 Windows machineId 文件", machineIdFile); + if (!StringUtils.hasText(machineId)) { + machineId = executePowerShellCommand("读取 Windows machineId", + "(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid"); + } + String productUuid = readConfiguredValue("读取 Windows productUuid 文件", productUuidFile); + if (!StringUtils.hasText(productUuid)) { + productUuid = executePowerShellCommand("读取 Windows productUuid", + "(Get-CimInstance Win32_ComputerSystemProduct).UUID"); + } + String macAddress = readConfiguredValue("读取 Windows MAC 文件", macAddressFile); + if (!StringUtils.hasText(macAddress)) { + macAddress = executePowerShellCommand("读取 Windows 默认网卡 MAC", + "(Get-NetAdapter | Where-Object {$_.Status -eq 'Up' -and $_.MacAddress} | Select-Object -First 1 -ExpandProperty MacAddress)"); + } return new MachineIdentity(machineId, productUuid, macAddress); } + /** + * 读取外部配置文件中的机器参数。 + * + * @param description 描述 + * @param location 资源位置 + * @return 文件内容;未配置时返回空字符串 + */ + protected String readConfiguredValue(String description, String location) { + if (!StringUtils.hasText(location)) { + return ""; + } + try { + Resource resource = resourceLoader.getResource(location); + if (!resource.exists() || !resource.isReadable()) { + throw new IllegalStateException(description + "失败,资源不存在或不可读: " + location); + } + String output = readStream(resource.getInputStream()).trim(); + if (!StringUtils.hasText(output)) { + throw new IllegalStateException(description + "失败,输出为空"); + } + return output; + } catch (IOException e) { + throw new IllegalStateException(description + "失败,无法读取资源: " + location, e); + } + } + /** * 通过 shell 执行命令。 * @@ -164,4 +245,26 @@ public class MachineIdentityCollector { inputStream.transferTo(outputStream); return outputStream.toString(StandardCharsets.UTF_8); } + + private OsType resolveOsType(String osName) { + if (osName.contains("win")) { + return OsType.WINDOWS; + } + if (osName.contains("mac") || osName.contains("darwin")) { + return OsType.MAC; + } + if (osName.contains("nux") || osName.contains("linux")) { + return OsType.LINUX; + } + throw new IllegalStateException("不支持的操作系统: " + currentOsName()); + } + + /** + * 当前操作系统分类。 + */ + private enum OsType { + WINDOWS, + MAC, + LINUX + } } diff --git a/easyflow-modules/easyflow-module-autoconfig/src/test/java/tech/easyflow/autoconfig/license/MachineIdentityCollectorTest.java b/easyflow-modules/easyflow-module-autoconfig/src/test/java/tech/easyflow/autoconfig/license/MachineIdentityCollectorTest.java index a97f85d..02be0a3 100644 --- a/easyflow-modules/easyflow-module-autoconfig/src/test/java/tech/easyflow/autoconfig/license/MachineIdentityCollectorTest.java +++ b/easyflow-modules/easyflow-module-autoconfig/src/test/java/tech/easyflow/autoconfig/license/MachineIdentityCollectorTest.java @@ -2,9 +2,12 @@ package tech.easyflow.autoconfig.license; import org.junit.Assert; import org.junit.Test; +import org.springframework.core.io.DefaultResourceLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -59,6 +62,35 @@ public class MachineIdentityCollectorTest { Assert.assertEquals("AA-BB-CC-DD-EE-FF", identity.macAddresses()); } + /** + * 已配置的覆盖文件应优先于系统命令。 + * + * @throws Exception 创建临时文件失败 + */ + @Test + public void shouldPreferConfiguredIdentityFiles() throws Exception { + Path tempDir = Files.createTempDirectory("machine-identity-collector"); + Path machineIdFile = tempDir.resolve("machine-id.txt"); + Path productUuidFile = tempDir.resolve("product-uuid.txt"); + Path macAddressFile = tempDir.resolve("mac-address.txt"); + Files.writeString(machineIdFile, "mounted-machine\n", StandardCharsets.UTF_8); + Files.writeString(productUuidFile, "mounted-product\n", StandardCharsets.UTF_8); + Files.writeString(macAddressFile, "02:42:ac:20:00:11\n", StandardCharsets.UTF_8); + + TestMachineIdentityCollector collector = new TestMachineIdentityCollector( + "Linux", + new DefaultResourceLoader(), + machineIdFile.toUri().toString(), + productUuidFile.toUri().toString(), + macAddressFile.toUri().toString() + ); + + MachineIdentity identity = collector.collect(); + Assert.assertEquals("mounted-machine", identity.machineId()); + Assert.assertEquals("mounted-product", identity.productUuid()); + Assert.assertEquals("02:42:ac:20:00:11", identity.macAddresses()); + } + /** * 空输出应被识别为采集失败。 */ @@ -85,6 +117,15 @@ public class MachineIdentityCollectorTest { private final Map powerShellOutputs = new HashMap<>(); private TestMachineIdentityCollector(String osName) { + this(osName, new DefaultResourceLoader(), null, null, null); + } + + private TestMachineIdentityCollector(String osName, + DefaultResourceLoader resourceLoader, + String machineIdFile, + String productUuidFile, + String macAddressFile) { + super(resourceLoader, machineIdFile, productUuidFile, macAddressFile); this.osName = osName; }