Bladeren bron

添加license授权模块,目前测试有证书可登录系统

zhaojinyu 4 uur geleden
bovenliggende
commit
aee2a43ac3
30 gewijzigde bestanden met toevoegingen van 1646 en 1 verwijderingen
  1. 1 0
      base-modules/pom.xml
  2. 42 0
      base-modules/service-license/pom.xml
  3. 69 0
      base-modules/service-license/service-license-client/pom.xml
  4. 31 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseApi.java
  5. 34 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseCheckInterceptor.java
  6. 47 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseCheckListener.java
  7. 17 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseClientApp.java
  8. 19 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseManagerHolder.java
  9. 257 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseVerify.java
  10. 29 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseVerifyParam.java
  11. 21 0
      base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseWebConfig.java
  12. 24 0
      base-modules/service-license/service-license-client/src/main/resources/bootstrap.yml
  13. 63 0
      base-modules/service-license/service-license-common/pom.xml
  14. 41 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomKeyStoreParam.java
  15. 259 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomLicenseManager.java
  16. 125 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomLicenseParam.java
  17. 21 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/LicenseCheckModel.java
  18. 11 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/LinuxServerInfos.java
  19. 104 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/OshiServerInfos.java
  20. 11 0
      base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/WindowsServerInfos.java
  21. 67 0
      base-modules/service-license/service-license-server/pom.xml
  22. 42 0
      base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseApi.java
  23. 98 0
      base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseCreator.java
  24. 38 0
      base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseCreatorParam.java
  25. 17 0
      base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseServerApp.java
  26. 12 0
      base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/ListProviders.java
  27. 24 0
      base-modules/service-license/service-license-server/src/main/resources/bootstrap.yml
  28. 94 0
      base-modules/service-license/service-license-server/src/main/resources/logback.xml
  29. 6 1
      base-modules/service-system/service-system-biz/pom.xml
  30. 22 0
      base-modules/service-system/service-system-biz/src/main/java/com/usky/system/service/config/WebConfig.java

+ 1 - 0
base-modules/pom.xml

@@ -13,6 +13,7 @@
         <module>service-job</module>
         <module>service-file</module>
         <module>service-system</module>
+        <module>service-license</module>
     </modules>
 
     <artifactId>base-modules</artifactId>

+ 42 - 0
base-modules/service-license/pom.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>base-modules</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>service-license</artifactId>
+    <packaging>pom</packaging>
+    <version>0.0.1</version>
+
+    <modules>
+        <module>service-license-common</module>
+        <module>service-license-server</module>
+        <module>service-license-client</module>
+    </modules>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <spring-boot.version>2.7.18</spring-boot.version>
+        <truelicense.version>1.33</truelicense.version>
+        <hutool.version>5.8.22</hutool.version>
+        <swagger.version>3.0.0</swagger.version>
+    </properties>
+
+    <!-- 子模块统一依赖管理 -->
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring-boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+</project>

+ 69 - 0
base-modules/service-license/service-license-client/pom.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.usky</groupId>
+        <artifactId>service-license</artifactId>
+        <version>0.0.1</version>
+    </parent>
+
+    <artifactId>service-license-client</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-license-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- Bouncy Castle 加密提供者 -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk18on</artifactId>
+            <version>1.78.1</version> <!-- 稳定版本,支持JDK8+ -->
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <!-- 可执行 jar 带 exec 后缀,不影响 Maven 依赖 -->
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <classifier>exec</classifier>   <!-- 关键行 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 31 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseApi.java

@@ -0,0 +1,31 @@
+package com.usky.license.client;
+
+import com.usky.license.common.LicenseCheckModel;
+import com.usky.license.common.OshiServerInfos;   // ① 引入通用实现
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@Api(tags = "License 生成接口")
+@RestController
+@RequestMapping("/license")
+public class LicenseApi {
+
+    /** 全平台通用实现(OSHI 3.9.1) */
+    private final OshiServerInfos serverInfos = new OshiServerInfos();
+
+    @ApiOperation("获取服务器硬件信息")
+    @GetMapping("/serverInfo")
+    public ResponseEntity<LicenseCheckModel> getServerInfos(
+            @RequestParam(value = "osName", required = false) String osName) {
+        // 参数仅做日志/兼容,不再决定实现类
+        if (osName == null) {
+            osName = System.getProperty("os.name");
+        }
+        return ResponseEntity.ok(serverInfos.getServerInfos());
+    }
+}

+ 34 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseCheckInterceptor.java

@@ -0,0 +1,34 @@
+package com.usky.license.client;
+
+import com.usky.common.core.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Component
+@Slf4j
+public class LicenseCheckInterceptor implements HandlerInterceptor {
+
+    @Autowired
+    private LicenseVerify licenseVerify;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        // 临时添加:打印 licenseVerify 是否为 null
+        if (licenseVerify == null) {
+            log.error("LicenseVerify 注入失败,为 null!");
+            throw new BusinessException("License 验证组件加载失败");
+        }
+
+        boolean verifyResult = licenseVerify.verify();
+        if (!verifyResult) {
+            log.warn("License 无效,拒绝服务");
+            throw new BusinessException("License 无效,请检查证书是否授权或已过期");
+        }
+        return true;
+    }
+}

+ 47 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseCheckListener.java

@@ -0,0 +1,47 @@
+package com.usky.license.client;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+
+@Component
+@Slf4j
+public class LicenseCheckListener implements ApplicationListener<ApplicationReadyEvent> {
+
+    @Value("${license.subject}")
+    private String subject;
+    @Value("${license.publicAlias}")
+    private String publicAlias;
+    @Value("${license.storePass}")
+    private String storePass;
+    @Value("${license.licensePath}")
+    private String licensePath;
+    @Value("${license.publicKeysStorePath}")
+    private String publicKeysStorePath;
+
+    @Autowired
+    private LicenseVerify licenseVerify;
+
+    @Override
+    public void onApplicationEvent(ApplicationReadyEvent event) {
+        // 原有证书安装逻辑...
+        if (!new File(licensePath).exists()) {
+            log.warn("证书文件 [{}] 不存在,跳过安装;请放置证书后重启。", licensePath);
+            return;
+        }
+
+        log.info("++++++++ 开始安装 License ++++++++");
+        LicenseVerifyParam param = new LicenseVerifyParam(subject, publicAlias,
+                storePass, licensePath, publicKeysStorePath);
+        licenseVerify.install(param);
+        log.info("++++++++ License 安装完成 ++++++++");
+
+        // 新增:读取证书有效期
+        licenseVerify.readLicenseExpireTime();
+    }
+}

+ 17 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseClientApp.java

@@ -0,0 +1,17 @@
+package com.usky.license.client;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+// 导入数据源自动配置排除类
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+
+/**
+ * 排除数据源自动配置,解决无数据库配置时的启动失败问题
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class LicenseClientApp {
+
+    public static void main(String[] args) {
+        SpringApplication.run(LicenseClientApp.class, args);
+    }
+}

+ 19 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseManagerHolder.java

@@ -0,0 +1,19 @@
+package com.usky.license.client;
+
+import com.usky.license.common.CustomLicenseManager;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+
+public class LicenseManagerHolder {
+    private static volatile LicenseManager INSTANCE;
+    public static LicenseManager getInstance(LicenseParam param) {
+        if (INSTANCE == null) {
+            synchronized (LicenseManagerHolder.class) {
+                if (INSTANCE == null) {
+                    INSTANCE = new CustomLicenseManager(param);
+                }
+            }
+        }
+        return INSTANCE;
+    }
+}

+ 257 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseVerify.java

@@ -0,0 +1,257 @@
+package com.usky.license.client;
+
+import com.usky.license.common.CustomLicenseManager;
+import com.usky.license.common.CustomLicenseParam;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+import de.schlichtherle.license.LicenseContent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.TimeZone;
+import java.util.prefs.Preferences;
+import org.springframework.core.env.Environment;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+
+@Slf4j
+@Component
+public class LicenseVerify {
+
+    @Value("${license.subject}")
+    private String defaultSubject;
+    @Value("${license.publicAlias}")
+    private String defaultPublicAlias;
+    @Value("${license.storePass}")
+    private String defaultStorePass;
+    @Value("${license.licensePath}")
+    private String defaultLicensePath;
+    @Value("${license.publicKeysStorePath}")
+    private String defaultPublicKeysStorePath;
+
+    @Autowired
+    private Environment env;
+
+    // ========== 新增:读取证书有效期 ==========
+    public void readLicenseExpireTime() {
+        // 1. 获取配置参数
+        String subject = env.getProperty("license.subject");
+        String publicAlias = env.getProperty("license.publicAlias");
+        String storePass = env.getProperty("license.storePass");
+        String licensePath = env.getProperty("license.licensePath");
+        String publicKeysStorePath = env.getProperty("license.publicKeysStorePath");
+
+        // 2. 非空校验
+        if (licensePath == null || licensePath.trim().isEmpty() || !new File(licensePath).exists()) {
+            log.error("证书文件不存在,路径:{}", licensePath);
+            return;
+        }
+        if (publicKeysStorePath == null || publicKeysStorePath.trim().isEmpty()) {
+            log.error("公钥库配置为空");
+            return;
+        }
+
+        try {
+            // 3. 构建LicenseParam
+            Preferences preferences = Preferences.userRoot().node(LicenseManager.class.getName());
+            LicenseParam licenseParam = new CustomLicenseParam(
+                    subject,
+                    preferences,
+                    new File(publicKeysStorePath),
+                    publicAlias,
+                    storePass.toCharArray(),
+                    new File(licensePath)
+            );
+
+            // 4. 初始化自定义LicenseManager
+            CustomLicenseManager licenseManager = new CustomLicenseManager(licenseParam);
+
+            // 5. 读取证书字节数组并解析内容
+            byte[] certBytes = Files.readAllBytes(new File(licensePath).toPath());
+            LicenseContent licenseContent = licenseManager.verifyCertificate(certBytes);
+
+            // 6. 获取有效期并格式化输出
+            Date notBefore = licenseContent.getNotBefore(); // 证书生效时间
+            Date notAfter = licenseContent.getNotAfter();   // 证书过期时间
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
+
+            log.info("===== 证书有效期信息 =====");
+            log.info("证书主题:{}", licenseContent.getSubject());
+            log.info("生效时间:{}", sdf.format(notBefore));
+            log.info("过期时间:{}", sdf.format(notAfter));
+            log.info("有效期时长:{} 天", (notAfter.getTime() - notBefore.getTime()) / (1000 * 60 * 60 * 24));
+            log.info("=======================");
+
+        } catch (Exception e) {
+            log.error("读取证书有效期失败", e);
+        }
+    }
+
+    // 无参安装(使用默认配置)
+    public boolean install() {
+        File licenseFile = new File(defaultLicensePath);
+        File publicKeysStoreFile = new File(defaultPublicKeysStorePath);
+        return doInstall(defaultSubject, defaultPublicAlias, defaultStorePass, licenseFile, publicKeysStoreFile);
+    }
+
+    // 自定义路径安装
+    public boolean install(String customLicensePath) {
+        File licenseFile = new File(customLicensePath);
+        File publicKeysStoreFile = new File(defaultPublicKeysStorePath);
+        return doInstall(defaultSubject, defaultPublicAlias, defaultStorePass, licenseFile, publicKeysStoreFile);
+    }
+
+    // 自定义参数安装
+    public boolean install(LicenseVerifyParam param) {
+        if (param == null) {
+            log.error("证书安装失败:LicenseVerifyParam参数为null");
+            return false;
+        }
+        // 参数非空校验
+        if (param.getSubject() == null || param.getSubject().trim().isEmpty()
+                || param.getPublicAlias() == null || param.getPublicAlias().trim().isEmpty()
+                || param.getStorePass() == null || param.getStorePass().trim().isEmpty()
+                || param.getLicensePath() == null || param.getLicensePath().trim().isEmpty()
+                || param.getPublicKeysStorePath() == null || param.getPublicKeysStorePath().trim().isEmpty()) {
+            log.error("证书安装失败:LicenseVerifyParam必要参数为空");
+            return false;
+        }
+        File licenseFile = new File(param.getLicensePath());
+        File publicKeysStoreFile = new File(param.getPublicKeysStorePath());
+        return doInstall(param.getSubject(), param.getPublicAlias(), param.getStorePass(), licenseFile, publicKeysStoreFile);
+    }
+
+    // 无参验证(此处是报错的核心方法,增加非空校验)
+    public boolean verify() {
+        // 核心:先校验配置参数是否为null,提前报错
+        if (defaultLicensePath == null || defaultLicensePath.trim().isEmpty()) {
+            log.error("证书验证失败:license.licensePath 配置为空请检查");
+            return false;
+        }
+        if (defaultPublicKeysStorePath == null || defaultPublicKeysStorePath.trim().isEmpty()) {
+            log.error("证书验证失败:license.publicKeysStorePath 配置为空请检查");
+            return false;
+        }
+        if (defaultSubject == null || defaultPublicAlias == null || defaultStorePass == null) {
+            log.error("证书验证失败:subject/publicAlias/storePass 配置为空请检查");
+            return false;
+        }
+
+        File licenseFile = new File(defaultLicensePath);
+        File publicKeysStoreFile = new File(defaultPublicKeysStorePath);
+        return doVerify(defaultSubject, defaultPublicAlias, defaultStorePass, licenseFile, publicKeysStoreFile);
+    }
+
+    // 自定义参数验证
+    public boolean verify(LicenseVerifyParam param) {
+        if (param == null) {
+            log.error("证书验证失败:LicenseVerifyParam参数为null");
+            return false;
+        }
+        // 参数非空校验
+        if (param.getSubject() == null || param.getSubject().trim().isEmpty()
+                || param.getPublicAlias() == null || param.getPublicAlias().trim().isEmpty()
+                || param.getStorePass() == null || param.getStorePass().trim().isEmpty()
+                || param.getLicensePath() == null || param.getLicensePath().trim().isEmpty()
+                || param.getPublicKeysStorePath() == null || param.getPublicKeysStorePath().trim().isEmpty()) {
+            log.error("证书验证失败:LicenseVerifyParam必要参数为空");
+            return false;
+        }
+        File licenseFile = new File(param.getLicensePath());
+        File publicKeysStoreFile = new File(param.getPublicKeysStorePath());
+        return doVerify(param.getSubject(), param.getPublicAlias(), param.getStorePass(), licenseFile, publicKeysStoreFile);
+    }
+
+    // 核心安装逻辑(验证+缓存)
+    private boolean doInstall(String subject, String publicAlias, String storePass, File licenseFile, File publicKeysStoreFile) {
+        // 文件存在性校验
+        if (!publicKeysStoreFile.exists()) {
+            log.error("证书安装失败:公钥库文件不存在 -> {}", publicKeysStoreFile.getAbsolutePath());
+            return false;
+        }
+        if (!licenseFile.exists()) {
+            log.error("证书安装失败:证书文件不存在 -> {}", licenseFile.getAbsolutePath());
+            return false;
+        }
+
+        try {
+            // 1. 构建LicenseParam(TrueLicense必要参数)
+            Preferences preferences = Preferences.userRoot().node(LicenseManager.class.getName());
+            LicenseParam licenseParam = new CustomLicenseParam(
+                    subject,
+                    preferences,
+                    publicKeysStoreFile,
+                    publicAlias,
+                    storePass.toCharArray(),
+                    licenseFile
+            );
+
+            // 2. 初始化自定义LicenseManager
+            CustomLicenseManager licenseManager = new CustomLicenseManager(licenseParam);
+
+            // 3. 读取证书字节数组(无需补位,明文签名证书)
+            byte[] certBytes = Files.readAllBytes(licenseFile.toPath());
+
+            // 4. 调用公共包装方法(核心!解决权限问题)
+            LicenseContent content = licenseManager.verifyCertificate(certBytes);
+
+            if (content != null) {
+                log.info("===== 证书安装(验证)成功 =====");
+                return true;
+            } else {
+                log.error("===== 证书安装失败:证书内容为空 =====");
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("===== 证书安装失败 =====", e);
+            return false;
+        }
+    }
+
+    // 核心验证逻辑
+    private boolean doVerify(String subject, String publicAlias, String storePass, File licenseFile, File publicKeysStoreFile) {
+        // 文件存在性校验
+        if (!publicKeysStoreFile.exists()) {
+            log.error("证书验证失败:公钥库文件不存在 -> {}", publicKeysStoreFile.getAbsolutePath());
+            return false;
+        }
+        if (!licenseFile.exists()) {
+            log.error("证书验证失败:证书文件不存在 -> {}", licenseFile.getAbsolutePath());
+            return false;
+        }
+
+        try {
+            // 1. 构建LicenseParam(TrueLicense必要参数)
+            Preferences preferences = Preferences.userRoot().node(LicenseManager.class.getName());
+            LicenseParam licenseParam = new CustomLicenseParam(
+                    subject,
+                    preferences,
+                    publicKeysStoreFile,
+                    publicAlias,
+                    storePass.toCharArray(),
+                    licenseFile
+            );
+
+            // 2. 初始化自定义LicenseManager
+            CustomLicenseManager licenseManager = new CustomLicenseManager(licenseParam);
+
+            // 3. 读取证书字节数组(无需补位,明文签名证书)
+            byte[] certBytes = Files.readAllBytes(licenseFile.toPath());
+
+            // 4. 调用公共包装方法(核心!解决权限问题)
+            licenseManager.verifyCertificate(certBytes);
+
+            log.info("===== 证书验证成功 =====");
+            return true;
+        } catch (Exception e) {
+            log.error("===== 证书验证失败 =====", e);
+            return false;
+        }
+    }
+}

+ 29 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseVerifyParam.java

@@ -0,0 +1,29 @@
+package com.usky.license.client;
+
+import lombok.Data;
+
+/**
+ * 证书验证/安装参数封装
+ */
+@Data
+public class LicenseVerifyParam {
+    // 证书主题(与keytool、服务端一致)
+    private String subject;
+    // 公钥别名
+    private String publicAlias;
+    // 公钥库密码
+    private String storePass;
+    // 证书文件路径
+    private String licensePath;
+    // 公钥库文件路径
+    private String publicKeysStorePath;
+
+    // 新增:5个参数的有参构造方法(解决报错核心)
+    public LicenseVerifyParam(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) {
+        this.subject = subject;
+        this.publicAlias = publicAlias;
+        this.storePass = storePass;
+        this.licensePath = licensePath;
+        this.publicKeysStorePath = publicKeysStorePath;
+    }
+}

+ 21 - 0
base-modules/service-license/service-license-client/src/main/java/com/usky/license/client/LicenseWebConfig.java

@@ -0,0 +1,21 @@
+package com.usky.license.client;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class LicenseWebConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private LicenseCheckInterceptor licenseCheckInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 排除掉 swagger、actuator 等
+        registry.addInterceptor(licenseCheckInterceptor)
+                .excludePathPatterns("/error", "/swagger-ui/**", "/v3/api-docs/**",
+                        "/actuator/**", "/license/**");
+    }
+}

+ 24 - 0
base-modules/service-license/service-license-client/src/main/resources/bootstrap.yml

@@ -0,0 +1,24 @@
+# Tomcat
+server:
+  port: 19999
+# Spring
+spring: 
+  application:
+    # 应用名称
+    name: service-license-client
+  profiles:
+    # 环境配置
+    active: dev
+  cloud:
+    nacos:
+      discovery:
+        # 服务注册地址
+        server-addr: usky-cloud-nacos:8848
+      config:
+        # 配置中心地址
+        server-addr: usky-cloud-nacos:8848
+        # 配置文件格式
+        file-extension: yml
+        # 共享配置
+        shared-configs:
+          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

+ 63 - 0
base-modules/service-license/service-license-common/pom.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.usky</groupId>
+        <artifactId>service-license</artifactId>
+        <version>0.0.1</version>
+    </parent>
+
+    <artifactId>service-license-common</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>de.schlichtherle.truelicense</groupId>
+            <artifactId>truelicense-core</artifactId>
+            <version>${truelicense.version}</version>
+        </dependency>
+
+        <!-- 监控服务器资源状态 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+            <version>3.9.1</version>
+        </dependency>
+
+        <!-- Bouncy Castle 加密提供者 -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk18on</artifactId>
+            <version>1.78.1</version> <!-- 稳定版本,支持JDK8+ -->
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.83</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-core</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 41 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomKeyStoreParam.java

@@ -0,0 +1,41 @@
+package com.usky.license.common;
+
+import de.schlichtherle.license.AbstractKeyStoreParam;
+
+import java.io.*;
+
+public class CustomKeyStoreParam extends AbstractKeyStoreParam {
+    private final String storePath;
+    private final String alias;
+    private final String storePwd;
+    private final String keyPwd;
+
+    public CustomKeyStoreParam(Class clazz, String path,
+                               String alias, String storePwd, String keyPwd) {
+        super(clazz, path);
+        this.storePath = path;
+        this.alias = alias;
+        this.storePwd = storePwd;
+        this.keyPwd = keyPwd;
+    }
+
+    @Override
+    public String getAlias() {
+        return alias;
+    }
+
+    @Override
+    public String getStorePwd() {
+        return storePwd;
+    }
+
+    @Override
+    public String getKeyPwd() {
+        return keyPwd;
+    }
+
+    @Override
+    public InputStream getStream() throws IOException {
+        return new FileInputStream(new File(storePath));
+    }
+}

+ 259 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomLicenseManager.java

@@ -0,0 +1,259 @@
+package com.usky.license.common;
+
+import com.usky.common.core.exception.BusinessException;
+import de.schlichtherle.license.*;
+import de.schlichtherle.xml.GenericCertificate;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.security.*;
+import java.util.Base64;
+import java.util.Date;
+import java.util.prefs.Preferences;
+
+@Slf4j
+public class CustomLicenseManager extends LicenseManager {
+
+    private static final int BUF_SIZE = 8 * 1024;
+
+    // BC提供者注册,确保RSA算法生效
+    static {
+        try {
+            Class<?> bcProviderClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
+            Provider bcProvider = (Provider) bcProviderClass.newInstance();
+            if (Security.getProvider("BC") == null) {
+                Security.addProvider(bcProvider);
+            }
+            log.info("成功注册Bouncy Castle加密提供者");
+        } catch (Exception e) {
+            log.debug("未引入Bouncy Castle,使用JDK内置加密提供者", e);
+        }
+    }
+
+    // 无参构造方法
+    public CustomLicenseManager() {}
+
+    // 带参构造方法
+    public CustomLicenseManager(LicenseParam param) {
+        super(param);
+    }
+
+    // 重写create方法:生成明文RSA签名证书,跳过DES加密(根源避免8字节长度限制)
+    @Override
+    protected synchronized byte[] create(LicenseContent content, LicenseNotary notary) throws Exception {
+        PrivateKey rsaPrivateKey = getRsaPrivateKey();
+        // 1. 序列化证书内容(ObjectOutputStream,方便反序列化)
+        byte[] contentBytes = serializeLicenseContent(content);
+        // 2. RSA SHA256签名
+        byte[] signatureBytes = signWithRsa(contentBytes, rsaPrivateKey);
+        String signatureBase64 = Base64.getEncoder().encodeToString(signatureBytes);
+
+        // 3. 封装证书:存储签名和原始内容Base64
+        GenericCertificate cert = new GenericCertificate();
+        cert.setSignature(signatureBase64);
+        cert.setEncoded(Base64.getEncoder().encodeToString(contentBytes));
+
+        // 4. 明文序列化证书(无加密)
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(cert);
+        oos.flush();
+        oos.close();
+
+        byte[] certBytes = baos.toByteArray();
+        log.debug("生成明文RSA签名证书,字节长度:{}", certBytes.length);
+        return certBytes;
+    }
+
+    // 新增:公共包装方法(核心!解决 final + protected 问题,供客户端直接调用)
+    public LicenseContent verifyCertificate(byte[] certBytes) throws Exception {
+        // 1. 直接反序列化证书(无需解密,明文存储)
+        ByteArrayInputStream bais = new ByteArrayInputStream(certBytes);
+        ObjectInputStream ois = new ObjectInputStream(bais);
+        GenericCertificate cert = (GenericCertificate) ois.readObject();
+        ois.close();
+
+        // 2. 获取RSA公钥(用于验签)
+        PublicKey rsaPublicKey = getRsaPublicKey();
+        // 3. 获取签名和原始内容Base64
+        String signatureBase64 = cert.getSignature();
+        String contentBase64 = cert.getEncoded();
+        if (signatureBase64 == null || contentBase64 == null) {
+            throw new BusinessException("证书格式非法,缺少签名或内容");
+        }
+
+        // 4. Base64解码
+        byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
+        byte[] contentBytes = Base64.getDecoder().decode(contentBase64);
+
+        // 5. RSA验签(防篡改)
+        if (!verifyRsaSignature(contentBytes, signatureBytes, rsaPublicKey)) {
+            throw new BusinessException("证书签名验证失败,可能已被篡改");
+        }
+
+        // 6. 反序列化证书内容
+        LicenseContent content = deserializeLicenseContent(contentBytes);
+        // 7. 硬件信息验证(防拷贝)
+        validate(content);
+
+        return content;
+    }
+
+    // 获取RSA私钥(服务端签名使用)
+    private PrivateKey getRsaPrivateKey() throws Exception {
+        LicenseParam param = getLicenseParam();
+        KeyStoreParam keyStoreParam = param.getKeyStoreParam();
+
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        InputStream is = null;
+        try {
+            is = keyStoreParam.getStream();
+            keyStore.load(is, keyStoreParam.getStorePwd().toCharArray());
+        } finally {
+            if (is != null) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    log.warn("关闭私钥库流异常,可忽略", e);
+                }
+            }
+        }
+
+        String alias = keyStoreParam.getAlias();
+        Key key = keyStore.getKey(alias, keyStoreParam.getKeyPwd().toCharArray());
+        if (!(key instanceof PrivateKey) || !"RSA".equals(key.getAlgorithm())) {
+            throw new Exception("密钥库中不是RSA私钥");
+        }
+        return (PrivateKey) key;
+    }
+
+    // 获取RSA公钥(客户端验签使用)
+    private PublicKey getRsaPublicKey() throws Exception {
+        LicenseParam param = getLicenseParam();
+        KeyStoreParam keyStoreParam = param.getKeyStoreParam();
+
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        InputStream is = null;
+        try {
+            is = keyStoreParam.getStream();
+            keyStore.load(is, keyStoreParam.getStorePwd().toCharArray());
+        } finally {
+            if (is != null) {
+                is.close();
+            }
+        }
+
+        String alias = keyStoreParam.getAlias();
+        // 公钥通过 getCertificate 获取(与私钥获取方式不同)
+        java.security.cert.Certificate cert = keyStore.getCertificate(alias);
+        PublicKey publicKey = cert.getPublicKey();
+        if (!"RSA".equals(publicKey.getAlgorithm())) {
+            throw new Exception("密钥库中不是RSA公钥");
+        }
+        return publicKey;
+    }
+
+    // RSA签名
+    private byte[] signWithRsa(byte[] content, PrivateKey privateKey) throws Exception {
+        Signature signature;
+        try {
+            signature = Signature.getInstance("SHA256withRSA", "BC");
+        } catch (NoSuchProviderException e) {
+            signature = Signature.getInstance("SHA256withRSA");
+            log.warn("未找到BC提供者,使用JDK默认RSA提供者", e);
+        }
+        signature.initSign(privateKey);
+        signature.update(content);
+        return signature.sign();
+    }
+
+    // RSA验签
+    private boolean verifyRsaSignature(byte[] content, byte[] signature, PublicKey publicKey) throws Exception {
+        Signature sig;
+        try {
+            sig = Signature.getInstance("SHA256withRSA", "BC");
+        } catch (NoSuchProviderException e) {
+            sig = Signature.getInstance("SHA256withRSA");
+        }
+        sig.initVerify(publicKey);
+        sig.update(content);
+        return sig.verify(signature);
+    }
+
+    // 序列化证书内容
+    private byte[] serializeLicenseContent(LicenseContent content) throws IOException {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+            oos.writeObject(content);
+            oos.flush();
+            return baos.toByteArray();
+        }
+    }
+
+    // 反序列化证书内容
+    private LicenseContent deserializeLicenseContent(byte[] contentBytes) throws IOException, ClassNotFoundException {
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(contentBytes);
+             ObjectInputStream ois = new ObjectInputStream(bais)) {
+            return (LicenseContent) ois.readObject();
+        }
+    }
+
+    // 硬件信息验证(不变,确保证书与机器绑定)
+    @Override
+    protected synchronized void validate(LicenseContent content) throws LicenseContentException {
+        Date now = new Date();
+        Date notBefore = content.getNotBefore();
+        Date notAfter = content.getNotAfter();
+
+        // 有效期验证
+        if (notAfter != null && now.after(notAfter)) {
+            throw new LicenseContentException("证书已过期");
+        }
+        if (notBefore != null && notAfter != null && notAfter.before(notBefore)) {
+            throw new LicenseContentException("证书生效时间晚于过期时间");
+        }
+        if (content.getConsumerType() == null) {
+            throw new LicenseContentException("证书缺少用户类型配置");
+        }
+
+        // 硬件信息匹配验证
+        LicenseCheckModel expected = (LicenseCheckModel) content.getExtra();
+        LicenseCheckModel actual = new OshiServerInfos().getServerInfos();
+        if (expected == null || actual == null) {
+            throw new LicenseContentException("无法获取硬件信息(预期/实际)");
+        }
+        // 主板序列号验证
+        if (!checkSerial(expected.getMainBoardSerial(), actual.getMainBoardSerial())) {
+            throw new LicenseContentException("主板序列号不匹配");
+        }
+        // CPU序列号验证
+        if (!checkSerial(expected.getCpuSerial(), actual.getCpuSerial())) {
+            throw new LicenseContentException("CPU序列号不匹配");
+        }
+        // IP地址验证
+        if (!checkList(expected.getIpAddress(), actual.getIpAddress())) {
+            throw new LicenseContentException("IP地址不匹配");
+        }
+        // MAC地址验证
+        if (!checkList(expected.getMacAddress(), actual.getMacAddress())) {
+            throw new LicenseContentException("MAC地址不匹配");
+        }
+    }
+
+    // 单个字段(序列号)匹配校验
+    private boolean checkSerial(String expect, String actual) {
+        return expect == null || expect.isEmpty() || expect.equals(actual);
+    }
+
+    // 列表字段(IP/MAC)匹配校验
+    private boolean checkList(java.util.List<String> expect, java.util.List<String> actual) {
+        if (expect == null || expect.isEmpty()) {
+            return true;
+        }
+        if (actual == null || actual.isEmpty()) {
+            return false;
+        }
+        // 只要有一个匹配即通过
+        return expect.stream().anyMatch(e -> actual.contains(e.trim()));
+    }
+}

+ 125 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/CustomLicenseParam.java

@@ -0,0 +1,125 @@
+package com.usky.license.common;
+
+import de.schlichtherle.license.LicenseParam;
+import de.schlichtherle.license.KeyStoreParam;
+import de.schlichtherle.license.CipherParam;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.prefs.Preferences;
+
+// 接口使用 implements,而非 extends,解决接口继承报错
+public class CustomLicenseParam implements LicenseParam {
+
+    // 自定义成员变量:存储所有配置信息
+    private final String subject;
+    private final Preferences preferences;
+    private final File privateKeyStore;
+    private final String privateKeyAlias;
+    private final char[] keyPass;
+    // 存储密码的字符串形式(解决返回值类型不兼容问题)
+    private final String keyPassStr;
+    private final File licenseFile;
+    private final KeyStoreParam keyStoreParam;
+    private final CipherParam cipherParam;
+
+    // 自定义构造方法:初始化字符串密码,解决返回值类型冲突
+    public CustomLicenseParam(String subject, Preferences preferences, File privateKeyStore,
+                              String privateKeyAlias, char[] keyPass, File licenseFile) {
+        this.subject = subject;
+        this.preferences = preferences;
+        this.privateKeyStore = privateKeyStore;
+        this.privateKeyAlias = privateKeyAlias;
+        this.keyPass = keyPass;
+        // 将 char[] 密码转为 String 类型,适配接口返回值要求
+        this.keyPassStr = new String(keyPass);
+        this.licenseFile = licenseFile;
+
+        // 1. 完全实现 KeyStoreParam 接口(补充所有抽象方法,修正返回值类型)
+        this.keyStoreParam = new KeyStoreParam() {
+            // 必须实现:KeyStoreParam 的 getAlias() 方法
+            @Override
+            public String getAlias() {
+                return privateKeyAlias;
+            }
+
+            // 必须实现:KeyStoreParam 的 getStream() 方法
+            @Override
+            public InputStream getStream() throws IOException {
+                return new FileInputStream(privateKeyStore);
+            }
+
+            // 修正:返回 String 类型,匹配接口要求
+            @Override
+            public String getStorePwd() {
+                return keyPassStr;
+            }
+
+            // 补充实现:KeyStoreParam 必须的 getKeyPwd() 方法
+            @Override
+            public String getKeyPwd() {
+                return keyPassStr;
+            }
+
+            // 普通方法:无需注解
+            public File getFile() {
+                return privateKeyStore;
+            }
+        };
+
+        // 2. 完全实现 CipherParam 接口(修正返回值类型)
+        this.cipherParam = new CipherParam() {
+            // 修正:返回 String 类型,匹配接口要求
+            @Override
+            public String getKeyPwd() {
+                return keyPassStr;
+            }
+        };
+    }
+
+    // 实现 LicenseParam 接口的必填方法(保留 @Override,接口存在这些方法)
+    @Override
+    public String getSubject() {
+        return this.subject;
+    }
+
+    @Override
+    public Preferences getPreferences() {
+        return this.preferences;
+    }
+
+    @Override
+    public KeyStoreParam getKeyStoreParam() {
+        return this.keyStoreParam;
+    }
+
+    @Override
+    public CipherParam getCipherParam() {
+        return this.cipherParam;
+    }
+
+    // 修正:移除 @Override 注解,作为普通方法存在,解决报错
+    public File getLicenseFile() {
+        return this.licenseFile;
+    }
+
+    // 普通方法:无注解,避免报错
+    public File getPrivateKeyStore() {
+        return this.privateKeyStore;
+    }
+
+    public String getPrivateKeyAlias() {
+        return this.privateKeyAlias;
+    }
+
+    public char[] getKeyPass() {
+        return this.keyPass;
+    }
+
+    // 核心:强制指定 RSA 签名算法,兜底解决 DSA 识别问题
+    public String getDigestAlgorithm() {
+        return "SHA256withRSA";
+    }
+}

+ 21 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/LicenseCheckModel.java

@@ -0,0 +1,21 @@
+package com.usky.license.common;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@ApiModel("服务器硬件校验信息")
+public class LicenseCheckModel implements Serializable {
+    @ApiModelProperty("允许通过的IP")
+    private List<String> ipAddress;
+    @ApiModelProperty("允许通过的MAC")
+    private List<String> macAddress;
+    @ApiModelProperty("允许通过的CPU序列号")
+    private String cpuSerial;
+    @ApiModelProperty("允许通过的主板序列号")
+    private String mainBoardSerial;
+}

+ 11 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/LinuxServerInfos.java

@@ -0,0 +1,11 @@
+package com.usky.license.common;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Linux 硬件信息(实际已通用,仅保留类名方便旧代码引用)
+ */
+@Slf4j
+public class LinuxServerInfos extends OshiServerInfos {
+    // 父类已实现全部逻辑,此处无需额外代码
+}

+ 104 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/OshiServerInfos.java

@@ -0,0 +1,104 @@
+package com.usky.license.common;
+
+import com.alibaba.fastjson.JSON;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.CentralProcessor;
+import oshi.hardware.ComputerSystem;
+import oshi.hardware.NetworkIF;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Data
+@Slf4j
+public class OshiServerInfos {
+
+    private static final SystemInfo SI = new SystemInfo();
+
+    /**
+     * 对外唯一入口:获取硬件信息
+     */
+    public LicenseCheckModel getServerInfos() {
+        LicenseCheckModel model = new LicenseCheckModel();
+        try {
+            model.setIpAddress(getIpAddress());
+            model.setMacAddress(getMacAddress());
+            model.setCpuSerial(getCPUSerial());
+            model.setMainBoardSerial(getMainBoardSerial());
+        } catch (Exception e) {
+            log.error("获取服务器硬件信息失败", e);
+        }
+        return model;
+    }
+
+    /* =============== IP 地址 =============== */
+    private List<String> getIpAddress() throws Exception {
+        return getLocalAllInetAddress().stream()
+                .map(InetAddress::getHostAddress)
+                .distinct()
+                .map(String::toLowerCase)
+                .collect(Collectors.toList());
+    }
+
+    /* =============== MAC 地址 =============== */
+    private List<String> getMacAddress() throws Exception {
+        return getLocalAllInetAddress().stream()
+                .map(this::getMacByInetAddress)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /* =============== CPU 序列号 =============== */
+    private String getCPUSerial() throws Exception {
+        String serial = SI.getHardware().getProcessor().getProcessorID().trim();
+        return serial.isEmpty() ? null : serial;
+    }
+
+    /* =============== 主板序列号 =============== */
+    private String getMainBoardSerial() throws Exception {
+        String serial = SI.getHardware().getComputerSystem().getSerialNumber().trim();
+        return serial.isEmpty() ? null : serial;
+    }
+
+    /* =============== 网络接口工具 =============== */
+    private List<InetAddress> getLocalAllInetAddress() throws Exception {
+        List<InetAddress> list = new ArrayList<>();
+        Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+        while (nets.hasMoreElements()) {
+            NetworkInterface iface = nets.nextElement();
+            if (!iface.isUp() || iface.isVirtual() || iface.isPointToPoint()) continue;
+            Enumeration<InetAddress> addrs = iface.getInetAddresses();
+            while (addrs.hasMoreElements()) {
+                InetAddress addr = addrs.nextElement();
+                if (!addr.isLoopbackAddress() && !addr.isLinkLocalAddress() && !addr.isMulticastAddress()) {
+                    list.add(addr);
+                }
+            }
+        }
+        return list;
+    }
+
+    private String getMacByInetAddress(InetAddress inet) {
+        try {
+            byte[] mac = NetworkInterface.getByInetAddress(inet).getHardwareAddress();
+            if (mac == null) return null;
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < mac.length; i++) {
+                if (i != 0) sb.append("-");
+                String hex = Integer.toHexString(mac[i] & 0xff);
+                if (hex.length() == 1) sb.append("0");
+                sb.append(hex);
+            }
+            return sb.toString().toUpperCase();
+        } catch (SocketException e) {
+            log.error("获取MAC失败", e);
+            return null;
+        }
+    }
+}

+ 11 - 0
base-modules/service-license/service-license-common/src/main/java/com/usky/license/common/WindowsServerInfos.java

@@ -0,0 +1,11 @@
+package com.usky.license.common;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Windows 硬件信息(实际已通用,仅保留类名方便旧代码引用)
+ */
+@Slf4j
+public class WindowsServerInfos extends OshiServerInfos {
+    // 父类已实现全部逻辑,此处无需额外代码
+}

+ 67 - 0
base-modules/service-license/service-license-server/pom.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.usky</groupId>
+        <artifactId>service-license</artifactId>
+        <version>0.0.1</version>
+    </parent>
+
+    <artifactId>service-license-server</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-license-common</artifactId>
+            <version>${project.parent.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <!-- Bouncy Castle 加密提供者 -->
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk18on</artifactId>
+            <version>1.78.1</version> <!-- 稳定版本,支持JDK8+ -->
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-boot-starter</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <!-- 必须:打出可执行 jar -->
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>   <!-- 生成可执行 fat jar -->
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 42 - 0
base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseApi.java

@@ -0,0 +1,42 @@
+package com.usky.license.server;
+
+import com.usky.license.common.LicenseCheckModel;
+import com.usky.license.common.OshiServerInfos;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.http.ResponseEntity;
+
+@Api(tags = "License 生成接口")
+@RestController
+@RequestMapping("/license")
+public class LicenseApi {
+
+    @Value("${license.licensePath}")
+    private String licensePath;
+
+    /** 全平台通用实现(OSHI 3.9.1) */
+    private final OshiServerInfos serverInfos = new OshiServerInfos();
+
+    @ApiOperation("获取服务器硬件信息")
+    @GetMapping("/serverInfo")
+    public ResponseEntity<LicenseCheckModel> getServerInfos(
+            @RequestParam(value = "osName", required = false) String osName) {
+        // 参数仅做日志/兼容,不再决定实现类
+        if (osName == null) {
+            osName = System.getProperty("os.name");
+        }
+        return ResponseEntity.ok(serverInfos.getServerInfos());
+    }
+
+    @ApiOperation("生成 License")
+    @PostMapping("/generateLicense")
+    public ResponseEntity<String> generate(@RequestBody LicenseCreatorParam param) {
+        if (param.getLicensePath() == null) {
+            param.setLicensePath(licensePath);
+        }
+        boolean ok = new LicenseCreator(param).generateLicense();
+        return ResponseEntity.ok(ok ? "证书生成成功" : "证书生成失败");
+    }
+}

+ 98 - 0
base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseCreator.java

@@ -0,0 +1,98 @@
+package com.usky.license.server;
+
+import com.usky.license.common.CustomLicenseManager;
+import com.usky.license.common.CustomLicenseParam;
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseContent;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.Calendar;
+import java.util.prefs.Preferences;
+
+@Slf4j
+public class LicenseCreator {
+
+    private LicenseCreatorParam param;
+
+    public LicenseCreator(LicenseCreatorParam param) {
+        this.param = param;
+    }
+
+    public boolean generateLicense() {
+        try {
+            // 1. 构建配置信息
+            Preferences preferences = Preferences.userRoot().node(LicenseManager.class.getName());
+            File privateKeyStoreFile = new File(param.getPrivateKeysStorePath());
+            File licenseFile = new File(param.getLicensePath());
+
+            // 校验私钥库格式(PKCS12)
+            KeyStore privateKeyStore = KeyStore.getInstance("PKCS12");
+            privateKeyStore.load(new FileInputStream(privateKeyStoreFile), param.getStorePass().toCharArray());
+            log.info("私钥库格式验证成功:PKCS12");
+
+            // 2. 使用自定义 CustomLicenseParam
+            CustomLicenseParam licenseParam = new CustomLicenseParam(
+                    param.getSubject(),
+                    preferences,
+                    privateKeyStoreFile,
+                    param.getPrivateAlias(),
+                    param.getKeyPass().toCharArray(),
+                    licenseFile
+            );
+
+            // 3. 初始化许可证管理器
+            CustomLicenseManager licenseManager = new CustomLicenseManager(licenseParam);
+            LicenseContent licenseContent = buildLicenseContent();
+
+            // 4. 生成证书
+            licenseManager.store(licenseContent, licenseFile);
+            log.info("证书生成成功,路径:{}", licenseFile.getAbsolutePath());
+            return true;
+        } catch (Exception e) {
+            log.error("证书生成失败", e);
+            return false;
+        }
+    }
+
+    // 构建证书内容:新增时区校正,不改动LicenseCreatorParam
+    private LicenseContent buildLicenseContent() {
+        LicenseContent content = new LicenseContent();
+        content.setSubject(param.getSubject());
+
+        // 核心:校正时区(将参数中的时间转换为东八区时间,抵消+8小时偏差)
+        Date correctIssuedTime = correctTimezoneToAsiaShanghai(param.getIssuedTime());
+        Date correctExpiryTime = correctTimezoneToAsiaShanghai(param.getExpiryTime());
+
+        // 设置校正后的时间
+        content.setNotBefore(correctIssuedTime);
+        content.setNotAfter(correctExpiryTime);
+        content.setConsumerType(param.getConsumerType());
+        content.setConsumerAmount(param.getConsumerAmount());
+        content.setExtra(param.getLicenseCheckModel());
+        return content;
+    }
+
+    // 时区校正工具方法:将传入的Date转换为东八区对应时间
+    private Date correctTimezoneToAsiaShanghai(Date sourceDate) {
+        if (sourceDate == null) {
+            return null;
+        }
+        // 1. 获取东八区时区和默认时区
+        TimeZone asiaShanghai = TimeZone.getTimeZone("Asia/Shanghai");
+        TimeZone defaultTimeZone = TimeZone.getDefault();
+
+        // 2. 计算两个时区的偏移量(东八区与默认时区的时间差,单位:毫秒)
+        int offset = asiaShanghai.getRawOffset() - defaultTimeZone.getRawOffset();
+        // 3. 校正时间
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(sourceDate);
+        calendar.add(Calendar.MILLISECOND, offset);
+
+        return calendar.getTime();
+    }
+}

+ 38 - 0
base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseCreatorParam.java

@@ -0,0 +1,38 @@
+package com.usky.license.server;
+
+import com.usky.license.common.LicenseCheckModel;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+@Data
+@ApiModel("证书创建参数")
+public class LicenseCreatorParam {
+    @ApiModelProperty("subject")
+    private String subject;
+    @ApiModelProperty("私钥 alias")
+    private String privateAlias;
+    @ApiModelProperty("私钥密码")
+    private String keyPass;
+    @ApiModelProperty("密钥库密码")
+    private String storePass;
+    @ApiModelProperty("license 输出路径")
+    private String licensePath;
+    @ApiModelProperty("私钥库路径")
+    private String privateKeysStorePath;
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date issuedTime;
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date expiryTime;
+    @ApiModelProperty("用户类型")
+    private String consumerType = "User";
+    @ApiModelProperty("用户数量")
+    private Integer consumerAmount = 1;
+    @ApiModelProperty("描述")
+    private String description;
+    @ApiModelProperty("硬件校验信息")
+    private LicenseCheckModel licenseCheckModel;
+}

+ 17 - 0
base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/LicenseServerApp.java

@@ -0,0 +1,17 @@
+package com.usky.license.server;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+// 导入数据源自动配置排除类
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+
+/**
+ * 排除数据源自动配置,解决无数据库配置时的启动失败问题
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+public class LicenseServerApp {
+
+    public static void main(String[] args) {
+        SpringApplication.run(LicenseServerApp.class, args);
+    }
+}

+ 12 - 0
base-modules/service-license/service-license-server/src/main/java/com/usky/license/server/ListProviders.java

@@ -0,0 +1,12 @@
+package com.usky.license.server;
+
+import java.security.Provider;
+import java.security.Security;
+
+public class ListProviders {
+    public static void main(String[] args) {
+        for (Provider provider : Security.getProviders()) {
+            System.out.println(provider.getName());
+        }
+    }
+}

+ 24 - 0
base-modules/service-license/service-license-server/src/main/resources/bootstrap.yml

@@ -0,0 +1,24 @@
+# Tomcat
+server:
+  port: 19998
+# Spring
+spring: 
+  application:
+    # 应用名称
+    name: service-license-server
+  profiles:
+    # 环境配置
+    active: dev
+  cloud:
+    nacos:
+      discovery:
+        # 服务注册地址
+        server-addr: usky-cloud-nacos:8848
+      config:
+        # 配置中心地址
+        server-addr: usky-cloud-nacos:8848
+        # 配置文件格式
+        file-extension: yml
+        # 共享配置
+        shared-configs:
+          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

+ 94 - 0
base-modules/service-license/service-license-server/src/main/resources/logback.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+    <property name="log.path" value="/var/log/uskycloud/service-license" />
+    <!-- 日志输出格式 -->
+    <property name="log.pattern" value="%d{MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{26}:%line: %msg%n" />
+    <!--    	<property name="log.pattern" value="%gray(%d{MM-dd HH:mm:ss.SSS}) %highlight(%-5level) &#45;&#45; [%gray(%thread)] %cyan(%logger{26}:%line): %msg%n" />-->
+
+
+    <property name="SQL_PACKAGE" value="com.usky.license.mapper"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="file_sql" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sql.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sql.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>3</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 系统日志输出 -->
+    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>3</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 系统模块日志级别控制  -->
+    <!--	<logger name="com.usky" level="info" />-->
+    <!-- Spring日志级别控制  -->
+    <!--	<logger name="org.springframework" level="warn" />-->
+
+    <logger name="${SQL_PACKAGE}" additivity="false" level="debug">
+        <appender-ref ref="console"/>
+        <appender-ref ref="file_sql"/>
+    </logger>
+
+    <!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+        <appender-ref ref="console" />
+    </root>
+</configuration>

+ 6 - 1
base-modules/service-system/service-system-biz/pom.xml

@@ -76,7 +76,12 @@
             <artifactId>pjl-comp-filter</artifactId>
             <version>1.7</version>
         </dependency>
-
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-license-client</artifactId>
+            <version>0.0.1</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 22 - 0
base-modules/service-system/service-system-biz/src/main/java/com/usky/system/service/config/WebConfig.java

@@ -0,0 +1,22 @@
+package com.usky.system.service.config;
+
+import com.usky.license.client.LicenseCheckInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Autowired
+    private LicenseCheckInterceptor licenseCheckInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 排除掉 swagger、actuator 等
+        registry.addInterceptor(licenseCheckInterceptor)
+                .excludePathPatterns("/error", "/swagger-ui/**", "/v3/api-docs/**",
+                        "/actuator/**", "/license/**");
+    }
+}