diff --git a/pom.xml b/pom.xml index a0e7089..da8a503 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ 1.6.3 1.6.2 2.9.11 + 1.33 3.8.1 ${env.NACOS_SERVER_NAME} ${env.NACOS_SERVER_PORT} @@ -295,6 +296,13 @@ ${wfc.version} + + + org.wfc + wfc-common-license + ${wfc.version} + + org.mapstruct @@ -343,6 +351,13 @@ IJPay-All ${ijapy.version} + + + + de.schlichtherle.truelicense + truelicense-core + ${turelicense.version} + diff --git a/wfc-auth/pom.xml b/wfc-auth/pom.xml index 66e3269..fed23b8 100644 --- a/wfc-auth/pom.xml +++ b/wfc-auth/pom.xml @@ -62,6 +62,11 @@ org.wfc wfc-api-user + + + org.wfc + wfc-common-license + diff --git a/wfc-common/pom.xml b/wfc-common/pom.xml index ca9c893..909c3c9 100644 --- a/wfc-common/pom.xml +++ b/wfc-common/pom.xml @@ -20,6 +20,7 @@ wfc-common-datasource wfc-common-mail wfc-common-mybatis + wfc-common-license wfc-common diff --git a/wfc-common/wfc-common-license/pom.xml b/wfc-common/wfc-common-license/pom.xml new file mode 100644 index 0000000..b0df6a3 --- /dev/null +++ b/wfc-common/wfc-common-license/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + org.wfc + wfc-common + 1.0.13 + + + wfc-common-license + + wfc-common-license服务 + + + + + org.wfc + wfc-common-core + + + + org.springframework + spring-webmvc + + + + de.schlichtherle.truelicense + truelicense-core + + + + diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomKeyStoreParam.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomKeyStoreParam.java new file mode 100644 index 0000000..cd6dbd6 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomKeyStoreParam.java @@ -0,0 +1,64 @@ +package org.wfc.common.license; + +import de.schlichtherle.license.AbstractKeyStoreParam; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * 自定义KeyStoreParam,用于将公私钥存储文件存放到其他磁盘位置而不是项目中 + * + */ +public class CustomKeyStoreParam extends AbstractKeyStoreParam { + + /** + * 公钥/私钥在磁盘上的存储路径 + */ + 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; + } + + /** + * 复写de.schlichtherle.license.AbstractKeyStoreParam的getStream()方法
+ * 用于将公私钥存储文件存放到其他磁盘位置而不是项目中 + * @param + * @return java.io.InputStream + */ + @Override + public InputStream getStream() throws IOException { + final InputStream in = new FileInputStream(new File(storePath)); + if (null == in){ + throw new FileNotFoundException(storePath); + } + + return in; + } +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomLicenseManager.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomLicenseManager.java new file mode 100644 index 0000000..ff86a0f --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/CustomLicenseManager.java @@ -0,0 +1,265 @@ +package org.wfc.common.license; + +import de.schlichtherle.license.LicenseContent; +import de.schlichtherle.license.LicenseContentException; +import de.schlichtherle.license.LicenseManager; +import de.schlichtherle.license.LicenseNotary; +import de.schlichtherle.license.LicenseParam; +import de.schlichtherle.license.NoLicenseInstalledException; +import de.schlichtherle.xml.GenericCertificate; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.wfc.common.license.domain.LicenseCheckModel; +import org.wfc.common.license.serverinfo.AbstractServerInfos; +import org.wfc.common.license.serverinfo.LinuxServerInfos; +import org.wfc.common.license.serverinfo.WindowsServerInfos; + +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,用于增加额外的服务器硬件信息校验 + */ +@Slf4j +public class CustomLicenseManager extends LicenseManager{ + + //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方法 + * @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地址等其他信息 + * @param + * @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地址等其他信息 + * @param + * @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 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地址等其他信息校验 + * @param content LicenseContent + */ + @Override + protected synchronized void validate(final LicenseContent content) + throws LicenseContentException { + //1. 首先调用父类的validate方法 + super.validate(content); + + //2. 然后校验自定义的License参数 + //License中可被允许的参数信息 + LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra(); + if(expectedCheckModel != null){ + //当前服务器真实的参数信息 + LicenseCheckModel serverCheckModel = getServerInfos(); + + if(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_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) { + log.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范围内
+ * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true + * @return boolean + */ + 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等)序列号是否在可允许范围内 + * @param + * @return boolean + */ + 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/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseManagerHolder.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseManagerHolder.java new file mode 100644 index 0000000..d180912 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseManagerHolder.java @@ -0,0 +1,25 @@ +package org.wfc.common.license; + +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/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseVerify.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseVerify.java new file mode 100644 index 0000000..dd48d62 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/LicenseVerify.java @@ -0,0 +1,88 @@ +package org.wfc.common.license; + +import de.schlichtherle.license.CipherParam; +import de.schlichtherle.license.DefaultCipherParam; +import de.schlichtherle.license.DefaultLicenseParam; +import de.schlichtherle.license.KeyStoreParam; +import de.schlichtherle.license.LicenseContent; +import de.schlichtherle.license.LicenseManager; +import de.schlichtherle.license.LicenseParam; +import lombok.extern.slf4j.Slf4j; +import org.wfc.common.license.domain.LicenseVerifyParam; + +import java.io.File; +import java.text.DateFormat; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.prefs.Preferences; + +/** + * License校验类 + */ +@Slf4j +public class LicenseVerify { + /** + * 安装License证书 + */ + public synchronized LicenseContent install(LicenseVerifyParam param){ + LicenseContent result = null; + 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())); + log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter()))); + }catch (Exception e){ + log.error("证书安装失败!",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(); +// System.out.println(licenseContent.getSubject()); + + log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter()))); + return true; + }catch (Exception e){ + log.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); + } + +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseCheckModel.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseCheckModel.java new file mode 100644 index 0000000..e12dc6d --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseCheckModel.java @@ -0,0 +1,73 @@ +package org.wfc.common.license.domain; + +import java.io.Serializable; +import java.util.List; + +/** + * 自定义需要校验的License参数 + */ +public class LicenseCheckModel implements Serializable{ + + private static final long serialVersionUID = 8600137500316662317L; + /** + * 可被允许的IP地址 + */ + private List ipAddress; + + /** + * 可被允许的MAC地址 + */ + private List macAddress; + + /** + * 可被允许的CPU序列号 + */ + private String cpuSerial; + + /** + * 可被允许的主板序列号 + */ + private String mainBoardSerial; + + public List getIpAddress() { + return ipAddress; + } + + public void setIpAddress(List ipAddress) { + this.ipAddress = ipAddress; + } + + public List getMacAddress() { + return macAddress; + } + + public void setMacAddress(List macAddress) { + this.macAddress = macAddress; + } + + public String getCpuSerial() { + return cpuSerial; + } + + public void setCpuSerial(String cpuSerial) { + this.cpuSerial = cpuSerial; + } + + public String getMainBoardSerial() { + return mainBoardSerial; + } + + public void setMainBoardSerial(String mainBoardSerial) { + this.mainBoardSerial = mainBoardSerial; + } + + @Override + public String toString() { + return "LicenseCheckModel{" + + "ipAddress=" + ipAddress + + ", macAddress=" + macAddress + + ", cpuSerial='" + cpuSerial + '\'' + + ", mainBoardSerial='" + mainBoardSerial + '\'' + + '}'; + } +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseVerifyParam.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseVerifyParam.java new file mode 100644 index 0000000..d37ec5f --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/domain/LicenseVerifyParam.java @@ -0,0 +1,95 @@ +package org.wfc.common.license.domain; + +/** + * License校验类需要的参数 + */ +public class LicenseVerifyParam { + + /** + * 证书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; + } + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public String getPublicAlias() { + return publicAlias; + } + + public void setPublicAlias(String publicAlias) { + this.publicAlias = publicAlias; + } + + public String getStorePass() { + return storePass; + } + + public void setStorePass(String storePass) { + this.storePass = storePass; + } + + public String getLicensePath() { + return licensePath; + } + + public void setLicensePath(String licensePath) { + this.licensePath = licensePath; + } + + public String getPublicKeysStorePath() { + return publicKeysStorePath; + } + + public void setPublicKeysStorePath(String publicKeysStorePath) { + this.publicKeysStorePath = publicKeysStorePath; + } + + @Override + public String toString() { + return "LicenseVerifyParam{" + + "subject='" + subject + '\'' + + ", publicAlias='" + publicAlias + '\'' + + ", storePass='" + storePass + '\'' + + ", licensePath='" + licensePath + '\'' + + ", publicKeysStorePath='" + publicKeysStorePath + '\'' + + '}'; + } +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseCheckInterceptor.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseCheckInterceptor.java new file mode 100644 index 0000000..c0e8b14 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseCheckInterceptor.java @@ -0,0 +1,62 @@ +package org.wfc.common.license.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.wfc.common.license.LicenseVerify; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * 使用拦截器拦截请求,验证证书的可用性 + * + */ +@Slf4j +@Component +public class LicenseCheckInterceptor implements HandlerInterceptor { + + /** + * 进入controller层之前拦截请求 + * @param request + * @param response + * @param handler + * @return + * @throws Exception + */ + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + LicenseVerify licenseVerify = new LicenseVerify(); + + //校验证书是否有效 + boolean verifyResult = licenseVerify.verify(); + + if(verifyResult){ + log.info("验证成功,证书可用"); + return true; + }else{ + log.info("验证失败,证书无效"); + response.setCharacterEncoding("utf-8"); + Map result = new HashMap<>(1); + result.put("result","您的证书无效,请核查服务器是否取得授权或重新申请证书!"); + + outputError(response, "Your License is invalid"); + + return false; + } + } + + // 输出错误信息 + private void outputError(HttpServletResponse response, String errorMsg) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + PrintWriter out = response.getWriter(); + out.write("{\"msg\": \"" + errorMsg + "\", \"code\": 500}"); + out.flush(); + out.close(); + } + +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseInterceptorConfig.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseInterceptorConfig.java new file mode 100644 index 0000000..08b545e --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/interceptor/LicenseInterceptorConfig.java @@ -0,0 +1,28 @@ +package org.wfc.common.license.interceptor; + +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; + +/** + * @Description: 拦截器配置 + */ +@Configuration +public class LicenseInterceptorConfig implements WebMvcConfigurer { + + @Resource + private LicenseCheckInterceptor licenseCheckInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + + //添加要拦截的url + registry.addInterceptor(licenseCheckInterceptor) + // 拦截的路径 + .addPathPatterns("/**"); + // 放行的路径 +// .excludePathPatterns("/admin/login"); + } +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseCheckRunner.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseCheckRunner.java new file mode 100644 index 0000000..b1fdaf5 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseCheckRunner.java @@ -0,0 +1,163 @@ +package org.wfc.common.license.runner; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import org.wfc.common.license.LicenseVerify; +import org.wfc.common.license.domain.LicenseVerifyParam; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@Data +public class LicenseCheckRunner implements ApplicationRunner { + + @Autowired + private LicenseProperties licenseProperties; + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture scheduledFuture; + + + // 启动定时任务 + public void startTimer() { + scheduledFuture = scheduler.scheduleAtFixedRate(this::timer, 0, 60, TimeUnit.SECONDS); + } + // 停止定时任务 + public void stopTimer() { + if (scheduledFuture != null) { + scheduledFuture.cancel(false); + } + } + /** + * 文件唯一身份标识 == 相当于人类的指纹一样 + */ + private static String md5 = ""; + private static boolean isLoad = true; + + public static void LicenseVerifyInstall(LicenseProperties licenseVerifyProperties) { + new LicenseCheckRunner(licenseVerifyProperties); + } + + public LicenseCheckRunner(LicenseProperties licenseProperties) { + // startTimer(); + this.licenseProperties = licenseProperties; + if (StrUtil.isNotEmpty(licenseProperties.getLicensePath())) { + install(); + try { + String readMd5 = getMd5(licenseProperties.getLicensePath()); + isLoad = true; + if (LicenseCheckRunner.md5 == null || "".equals(LicenseCheckRunner.md5)) { + LicenseCheckRunner.md5 = readMd5; + } + } catch (Exception e) { + + } + } + } + + + /** + * 5秒检测一次,不能太快也不能太慢 + */ + protected void timer() { + if (!isLoad) { + return; + } + String readMd5 = null; + try { + readMd5 = getMd5(licenseProperties.getLicensePath()); + } catch (Exception e) { + throw new RuntimeException(e); + } + // 不相等,说明lic变化了 + if (!readMd5.equals(LicenseCheckRunner.md5)) { + log.info("定时任务:检测到证书有变化"); + install(); + LicenseCheckRunner.md5 = readMd5; + } + + } + + private void install() { + log.info("++++++++ 开始安装证书 ++++++++"); + LicenseVerifyParam param = new LicenseVerifyParam(); + param.setSubject(licenseProperties.getSubject()); + param.setPublicAlias(licenseProperties.getPublicAlias()); + param.setStorePass(licenseProperties.getStorePass()); + // 相对路径resources资源目录 + String resourcePath = getClass().getClassLoader().getResource("").getPath(); + // 证书地址 +// param.setLicensePath(resourcePath + "license.lic"); + param.setLicensePath(licenseProperties.getLicensePath()); + // 公钥地址 +// param.setPublicKeysStorePath(resourcePath + "publicCerts.keystore"); + param.setPublicKeysStorePath(licenseProperties.getPublicKeysStorePath()); + // 安装证书 + LicenseVerify licenseVerify = new LicenseVerify(); + licenseVerify.install(param); + log.info("++++++++ 证书安装结束 ++++++++"); + } + + /** + *

获取文件的md5

+ */ + public String getMd5(String filePath) throws Exception { + File file; + String md5 = ""; + try { + file = new File(filePath); + if (file.exists()) { + FileInputStream is = new FileInputStream(file); + byte[] data = new byte[is.available()]; + is.read(data); + md5 = calculateMD5(data); + is.close(); + } + } catch (FileNotFoundException e) { + log.error("找不到证书文件 {}", e.getMessage()); + } + return md5; + } + + public static String calculateMD5(byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hashBytes = md.digest(data); + + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + log.error("证书文件计算MD5失败 {}", e.getMessage()); + return null; + } + } + + + @Override + public void run(ApplicationArguments args) throws Exception { + install(); + startTimer(); + } +} \ No newline at end of file diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseProperties.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseProperties.java new file mode 100644 index 0000000..7066460 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/runner/LicenseProperties.java @@ -0,0 +1,39 @@ +package org.wfc.common.license.runner; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author: cyc + * @since: 2025-04-11 + */ +@Data +@Configuration +@ConfigurationProperties(prefix = "license") +public class LicenseProperties { + /** + * 证书subject + */ + private String subject; + + /** + * 公钥别称 + */ + private String publicAlias; + + /** + * 访问公钥库的密码 + */ + private String storePass; + + /** + * 证书生成路径 + */ + private String licensePath; + + /** + * 密钥库存储路径 + */ + private String publicKeysStorePath; +} diff --git a/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/AbstractServerInfos.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/AbstractServerInfos.java new file mode 100644 index 0000000..ba6cfeb --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/AbstractServerInfos.java @@ -0,0 +1,120 @@ +package org.wfc.common.license.serverinfo; + +import lombok.extern.slf4j.Slf4j; +import org.wfc.common.license.domain.LicenseCheckModel; + +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序列号、主板序列号等 + */ +@Slf4j +public abstract class AbstractServerInfos { + + /** + * 组装需要额外校验的License参数 + * @return demo.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){ + log.error("获取服务器硬件信息失败",e); + } + + return result; + } + + /** + * 获取IP地址 + * @return java.util.List + */ + protected abstract List getIpAddress() throws Exception; + + /** + * 获取Mac地址 + * @return java.util.List + */ + protected abstract List getMacAddress() throws Exception; + + /** + * 获取CPU序列号 + * @return java.lang.String + */ + protected abstract String getCPUSerial() throws Exception; + + /** + * 获取主板序列号 + * @return java.lang.String + */ + protected abstract String getMainBoardSerial() throws Exception; + + /** + * 获取当前服务器所有符合条件的InetAddress + * @return java.util.List + */ + 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地址 + * @param + * @return void + */ + 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/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/WindowsServerInfos.java b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/WindowsServerInfos.java new file mode 100644 index 0000000..9761cab --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/java/org/wfc/common/license/serverinfo/WindowsServerInfos.java @@ -0,0 +1,85 @@ +package org.wfc.common.license.serverinfo; + +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 != null && 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 != null && scanner.hasNext()){ + scanner.next(); + } + + if(scanner.hasNext()){ + serialNumber = scanner.next().trim(); + } + + scanner.close(); + return serialNumber; + } +} diff --git a/wfc-common/wfc-common-license/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/wfc-common/wfc-common-license/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3ac4935 --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +org.wfc.common.license.runner.LicenseProperties +org.wfc.common.license.runner.LicenseCheckRunner +org.wfc.common.license.interceptor.LicenseCheckInterceptor +org.wfc.common.license.interceptor.LicenseInterceptorConfig \ No newline at end of file diff --git a/wfc-common/wfc-common-license/src/main/resources/application.yml b/wfc-common/wfc-common-license/src/main/resources/application.yml new file mode 100644 index 0000000..6fc2d6d --- /dev/null +++ b/wfc-common/wfc-common-license/src/main/resources/application.yml @@ -0,0 +1,6 @@ +license: + subject: license_demo + publicAlias: publicCert + storePass: public_password1234 + licensePath: D:/workspace/gitee/spring-boot2-license/license/license.lic + publicKeysStorePath: D:/workspace/gitee/spring-boot2-license/license/publicCerts.keystore \ No newline at end of file diff --git a/wfc-modules/wfc-system/pom.xml b/wfc-modules/wfc-system/pom.xml index b6dd658..e3519ba 100644 --- a/wfc-modules/wfc-system/pom.xml +++ b/wfc-modules/wfc-system/pom.xml @@ -89,6 +89,11 @@ org.mapstruct mapstruct + + + org.wfc + wfc-common-license + diff --git a/wfc-modules/wfc-user/pom.xml b/wfc-modules/wfc-user/pom.xml index bc5e3b4..f1bfc17 100644 --- a/wfc-modules/wfc-user/pom.xml +++ b/wfc-modules/wfc-user/pom.xml @@ -103,6 +103,11 @@ org.wfc wfc-common-mail + + + org.wfc + wfc-common-license +