diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/license/LicenseController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/license/LicenseController.java
new file mode 100644
index 0000000..3c9cd7c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/license/LicenseController.java
@@ -0,0 +1,52 @@
+package com.ruoyi.web.controller.license;
+
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.license.domain.LicenseCreatorParam;
+import com.ruoyi.license.service.AbstractServerInfos;
+import com.ruoyi.license.service.impl.LinuxServerInfos;
+import com.ruoyi.license.service.impl.WindowsServerInfos;
+import com.ruoyi.license.utils.LicenseCreator;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+
+/**
+ * 用于生成证书文件,不能放在给客户部署的代码里
+ */
+@RestController
+@RequestMapping("/license")
+public class LicenseController {
+
+ /**
+ * 获取服务器硬件信息
+ */
+ @GetMapping(value = "/getInfo")
+ public AjaxResult getInfo(@RequestParam(value = "osName",required = false) String osName) {
+ //操作系统类型
+ if(StringUtils.isBlank(osName)){
+ osName = System.getProperty("os.name");
+ }
+ osName = osName.toLowerCase();
+ AbstractServerInfos abstractServerInfos = null;
+ //根据不同操作系统类型选择不同的数据获取方法
+ if (osName.startsWith("windows")) {
+ abstractServerInfos = new WindowsServerInfos();
+ } else if (osName.startsWith("linux")) {
+ abstractServerInfos = new LinuxServerInfos();
+ }else{//其他服务器类型
+ abstractServerInfos = new LinuxServerInfos();
+ }
+ return AjaxResult.success(abstractServerInfos.getServerInfos());
+ }
+
+ @PostMapping(value = "/generate")
+ public AjaxResult generate(@RequestBody LicenseCreatorParam param) {
+ LicenseCreator licenseCreator = new LicenseCreator(param);
+ boolean result = licenseCreator.generateLicense();
+ if(result){
+ return AjaxResult.success();
+ }else{
+ return AjaxResult.error("证书文件生成失败");
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-dev.yml
similarity index 100%
rename from ruoyi-admin/src/main/resources/application-druid.yml
rename to ruoyi-admin/src/main/resources/application-dev.yml
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 5143cc6..bda0206 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -59,7 +59,7 @@ spring:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
profiles:
- active: druid
+ active: dev
# 文件上传
servlet:
multipart:
@@ -166,3 +166,11 @@ aj:
jasypt:
encryptor:
password: BUSINESS
+
+license:
+ subject: license_demo #证书subject,建议用统一平台的AppCode
+ publicAlias: publicCert #密钥别称
+ storePass: public_password1234 #访问秘钥库的密码,建议用统一平台的client_security
+ licensePath: license.lic #证书存储路径
+ publicKeysStorePath: D:/license/publicCerts.keystore #密钥库存储路径
+ node_env: dev
diff --git a/ruoyi-admin/src/main/resources/license.lic b/ruoyi-admin/src/main/resources/license.lic
new file mode 100644
index 0000000..d87309b
Binary files /dev/null and b/ruoyi-admin/src/main/resources/license.lic differ
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 7349c3a..68d6a6c 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -111,6 +111,11 @@
hutool-all
+
+ org.projectlombok
+ lombok
+
+
diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml
index febef4c..164ccb1 100644
--- a/ruoyi-framework/pom.xml
+++ b/ruoyi-framework/pom.xml
@@ -83,5 +83,12 @@
captcha-spring-boot-starter
1.2.7
+
+
+
+ de.schlichtherle.truelicense
+ truelicense-core
+ 1.33
+
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java
new file mode 100644
index 0000000..e8e622e
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/InterceptorConfig.java
@@ -0,0 +1,37 @@
+package com.ruoyi.framework.config;
+
+import com.ruoyi.license.interceptor.LicenseCheckInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.Resource;
+
+/**
+ * packageName com.ruoyi.framework.config
+ *
+ * @author wangxy
+ * @version JDK 8
+ * @className InterceptorConfig
+ * @date 2024/6/13
+ * @description 拦截器配置
+ */
+
+@Configuration
+public class InterceptorConfig implements WebMvcConfigurer {
+
+ @Resource
+ private LicenseCheckInterceptor licenseCheckInterceptor;
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+
+ //添加要拦截的url
+ registry.addInterceptor(licenseCheckInterceptor)
+ // 拦截的路径
+ .addPathPatterns("/login");
+ // 放行的路径
+// .excludePathPatterns("/admin/login");
+ }
+
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
index 68baa33..40a6c13 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
@@ -302,6 +302,8 @@ public class ShiroConfig
filterChainDefinitionMap.put("/login", "anon,captchaValidate");
// 注册相关
filterChainDefinitionMap.put("/register", "anon,captchaValidate");
+
+ filterChainDefinitionMap.put("/license/**", "anon,captchaValidate");
// 系统权限列表
// filterChainDefinitionMap.putAll(SpringUtils.getBean(IMenuService.class).selectPermsAll());
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/domain/CustomKeyStoreParam.java b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/CustomKeyStoreParam.java
new file mode 100644
index 0000000..8d11368
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/CustomKeyStoreParam.java
@@ -0,0 +1,59 @@
+package com.ruoyi.license.domain;
+
+import de.schlichtherle.license.AbstractKeyStoreParam;
+
+import java.io.*;
+
+/**
+ * 自定义KeyStoreParam
+ */
+public class CustomKeyStoreParam extends AbstractKeyStoreParam {
+ private static final long serialVersionUID = 1L;
+
+ /** 公钥/私钥在磁盘上的存储路径 */
+ private String storePath;
+ private String alias;
+ private String storePwd;
+ private 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;
+ }
+
+ /**
+ * 重写getStream()方法
+ * @return
+ * @throws IOException
+ */
+ @Override
+ public InputStream getStream() throws IOException {
+ // 本地开发环境,License生成
+ final InputStream in = new FileInputStream(new File(storePath));
+ // 线上环境,直接用这个
+ // InputStream in = new ClassPathResource(storePath).getStream();
+ if (null == in){
+ throw new FileNotFoundException(storePath);
+ }
+ return in;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCheckModel.java b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCheckModel.java
new file mode 100644
index 0000000..a3de0b3
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCheckModel.java
@@ -0,0 +1,44 @@
+package com.ruoyi.license.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * License校验实体类
+ * @author 13560
+ */
+@Data
+public class LicenseCheckModel implements Serializable {
+ private static final long serialVersionUID = 1L;
+ /**
+ * 可被允许的IP地址
+ */
+ private List ipAddress;
+
+ /**
+ * 可被允许的MAC地址
+ */
+ private List macAddress;
+
+ /**
+ * 可被允许的CPU序列号
+ */
+ private String cpuSerial;
+
+ /**
+ * 可被允许的主板序列号
+ */
+ private String mainBoardSerial;
+
+ @Override
+ public String toString() {
+ return "LicenseCheckModel{" +
+ "ipAddress=" + ipAddress +
+ ", macAddress=" + macAddress +
+ ", cpuSerial='" + cpuSerial + '\'' +
+ ", mainBoardSerial='" + mainBoardSerial + '\'' +
+ '}';
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCreatorParam.java b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCreatorParam.java
new file mode 100644
index 0000000..2e57607
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseCreatorParam.java
@@ -0,0 +1,93 @@
+package com.ruoyi.license.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * License生成类需要的参数
+ */
+@Data
+public class LicenseCreatorParam {
+ private static final long serialVersionUID = 1L;
+ /**
+ * 证书subject
+ */
+ private String subject;
+
+ /**
+ * 密钥别称
+ */
+ private String privateAlias;
+
+ /**
+ * 密钥密码(需要妥善保管,不能让使用者知道)
+ */
+ private String keyPass;
+
+ /**
+ * 访问秘钥库的密码
+ */
+ private String storePass;
+
+ /**
+ * 证书生成路径
+ */
+ private String licensePath;
+
+ /**
+ * 密钥库存储路径
+ */
+ private String privateKeysStorePath;
+
+ /**
+ * 证书生效时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date issuedTime = new Date();
+
+ /**
+ * 证书失效时间
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+ private Date expiryTime;
+
+ /**
+ * 用户类型
+ */
+ private String consumerType = "user";
+
+ /**
+ * 用户数量
+ */
+ private Integer consumerAmount = 1;
+
+ /**
+ * 描述信息
+ */
+ private String description = "";
+
+ /**
+ * 额外的服务器硬件校验信息
+ */
+ private LicenseCheckModel licenseCheckModel;
+
+ @Override
+ public String toString() {
+ return "LicenseCreatorParam{" +
+ "subject='" + subject + '\'' +
+ ", privateAlias='" + privateAlias + '\'' +
+ ", keyPass='" + keyPass + '\'' +
+ ", storePass='" + storePass + '\'' +
+ ", licensePath='" + licensePath + '\'' +
+ ", privateKeysStorePath='" + privateKeysStorePath + '\'' +
+ ", issuedTime=" + issuedTime +
+ ", expiryTime=" + expiryTime +
+ ", consumerType='" + consumerType + '\'' +
+ ", consumerAmount=" + consumerAmount +
+ ", description='" + description + '\'' +
+ ", licenseCheckModel=" + licenseCheckModel +
+ '}';
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseVerifyParam.java b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseVerifyParam.java
new file mode 100644
index 0000000..9ef5f34
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/domain/LicenseVerifyParam.java
@@ -0,0 +1,59 @@
+package com.ruoyi.license.domain;
+
+import lombok.Data;
+
+/**
+ * License校验类需要的参数
+ */
+@Data
+public class LicenseVerifyParam {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 证书subject
+ */
+ private String subject;
+
+ /**
+ * 公钥别称
+ */
+ private String publicAlias;
+
+ /**
+ * 访问公钥库的密码
+ */
+ private String storePass;
+
+ /**
+ * 证书生成路径
+ */
+ private String licensePath;
+
+ /**
+ * 密钥库存储路径
+ */
+ private String publicKeysStorePath;
+
+ public LicenseVerifyParam() {
+
+ }
+
+ 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;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseVerifyParam{" +
+ "subject='" + subject + '\'' +
+ ", publicAlias='" + publicAlias + '\'' +
+ ", storePass='" + storePass + '\'' +
+ ", licensePath='" + licensePath + '\'' +
+ ", publicKeysStorePath='" + publicKeysStorePath + '\'' +
+ '}';
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/interceptor/LicenseCheckInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/license/interceptor/LicenseCheckInterceptor.java
new file mode 100644
index 0000000..2578b33
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/interceptor/LicenseCheckInterceptor.java
@@ -0,0 +1,32 @@
+package com.ruoyi.license.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.license.utils.LicenseVerify;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * LicenseCheckInterceptor
+ */
+@Component
+public class LicenseCheckInterceptor implements HandlerInterceptor {
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ LicenseVerify licenseVerify = new LicenseVerify();
+ boolean verifyResult = licenseVerify.verify();
+
+ if (verifyResult) {
+ return true;
+ } else {
+ AjaxResult ajaxResult = AjaxResult.error("您的证书无效,请核查服务器是否取得授权或重新申请证书!");
+ ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+ return false;
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/listener/LicenseCheckListener.java b/ruoyi-framework/src/main/java/com/ruoyi/license/listener/LicenseCheckListener.java
new file mode 100644
index 0000000..6b9d691
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/listener/LicenseCheckListener.java
@@ -0,0 +1,71 @@
+package com.ruoyi.license.listener;
+
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.license.domain.LicenseVerifyParam;
+import com.ruoyi.license.utils.LicenseVerify;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
+
+/**
+ * 在项目启动时安装证书
+ */
+
+@Component
+public class LicenseCheckListener implements ApplicationListener {
+ private static final Logger logger = LoggerFactory.getLogger("LicenseCheckListener");
+ /**
+ * 证书subject
+ */
+ @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;
+
+ @Value("${license.node_env}")
+ private String nodeEnv;
+
+ @Override
+ public void onApplicationEvent(ContextRefreshedEvent event) {
+ String envDevLab = "dev";
+ if(!StringUtils.equalsIgnoreCase(envDevLab,nodeEnv)){
+ logger.info("++++++++ 开始安装证书 ++++++++");
+ LicenseVerifyParam param = new LicenseVerifyParam();
+ param.setSubject(subject);
+ param.setPublicAlias(publicAlias);
+ param.setStorePass(storePass);
+ param.setLicensePath(licensePath);
+ param.setPublicKeysStorePath(publicKeysStorePath);
+ LicenseVerify licenseVerify = new LicenseVerify();
+ licenseVerify.install(param);
+ logger.info("++++++++ 证书安装结束 ++++++++");
+
+ }
+
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/service/AbstractServerInfos.java b/ruoyi-framework/src/main/java/com/ruoyi/license/service/AbstractServerInfos.java
new file mode 100644
index 0000000..71a3a35
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/service/AbstractServerInfos.java
@@ -0,0 +1,111 @@
+package com.ruoyi.license.service;
+
+import com.ruoyi.license.domain.LicenseCheckModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等
+ */
+public abstract class AbstractServerInfos {
+ private static final Logger logger = LoggerFactory.getLogger("AbstractServerInfos");
+
+ /**
+ * 组装需要额外校验的License参数
+ */
+ 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地址
+ */
+ protected abstract List getIpAddress() throws Exception;
+
+ /**
+ * 获取Mac地址
+ */
+ protected abstract List getMacAddress() throws Exception;
+
+ /**
+ * 获取CPU序列号
+ */
+ protected abstract String getCPUSerial() throws Exception;
+
+ /**
+ * 获取主板序列号
+ */
+ protected abstract String getMainBoardSerial() throws Exception;
+
+ /**
+ * 获取当前服务器所有符合条件的InetAddress
+ */
+ protected List getLocalAllInetAddress() throws Exception {
+ List result = new ArrayList<>(4);
+
+ // 遍历所有的网络接口
+ for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
+ NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();
+ // 在所有的接口下再遍历IP
+ for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
+ InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();
+
+ //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
+ if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/
+ && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){
+ result.add(inetAddr);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * 获取某个网络接口的Mac地址
+ */
+ protected String getMacByInetAddress(InetAddress inetAddr){
+ try {
+ byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
+ StringBuffer stringBuffer = new StringBuffer();
+
+ for(int i=0;i getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取CPU序列号
+ String[] shell = {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
+ 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)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用dmidecode命令获取主板序列号
+ String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
+ 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)){
+ serialNumber = line;
+ }
+
+ reader.close();
+ return serialNumber;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/service/impl/WindowsServerInfos.java b/ruoyi-framework/src/main/java/com/ruoyi/license/service/impl/WindowsServerInfos.java
new file mode 100644
index 0000000..34220db
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/service/impl/WindowsServerInfos.java
@@ -0,0 +1,86 @@
+package com.ruoyi.license.service.impl;
+
+import com.ruoyi.license.service.AbstractServerInfos;
+
+import java.net.InetAddress;
+import java.util.List;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+
+/**
+ * 用于获取客户Windows服务器的基本信息
+ */
+public class WindowsServerInfos extends AbstractServerInfos {
+ @Override
+ protected List getIpAddress() throws Exception {
+ List result = null;
+
+ //获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected List getMacAddress() throws Exception {
+ List result = null;
+
+ //1. 获取所有网络接口
+ List inetAddresses = getLocalAllInetAddress();
+
+ if(inetAddresses != null && inetAddresses.size() > 0){
+ //2. 获取所有网络接口的Mac地址
+ result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
+ }
+
+ return result;
+ }
+
+ @Override
+ protected String getCPUSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取CPU序列号
+ Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+
+ @Override
+ protected String getMainBoardSerial() throws Exception {
+ //序列号
+ String serialNumber = "";
+
+ //使用WMIC获取主板序列号
+ Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
+ process.getOutputStream().close();
+ Scanner scanner = new Scanner(process.getInputStream());
+
+ if(scanner.hasNext()){
+ scanner.next();
+ }
+
+ if(scanner.hasNext()){
+ serialNumber = scanner.next().trim();
+ }
+
+ scanner.close();
+ return serialNumber;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/utils/CustomLicenseManager.java b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/CustomLicenseManager.java
new file mode 100644
index 0000000..3ea9fa5
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/CustomLicenseManager.java
@@ -0,0 +1,245 @@
+package com.ruoyi.license.utils;
+
+import com.ruoyi.license.domain.LicenseCheckModel;
+import com.ruoyi.license.service.AbstractServerInfos;
+import com.ruoyi.license.service.impl.LinuxServerInfos;
+import com.ruoyi.license.service.impl.WindowsServerInfos;
+import de.schlichtherle.license.*;
+import de.schlichtherle.xml.GenericCertificate;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.beans.XMLDecoder;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 自定义LicenseManager,用于增加额外的服务器硬件信息校验
+ */
+public class CustomLicenseManager extends LicenseManager {
+ private static final Logger logger = LoggerFactory.getLogger("CustomLicenseManager");
+
+ //XML编码
+ private static final String XML_CHARSET = "UTF-8";
+ //默认BUFSIZE
+ private static final int DEFAULT_BUFSIZE = 8 * 1024;
+
+ public CustomLicenseManager() {
+
+ }
+
+ public CustomLicenseManager(LicenseParam param) {
+ super(param);
+ }
+
+ /**
+ * 复写create方法
+ */
+ @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地址等其他信息
+ */
+ @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地址等其他信息
+ */
+ @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;
+ }
+
+ /**
+ * 校验生成证书的参数信息
+ */
+ protected synchronized void validateCreate(final LicenseContent content)
+ throws LicenseContentException {
+ final LicenseParam param = getLicenseParam();
+
+ 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地址等其他信息校验
+ */
+ @Override
+ protected synchronized void validate(final LicenseContent content)
+ throws LicenseContentException {
+ //1. 首先调用父类的validate方法
+ super.validate(content);
+
+ //2. 然后校验自定义的License参数
+ //License中可被允许的参数信息
+ LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
+ //当前服务器真实的参数信息
+ 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
+ */
+ 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_BUFSIZE),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参数
+ */
+ 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范围内
+ */
+ private boolean checkIpAddress(List expectedList,List 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等)序列号是否在可允许范围内
+ */
+ private boolean checkSerial(String expectedSerial,String serverSerial){
+ if(StringUtils.isNotBlank(expectedSerial)){
+ if(StringUtils.isNotBlank(serverSerial)){
+ if(expectedSerial.equals(serverSerial)){
+ return true;
+ }
+ }
+
+ return false;
+ }else{
+ return true;
+ }
+ }
+
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseCreator.java
new file mode 100644
index 0000000..008ea08
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseCreator.java
@@ -0,0 +1,92 @@
+package com.ruoyi.license.utils;
+
+import com.ruoyi.license.domain.CustomKeyStoreParam;
+import com.ruoyi.license.domain.LicenseCreatorParam;
+import de.schlichtherle.license.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.prefs.Preferences;
+
+/**
+ * License生成类
+ */
+
+public class LicenseCreator {
+ private static final Logger logger = LoggerFactory.getLogger("LicenseCreator");
+ private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");
+ private LicenseCreatorParam param;
+
+ public LicenseCreator(LicenseCreatorParam param) {
+ this.param = param;
+ }
+
+ /**
+ * 生成License证书
+ */
+ public boolean generateLicense(){
+ try {
+ LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());
+ LicenseContent licenseContent = initLicenseContent();
+
+ licenseManager.store(licenseContent,new File(param.getLicensePath()));
+
+ return true;
+ }catch (Exception e){
+ logger.error(MessageFormat.format("证书生成失败:{0}", param) ,e);
+ return false;
+ }
+ }
+
+ /**
+ * 初始化证书生成参数
+ */
+ private LicenseParam initLicenseParam(){
+ Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
+
+ //设置对证书内容加密的秘钥
+ CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
+
+ KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
+ ,param.getPrivateKeysStorePath()
+ ,param.getPrivateAlias()
+ ,param.getStorePass()
+ ,param.getKeyPass());
+
+ LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()
+ ,preferences
+ ,privateStoreParam
+ ,cipherParam);
+
+ return licenseParam;
+ }
+
+ /**
+ * 设置证书生成正文信息
+ */
+ private LicenseContent initLicenseContent(){
+ LicenseContent licenseContent = new LicenseContent();
+ licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);
+ licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
+
+ licenseContent.setSubject(param.getSubject());
+ licenseContent.setIssued(param.getIssuedTime());
+ licenseContent.setNotBefore(param.getIssuedTime());
+ licenseContent.setNotAfter(param.getExpiryTime());
+ licenseContent.setConsumerType(param.getConsumerType());
+ licenseContent.setConsumerAmount(param.getConsumerAmount());
+ licenseContent.setInfo(param.getDescription());
+
+ //扩展校验服务器硬件信息
+ licenseContent.setExtra(param.getLicenseCheckModel());
+
+ return licenseContent;
+ }
+
+ public static void main(String[] args) {
+
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseManagerHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseManagerHolder.java
new file mode 100644
index 0000000..952f053
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseManagerHolder.java
@@ -0,0 +1,24 @@
+package com.ruoyi.license.utils;
+
+import de.schlichtherle.license.LicenseManager;
+import de.schlichtherle.license.LicenseParam;
+
+/**
+ * de.schlichtherle.license.LicenseManager的单例
+ */
+
+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;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseVerify.java b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseVerify.java
new file mode 100644
index 0000000..4d4353f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/license/utils/LicenseVerify.java
@@ -0,0 +1,84 @@
+package com.ruoyi.license.utils;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.license.domain.CustomKeyStoreParam;
+import com.ruoyi.license.domain.LicenseVerifyParam;
+import de.schlichtherle.license.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.prefs.Preferences;
+
+/**
+ * License校验类
+ */
+public class LicenseVerify {
+ private static final Logger logger = LoggerFactory.getLogger("LicenseVerify");
+
+ /**
+ * 安装License证书
+ */
+ public synchronized LicenseContent install(LicenseVerifyParam param){
+ LicenseContent result = null;
+ DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try{
+ LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
+ licenseManager.uninstall();
+ InputStream stream = new ClassPathResource(param.getLicensePath()).getStream();
+ String tempFilePath = RuoYiConfig.getProfile() + "/license_temp.lic";
+ File tempFile = new File(tempFilePath);
+ File file = FileUtil.writeFromStream(stream, tempFile);
+ result = licenseManager.install(file);
+ logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));
+ }catch (Exception e){
+ logger.error("证书安装失败!", e);
+ }
+
+ return result;
+ }
+
+ /**
+ * 校验License证书
+ */
+ 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;
+ }
+ }
+
+ /**
+ * 初始化证书生成参数
+ */
+ 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);
+ }
+}