浏览代码

增加证书安装校验

wukai 1 年之前
父节点
当前提交
60754454af

+ 7 - 3
doc-admin/src/main/resources/application-test.yml

@@ -14,6 +14,12 @@ ruoyi:
   captchaType: math
   # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
   profile: /opt/doc/uploadPath
+
+#证书相关配置
+license:
+  subject: doc_test
+  licensePath: D:\SYSTEM\Desktop\temp\license.lic
+
 # 开发环境配置
 server:
   # 服务器的HTTP端口,默认为8080
@@ -132,8 +138,6 @@ spring:
   messages:
     # 国际化资源文件路径
     basename: i18n/messages
-  profiles:
-    active: dev
   # 文件上传
   servlet:
     multipart:
@@ -199,4 +203,4 @@ user:
     # 密码最大错误次数
     maxRetryCount: 5
     # 密码锁定时间(默认10分钟)
-    lockTime: 10
+    lockTime: 10

+ 6 - 2
doc-admin/src/main/resources/application.yml

@@ -12,7 +12,11 @@ ruoyi:
   addressEnabled: false
   # 验证码类型 math 数字计算 char 字符验证
   captchaType: math
-# 公共配置
+#证书相关配置
+license:
+  subject: doc_test
+  licensePath: D:\SYSTEM\Desktop\temp\license.lic
+# 服务配置
 server:
   servlet:
     # 应用的访问路径
@@ -97,4 +101,4 @@ xss:
   # 排除链接(多个用逗号分隔)
   excludes: /system/notice
   # 匹配链接
-  urlPatterns: /system/*,/monitor/*,/tool/*
+  urlPatterns: /system/*,/monitor/*,/tool/*

二进制
doc-admin/src/main/resources/license/publicKeys.keystore


+ 2 - 0
doc-common/src/main/java/com/doc/common/core/domain/entity/SysUser.java

@@ -1,6 +1,7 @@
 package com.doc.common.core.domain.entity;
 
 import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.doc.common.annotation.Excel;
 import com.doc.common.annotation.Excel.ColumnType;
 import com.doc.common.annotation.Excel.Type;
@@ -28,6 +29,7 @@ public class SysUser extends BaseEntity {
      * 用户ID
      */
     @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
+    @TableId
     private Long userId;
 
     /**

+ 7 - 3
doc-framework/pom.xml

@@ -52,7 +52,12 @@
             <groupId>com.github.oshi</groupId>
             <artifactId>oshi-core</artifactId>
         </dependency>
-
+        <!--生成license-->
+        <dependency>
+            <groupId>de.schlichtherle.truelicense</groupId>
+            <artifactId>truelicense-core</artifactId>
+            <version>1.33</version>
+        </dependency>
         <!-- 系统模块-->
         <dependency>
             <groupId>com.jjt</groupId>
@@ -64,5 +69,4 @@
         </dependency>
 
     </dependencies>
-
-</project>
+</project>

+ 274 - 0
doc-framework/src/main/java/com/doc/framework/license/CustomLicenseManager.java

@@ -0,0 +1,274 @@
+package com.doc.framework.license;
+
+import com.doc.framework.license.conf.AbstractServerInfos;
+import com.doc.framework.license.conf.LinuxServerInfos;
+import com.doc.framework.license.conf.WindowsServerInfos;
+import com.doc.framework.license.domain.LicenseCheckModel;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import de.schlichtherle.license.*;
+import de.schlichtherle.xml.GenericCertificate;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.beans.XMLDecoder;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义证书管理
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+public class CustomLicenseManager extends LicenseManager {
+
+    private static final Logger logger = LogManager.getLogger(CustomLicenseManager.class);
+
+    /**
+     * XML编码
+     */
+    private static final String XML_CHARSET = "UTF-8";
+    /**
+     * 默认BUFF SIZE
+     */
+    private static final int DEFAULT_BUFF_SIZE = 8 * 1024;
+
+
+    public CustomLicenseManager(LicenseParam param) {
+        super(param);
+    }
+
+    /**
+     * 复写create方法
+     *
+     * @return byte[]
+     */
+    @Override
+    protected synchronized byte[] create(
+            LicenseContent content,
+            LicenseNotary notary)
+            throws Exception {
+        initialize(content);
+        this.validateCreate(content);
+        final GenericCertificate certificate = notary.sign(content);
+        return getPrivacyGuard().cert2key(certificate);
+    }
+
+    /**
+     * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+     *
+     * @return de.schlichtherle.license.LicenseContent
+     */
+    @Override
+    protected synchronized LicenseContent install(
+            final byte[] key,
+            final LicenseNotary notary)
+            throws Exception {
+        final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
+
+        notary.verify(certificate);
+        final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded());
+        this.validate(content);
+        setLicenseKey(key);
+        setCertificate(certificate);
+
+        return content;
+    }
+
+    /**
+     * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息
+     *
+     * @return de.schlichtherle.license.LicenseContent
+     */
+    @Override
+    protected synchronized LicenseContent verify(final LicenseNotary notary)
+            throws Exception {
+        GenericCertificate certificate = getCertificate();
+
+        // Load license key from preferences,
+        final byte[] key = getLicenseKey();
+        if (null == key) {
+            throw new NoLicenseInstalledException(getLicenseParam().getSubject());
+        }
+
+        certificate = getPrivacyGuard().key2cert(key);
+        notary.verify(certificate);
+        final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded());
+        this.validate(content);
+        setCertificate(certificate);
+
+        return content;
+    }
+
+    /**
+     * 校验生成证书的参数信息
+     *
+     * @param content 证书正文
+     */
+    protected synchronized void validateCreate(final LicenseContent content)
+            throws LicenseContentException {
+        final Date now = new Date();
+        final Date notBefore = content.getNotBefore();
+        final Date notAfter = content.getNotAfter();
+        if (null != notAfter && now.after(notAfter)) {
+            throw new LicenseContentException("证书失效时间不能早于当前时间");
+        }
+        if (null != notBefore && null != notAfter && notAfter.before(notBefore)) {
+            throw new LicenseContentException("证书生效时间不能晚于证书失效时间");
+        }
+        final String consumerType = content.getConsumerType();
+        if (null == consumerType) {
+            throw new LicenseContentException("用户类型不能为空");
+        }
+    }
+
+
+    /**
+     * 复写validate方法,增加IP地址、Mac地址等其他信息校验
+     *
+     * @param content LicenseContent
+     */
+    @Override
+    protected synchronized void validate(final LicenseContent content)
+            throws LicenseContentException {
+        //1. 首先调用父类的validate方法
+        super.validate(content);
+
+        //2. 然后校验自定义的License参数
+        //License中可被允许的参数信息
+        ObjectMapper objectMapper = new ObjectMapper();
+        LicenseCheckModel expectedCheckModel = null;
+        try {
+            expectedCheckModel = objectMapper.readValue((String) content.getExtra(), LicenseCheckModel.class);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+        //当前服务器真实的参数信息
+        LicenseCheckModel serverCheckModel = getServerInfos();
+
+        if (expectedCheckModel != null && serverCheckModel != null) {
+            //校验IP地址
+            if (!checkIpAddress(expectedCheckModel.getIpAddress(), serverCheckModel.getIpAddress())) {
+                throw new LicenseContentException("当前服务器的IP没在授权范围内");
+            }
+
+            //校验Mac地址
+            if (!checkIpAddress(expectedCheckModel.getMacAddress(), serverCheckModel.getMacAddress())) {
+                throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");
+            }
+
+            //校验主板序列号
+            if (!checkSerial(expectedCheckModel.getMainBoardSerial(), serverCheckModel.getMainBoardSerial())) {
+                throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");
+            }
+
+            //校验CPU序列号
+            if (!checkSerial(expectedCheckModel.getCpuSerial(), serverCheckModel.getCpuSerial())) {
+                throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");
+            }
+        } else {
+            throw new LicenseContentException("不能获取服务器硬件信息");
+        }
+    }
+
+
+    /**
+     * 重写XMLDecoder解析XML
+     *
+     * @param encoded XML类型字符串
+     * @return java.lang.Object
+     */
+    private Object load(String encoded) {
+        BufferedInputStream inputStream = null;
+        XMLDecoder decoder = null;
+        try {
+            inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));
+
+            decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFF_SIZE), null, null);
+
+            return decoder.readObject();
+        } catch (UnsupportedEncodingException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (decoder != null) {
+                    decoder.close();
+                }
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+            } catch (Exception e) {
+                logger.error("XMLDecoder解析XML失败", e);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 获取当前服务器需要额外校验的License参数
+     *
+     * @return demo.LicenseCheckModel
+     */
+    private LicenseCheckModel getServerInfos() {
+        //操作系统类型
+        String osName = System.getProperty("os.name").toLowerCase();
+        AbstractServerInfos abstractServerInfos = null;
+
+        //根据不同操作系统类型选择不同的数据获取方法
+        if (osName.startsWith("windows")) {
+            abstractServerInfos = new WindowsServerInfos();
+        } else if (osName.startsWith("linux")) {
+            abstractServerInfos = new LinuxServerInfos();
+        } else {//其他服务器类型
+            abstractServerInfos = new LinuxServerInfos();
+        }
+
+        return abstractServerInfos.getServerInfos();
+    }
+
+    /**
+     * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/>
+     * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
+     *
+     * @return boolean
+     */
+    private boolean checkIpAddress(List<String> expectedList, List<String> serverList) {
+        if (expectedList != null && expectedList.size() > 0) {
+            if (serverList != null && serverList.size() > 0) {
+                for (String expected : expectedList) {
+                    if (serverList.contains(expected.trim())) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
+     *
+     * @return boolean
+     */
+    private boolean checkSerial(String expectedSerial, String serverSerial) {
+        if (StringUtils.isNotBlank(expectedSerial)) {
+            if (StringUtils.isNotBlank(serverSerial)) {
+                return expectedSerial.equals(serverSerial);
+            }
+
+            return false;
+        } else {
+            return true;
+        }
+    }
+}

+ 122 - 0
doc-framework/src/main/java/com/doc/framework/license/LicenseCheckListener.java

@@ -0,0 +1,122 @@
+package com.doc.framework.license;
+
+import com.alibaba.fastjson2.JSON;
+import com.doc.framework.license.conf.AbstractServerInfos;
+import com.doc.framework.license.conf.LinuxServerInfos;
+import com.doc.framework.license.conf.WindowsServerInfos;
+import com.doc.framework.license.domain.LicenseVerifyParam;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.util.Objects;
+
+/**
+ * 授权许可验证监听器
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+@Component
+public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {
+
+    private static final Logger logger = LoggerFactory.getLogger(LicenseCheckListener.class);
+
+    /**
+     * 证书subject
+     */
+    @Value("${license.subject}")
+    private String subject;
+
+    /**
+     * 证书生成路径
+     */
+    @Value("${license.licensePath}")
+    private String licensePath;
+
+    public LicenseCheckListener() {
+    }
+
+    @Override
+    public void onApplicationEvent(ContextRefreshedEvent event) {
+        ApplicationContext context = event.getApplicationContext().getParent();
+        if (context == null) {
+            if (StringUtils.isNotBlank(licensePath)) {
+                try {
+                    logger.info("++++++++ 开始安装证书 ++++++++");
+
+                    //公钥存放位置
+                    File file = new File(Objects.requireNonNull(this.getClass().getResource("/")).getPath());
+                    String publicKeysStorePath = file.getAbsolutePath() + File.separator + "license/publicKeys.keystore";
+
+                    LicenseVerifyParam param = new LicenseVerifyParam();
+                    param.setSubject(subject);
+                    param.setLicensePath(licensePath);
+                    param.setPublicAlias("publicKey");
+                    param.setStorePass("jjt@2023");
+                    param.setPublicKeysStorePath(publicKeysStorePath);
+
+                    LicenseVerify licenseVerify = new LicenseVerify();
+                    //安装证书
+                    licenseVerify.install(param);
+                } catch (Exception e) {
+                    logger.error(e.getMessage());
+                    close();
+                }
+
+                logger.info("++++++++ 证书安装结束 ++++++++");
+            } else {
+                close();
+            }
+        }
+    }
+
+    @Resource
+    private ApplicationContext context;
+
+    /**
+     * 关闭应用
+     */
+    private void close() {
+        try {
+
+            //操作系统类型
+            String osName = System.getProperty("os.name");
+            osName = osName.toLowerCase();
+
+            AbstractServerInfos infos;
+            String windows = "windows";
+            String linux = "linux";
+            //根据不同操作系统类型选择不同的数据获取方法
+            if (osName.startsWith(windows)) {
+                infos = new WindowsServerInfos();
+            } else if (osName.startsWith(linux)) {
+                infos = new LinuxServerInfos();
+            } else {//其他服务器类型
+                infos = new LinuxServerInfos();
+            }
+
+            logger.error("==============证书安装失败===================");
+            logger.error("========请将以下信息提供给供应商===============");
+            logger.error("===========================================");
+            logger.error("===========================================");
+            logger.error("===========================================");
+            logger.error(JSON.toJSONString(infos.getServerInfos()));
+            logger.error("===========================================");
+            logger.error("===========================================");
+            logger.error("===========================================");
+            ((ConfigurableApplicationContext) context).close();
+        } catch (Exception ignored) {
+        }
+
+    }
+}

+ 29 - 0
doc-framework/src/main/java/com/doc/framework/license/LicenseManagerHolder.java

@@ -0,0 +1,29 @@
+package com.doc.framework.license;
+
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+
+/**
+ * 许可管理,实现
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+
+public class LicenseManagerHolder {
+
+    private static volatile LicenseManager LICENSE_MANAGER;
+
+    public static LicenseManager getInstance(LicenseParam param) {
+        if (LICENSE_MANAGER == null) {
+            synchronized (LicenseManagerHolder.class) {
+                if (LICENSE_MANAGER == null) {
+                    LICENSE_MANAGER = new CustomLicenseManager(param);
+                }
+            }
+        }
+
+        return LICENSE_MANAGER;
+    }
+}

+ 90 - 0
doc-framework/src/main/java/com/doc/framework/license/LicenseVerify.java

@@ -0,0 +1,90 @@
+package com.doc.framework.license;
+
+import com.doc.framework.license.domain.CustomKeyStoreParam;
+import com.doc.framework.license.domain.LicenseVerifyParam;
+import de.schlichtherle.license.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.prefs.Preferences;
+
+/**
+ * 证书安装验证
+ *
+ * @author wukai
+ */
+
+public class LicenseVerify {
+
+    private static final Logger logger = LoggerFactory.getLogger(LicenseVerify.class);
+
+    /**
+     * 安装License证书
+     */
+    public synchronized LicenseContent install(LicenseVerifyParam param) throws Exception {
+        LicenseContent result;
+        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        //1. 安装证书
+        try {
+            LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
+            licenseManager.uninstall();
+
+            result = licenseManager.install(new File(param.getLicensePath()));
+            logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
+        } catch (Exception e) {
+//            logger.error("证书安装失败!", e);
+            throw e;
+        }
+
+        return result;
+    }
+
+    /**
+     * 校验License证书
+     *
+     * @return boolean
+     */
+    public boolean verify() {
+        LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
+        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        //2. 校验证书
+        try {
+            LicenseContent licenseContent = licenseManager.verify();
+
+            logger.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}", format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter())));
+            return true;
+        } catch (Exception e) {
+            logger.error("证书校验失败!", e);
+            return false;
+        }
+    }
+
+    /**
+     * 初始化证书生成参数
+     *
+     * @param param License校验类需要的参数
+     * @return de.schlichtherle.license.LicenseParam
+     */
+    private LicenseParam initLicenseParam(LicenseVerifyParam param) {
+        Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
+
+        CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
+
+        KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
+                , param.getPublicKeysStorePath()
+                , param.getPublicAlias()
+                , param.getStorePass()
+                , null);
+
+        return new DefaultLicenseParam(param.getSubject()
+                , preferences
+                , publicStoreParam
+                , cipherParam);
+    }
+}

+ 214 - 0
doc-framework/src/main/java/com/doc/framework/license/conf/AbstractServerInfos.java

@@ -0,0 +1,214 @@
+package com.doc.framework.license.conf;
+
+import com.doc.framework.license.domain.LicenseCheckModel;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+
+/**
+ * 获取服务器信息抽象类
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+public abstract class AbstractServerInfos {
+
+    private static final Logger logger = LogManager.getLogger(AbstractServerInfos.class);
+
+    /**
+     * 组装需要额外校验的License参数
+     *
+     * @return entity.vo.LicenseCheckModel
+     */
+    public LicenseCheckModel getServerInfos() {
+        LicenseCheckModel result = new LicenseCheckModel();
+
+        try {
+            result.setIpAddress(this.getIpAddress());
+            result.setMacAddress(this.getMacAddress());
+            result.setCpuSerial(this.getCpuSerial());
+            result.setMainBoardSerial(this.getMainBoardSerial());
+        } catch (Exception e) {
+            logger.error("获取服务器硬件信息失败", e);
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取IP地址
+     *
+     * @return ip地址列表
+     */
+    protected List<String> getIpAddress() throws Exception {
+        List<String> result = null;
+
+        //获取所有网络接口
+        List<InetAddress> inetAddresses = getLocalAllInetAddress();
+
+        if (inetAddresses != null && inetAddresses.size() > 0) {
+            result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取Mac地址
+     *
+     * @return mac地址列表
+     */
+    protected List<String> getMacAddress() throws Exception {
+        List<String> result = null;
+
+        //1. 获取所有网络接口
+        List<InetAddress> inetAddresses = getLocalAllInetAddress();
+
+        if (inetAddresses != null && inetAddresses.size() > 0) {
+            //2. 获取所有网络接口的Mac地址
+            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+        }
+
+        return result;
+    }
+
+    ;
+
+    /**
+     * 获取CPU序列号
+     *
+     * @return 序列号
+     * @throws Exception 异常
+     */
+    protected abstract String getCpuSerial() throws Exception;
+
+    /**
+     * 获取主板序列号
+     *
+     * @return 序列号
+     * @throws Exception 异常
+     */
+    protected abstract String getMainBoardSerial() throws Exception;
+
+    /**
+     * 获取当前服务器所有符合条件的InetAddress
+     *
+     * @return 列表
+     */
+    protected List<InetAddress> getLocalAllInetAddress() throws Exception {
+        List<InetAddress> result = new ArrayList<>(4);
+        // 遍历所有的网络接口
+        for (Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
+            NetworkInterface ni = networkInterfaces.nextElement();
+            // 在所有的接口下再遍历IP
+            for (Enumeration<InetAddress> inetAddresses = ni.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
+                InetAddress addr = inetAddresses.nextElement();
+
+                //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
+                if (!addr.isLoopbackAddress()
+                        /*&& !inetAddr.isSiteLocalAddress()*/
+                        && !addr.isLinkLocalAddress() && !addr.isMulticastAddress()) {
+                    result.add(addr);
+                }
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取某个网络接口的Mac地址
+     *
+     * @return mac
+     */
+    protected String getMacByInetAddress(InetAddress inetAddr) {
+        try {
+            byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
+            StringBuilder stringBuffer = new StringBuilder();
+
+            for (int i = 0; i < mac.length; i++) {
+                if (i != 0) {
+                    stringBuffer.append("-");
+                }
+
+                //将十六进制byte转化为字符串
+                String temp = Integer.toHexString(mac[i] & 0xff);
+                if (temp.length() == 1) {
+                    stringBuffer.append("0").append(temp);
+                } else {
+                    stringBuffer.append(temp);
+                }
+            }
+
+            return stringBuffer.toString().toUpperCase();
+        } catch (SocketException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    /**
+     * 执行shell脚本获取序列号
+     *
+     * @param shell shell 脚本
+     * @return 序列号
+     */
+    protected String execShell(String[] shell) throws Exception {
+        //序列号
+        String number = "";
+
+        //使用dmidecode命令获取主板序列号
+        Process process = Runtime.getRuntime().exec(shell);
+        process.getOutputStream().close();
+
+        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+
+        String line = reader.readLine().trim();
+        if (StringUtils.isNotBlank(line)) {
+            number = line;
+        }
+
+        reader.close();
+        return number;
+    }
+
+    /**
+     * 执行windows命令获取序列号
+     *
+     * @param cmd 命令
+     * @return 序列号
+     */
+    protected String execCommand(String cmd) throws Exception {
+        //序列号
+        String number = "";
+
+        Process process = Runtime.getRuntime().exec(cmd);
+        process.getOutputStream().close();
+        try (Scanner scanner = new Scanner(process.getInputStream())) {
+
+            if (scanner.hasNext()) {
+                //第一行舍去,第二行才显示的序列号
+                scanner.next();
+            }
+
+            if (scanner.hasNext()) {
+                number = scanner.next().trim();
+            }
+        }
+
+        return number;
+    }
+}

+ 27 - 0
doc-framework/src/main/java/com/doc/framework/license/conf/LinuxServerInfos.java

@@ -0,0 +1,27 @@
+package com.doc.framework.license.conf;
+
+/**
+ * 获取linux服务器信息
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+public class LinuxServerInfos extends AbstractServerInfos {
+
+    @Override
+    protected String getCpuSerial() throws Exception {
+        //使用dmidecode命令获取CPU序列号
+        String[] shell = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
+
+        return execShell(shell);
+    }
+
+    @Override
+    protected String getMainBoardSerial() throws Exception {
+        //使用dmidecode命令获取主板序列号
+        String[] shell = {"/bin/bash", "-c", "dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
+
+        return execShell(shell);
+    }
+}

+ 21 - 0
doc-framework/src/main/java/com/doc/framework/license/conf/WindowsServerInfos.java

@@ -0,0 +1,21 @@
+package com.doc.framework.license.conf;
+
+/**
+ * 获取windows服务器信息
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+public class WindowsServerInfos extends AbstractServerInfos {
+    @Override
+    protected String getCpuSerial() throws Exception {
+        //使用WMIC获取CPU序列号
+        return execCommand("wmic cpu get processorid");
+    }
+
+    @Override
+    protected String getMainBoardSerial() throws Exception {
+        //使用WMIC获取主板序列号
+        return execCommand("wmic baseboard get serialnumber");
+    }
+}

+ 61 - 0
doc-framework/src/main/java/com/doc/framework/license/domain/CustomKeyStoreParam.java

@@ -0,0 +1,61 @@
+package com.doc.framework.license.domain;
+
+import de.schlichtherle.license.AbstractKeyStoreParam;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/**
+ * 自定义存储参数
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+
+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 resource, String alias, String storePwd, String keyPwd) {
+        super(clazz, resource);
+        this.storePath = resource;
+        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;
+    }
+
+    /**
+     * 复写de.schlichtherle.license.AbstractKeyStoreParam的getStream()方法<br/>
+     * 用于将公私钥存储文件存放到其他磁盘位置而不是项目中
+     *
+     * @return java.io.InputStream
+     */
+    @Override
+    public InputStream getStream() throws IOException {
+        return Files.newInputStream(new File(storePath).toPath());
+    }
+}

+ 37 - 0
doc-framework/src/main/java/com/doc/framework/license/domain/LicenseCheckModel.java

@@ -0,0 +1,37 @@
+package com.doc.framework.license.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 服务器信息实体
+ *
+ * @author wukai
+ * @date 2023-12-29
+ */
+@Data
+public class LicenseCheckModel implements Serializable {
+
+    /**
+     * 可被允许的IP地址
+     */
+    private List<String> ipAddress;
+
+    /**
+     * 可被允许的MAC地址
+     */
+    private List<String> macAddress;
+
+    /**
+     * 可被允许的CPU序列号
+     */
+    private String cpuSerial;
+
+    /**
+     * 可被允许的主板序列号
+     */
+    private String mainBoardSerial;
+
+}

+ 37 - 0
doc-framework/src/main/java/com/doc/framework/license/domain/LicenseVerifyParam.java

@@ -0,0 +1,37 @@
+package com.doc.framework.license.domain;
+
+import lombok.Data;
+
+/**
+ * license验证参数
+ *
+ * @author wukai
+ */
+@Data
+public class LicenseVerifyParam {
+
+    /**
+     * 证书subject
+     */
+    private String subject;
+
+    /**
+     * 公钥别称
+     */
+    private String publicAlias;
+
+    /**
+     * 访问公钥库的密码
+     */
+    private String storePass;
+
+    /**
+     * 证书生成路径
+     */
+    private String licensePath;
+
+    /**
+     * 密钥库存储路径
+     */
+    private String publicKeysStorePath;
+}