src/main/java/com/zy/system/config/LicenseSchemaInitializer.java
New file @@ -0,0 +1,79 @@ package com.zy.system.config; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.sql.DataSource; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.Statement; @Component public class LicenseSchemaInitializer { private final DataSource dataSource; public LicenseSchemaInitializer(DataSource dataSource) { this.dataSource = dataSource; } @PostConstruct public void init() { ensureTable(); ensureColumn("sys_license_infos", "request_code", "NVARCHAR(2048) NULL"); } private void ensureTable() { try (Connection connection = dataSource.getConnection()) { if (hasTable(connection, "sys_license_infos")) { return; } try (Statement statement = connection.createStatement()) { statement.executeUpdate("CREATE TABLE sys_license_infos (" + "id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, " + "[license] NVARCHAR(MAX) NULL, " + "license_time NVARCHAR(255) NULL, " + "request_code NVARCHAR(2048) NULL, " + "create_time DATETIME NULL)"); } } catch (Exception ignored) { } } private void ensureColumn(String tableName, String columnName, String columnDefinition) { try (Connection connection = dataSource.getConnection()) { if (!hasTable(connection, tableName) || hasColumn(connection, tableName, columnName)) { return; } try (Statement statement = connection.createStatement()) { statement.executeUpdate("ALTER TABLE " + tableName + " ADD " + columnName + " " + columnDefinition); } } catch (Exception ignored) { } } private boolean hasTable(Connection connection, String tableName) throws Exception { DatabaseMetaData metaData = connection.getMetaData(); try (ResultSet resultSet = metaData.getTables(connection.getCatalog(), null, tableName, new String[]{"TABLE"})) { while (resultSet.next()) { if (tableName.equalsIgnoreCase(resultSet.getString("TABLE_NAME"))) { return true; } } } return false; } private boolean hasColumn(Connection connection, String tableName, String columnName) throws Exception { DatabaseMetaData metaData = connection.getMetaData(); try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, tableName, columnName)) { while (resultSet.next()) { if (columnName.equalsIgnoreCase(resultSet.getString("COLUMN_NAME"))) { return true; } } } return false; } } src/main/java/com/zy/system/controller/LicenseCreatorController.java
@@ -1,109 +1,161 @@ package com.zy.system.controller; import com.core.common.Cools; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.zy.system.entity.license.*; import com.zy.system.entity.LicenseInfos; import com.zy.system.entity.license.LicenseCheckListener; import com.zy.system.entity.license.LicenseUploadParam; import com.zy.system.entity.license.LicenseUtils; import com.zy.system.entity.license.LicenseVerify; import com.zy.system.entity.license.LicenseVerifyParam; import com.zy.system.service.LicenseInfosService; import com.zy.system.timer.LicenseTimer; import de.schlichtherle.license.LicenseContent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.Date; /** * * 用于生成证书文件,不能放在给客户部署的代码里 * 许可证运行时管理 */ @RestController @RequestMapping("/license") public class LicenseCreatorController { @Value("${license.licensePath}") private String licensePath; @Value("${license.subject}") private String licenseSubject; @Value("${license.publicAlias}") private String publicAlias; @Value("${license.storePass}") private String storePass; @Value("${license.publicKeysStorePath}") private String publicKeysStorePath; @Autowired private LicenseCheckListener licenseCheckListener; @Autowired private LicenseTimer licenseTimer; @Autowired private LicenseInfosService licenseInfosService; /** * 获取服务器硬件信息 * @param osName 操作系统类型,如果为空则自动判断 * 获取请求码。 */ @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public LicenseCheck getServerInfos(@RequestParam(value = "osName",required = false) String osName) { //操作系统类型 if(Cools.isEmpty(osName)){ osName = System.getProperty("os.name"); } osName = osName.toLowerCase(); @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/getRequestCode", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R getRequestCode() { return R.ok(LicenseUtils.buildRequestCode(licenseSubject)); } AbstractServerInfos abstractServerInfos = null; //根据不同操作系统类型选择不同的数据获取方法 if (osName.startsWith("windows")) { abstractServerInfos = new WindowsServerInfos(); } else if (osName.startsWith("linux")) { // abstractServerInfos = new LinuxServerInfos(); }else{//其他服务器类型 abstractServerInfos = new WindowsServerInfos(); } return abstractServerInfos.getServerInfos(); /** * 获取系统配置。 */ @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/getServerInfos", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public Object getServerInfos(@RequestParam(value = "osName", required = false) String osName) { return LicenseUtils.getServerInfos(); } /** * 获取许可证有效期天数 */ @RequestMapping(value = "/getLicenseDays") @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/getLicenseDays", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R getLicenseDays() { return R.ok(licenseTimer.getLicenseDays()); int licenseDays = licenseTimer.getLicenseDays(); if (!licenseTimer.getSystemSupport()) { licenseDays = -1; } return R.ok(licenseDays); } @RequestMapping(value = "/updateLicense") public R updateLicense(@RequestParam("file") MultipartFile[] files){ MultipartFile file = files[0]; String licensePathFileName = this.getClass().getClassLoader().getResource(licensePath).getPath(); File licensePathFile = new File(licensePathFileName); //服务器端保存的文件对象 File serverFile = new File(licensePathFile.getPath()); if (serverFile.exists()) { try { serverFile.delete();//存在文件,删除 } catch (Exception e) { e.printStackTrace(); } @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/updateLicense", consumes = MediaType.APPLICATION_JSON_VALUE, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R updateLicense(@RequestBody LicenseUploadParam param){ if (param == null || param.getLicense() == null || param.getLicense().trim().isEmpty()) { return R.error("许可证内容不能为空"); } return saveAndActivateLicense(param.getLicense().trim()); } @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/updateLicense", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R updateLicense(@RequestParam("file") MultipartFile[] files) { if (files == null || files.length == 0 || files[0] == null || files[0].isEmpty()) { return R.error("许可证文件不能为空"); } try { //创建文件 serverFile.createNewFile(); //将上传的文件写入到服务器端文件内 file.transferTo(serverFile); } catch (IOException e) { e.printStackTrace(); String licenseBase64 = Base64.getEncoder().encodeToString(files[0].getBytes()); return saveAndActivateLicense(licenseBase64); } catch (Exception e) { return R.error("许可证读取失败"); } //重新加载许可证 boolean loadedLicense = licenseCheckListener.loadLicense(); if (loadedLicense) { return R.ok(); } return R.error("许可证更新失败"); } @RequestMapping(value = "/activate") @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/activate", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R activate() { licenseTimer.timer(); if (!licenseTimer.getSystemSupport()) { return R.error("许可证激活失败"); } return R.ok(); } } @ManagerAuth(value = ManagerAuth.Auth.NONE) @RequestMapping(value = "/getProjectName", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) public R getProjectName() { return R.ok(licenseSubject); } private R saveAndActivateLicense(String licenseBase64) { LicenseVerifyParam verifyParam = buildVerifyParam(); LicenseVerify licenseVerify = new LicenseVerify(); LicenseContent install = licenseVerify.install(verifyParam, licenseBase64); if (install == null) { return R.error("许可证内容无效"); } LicenseInfos licenseInfos = new LicenseInfos(); licenseInfos.setLicense(licenseBase64); licenseInfos.setLicenseTime(formatLicenseTime(install)); licenseInfos.setRequestCode(LicenseUtils.buildRequestCode(licenseSubject)); licenseInfos.setCreateTime(new Date()); if (!licenseInfosService.insert(licenseInfos)) { return R.error("许可证保存失败"); } boolean loadedLicense = licenseCheckListener.loadLicense(false); if (!loadedLicense) { return R.error("许可证激活失败"); } licenseTimer.verify(); if (!licenseTimer.getSystemSupport()) { return R.error("许可证校验失败"); } return R.ok(); } private LicenseVerifyParam buildVerifyParam() { LicenseVerifyParam param = new LicenseVerifyParam(); param.setSubject(licenseSubject); param.setPublicAlias(publicAlias); param.setStorePass(storePass); param.setPublicKeysStorePath(publicKeysStorePath); return param; } private String formatLicenseTime(LicenseContent install) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return format.format(install.getNotBefore()) + " - " + format.format(install.getNotAfter()); } } src/main/java/com/zy/system/entity/LicenseInfos.java
New file @@ -0,0 +1,42 @@ package com.zy.system.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import com.core.common.Cools; import lombok.Data; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.Date; @Data @TableName("sys_license_infos") public class LicenseInfos implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; private String license; @TableField("license_time") private String licenseTime; @TableField("request_code") private String requestCode; @TableField("create_time") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; public String getCreateTime$() { if (Cools.isEmpty(this.createTime)) { return ""; } return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime); } } src/main/java/com/zy/system/entity/license/AbstractServerInfos.java
@@ -24,11 +24,26 @@ try { result.setIpAddress(this.getIpAddress()); }catch (Exception e){ logger.error("获取服务器IP失败",e); } try { result.setMacAddress(this.getMacAddress()); }catch (Exception e){ logger.error("获取服务器MAC失败",e); } try { result.setCpuSerial(this.getCPUSerial()); }catch (Exception e){ logger.error("获取服务器CPU序列号失败",e); } try { result.setMainBoardSerial(this.getMainBoardSerial()); }catch (Exception e){ logger.error("获取服务器硬件信息失败",e); logger.error("获取服务器主板序列号失败",e); } return result; @@ -84,6 +99,9 @@ protected String getMacByInetAddress(InetAddress inetAddr){ try { byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress(); if (mac == null) { return null; } StringBuffer stringBuffer = new StringBuffer(); for(int i=0;i<mac.length;i++){ @@ -108,4 +126,4 @@ return null; } } } src/main/java/com/zy/system/entity/license/CustomLicenseManager.java
@@ -1,6 +1,5 @@ package com.zy.system.entity.license; import com.core.common.Cools; import de.schlichtherle.license.*; import de.schlichtherle.xml.GenericCertificate; import org.apache.logging.log4j.LogManager; @@ -11,7 +10,6 @@ import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.List; /** * 自定义LicenseManager,用于增加额外的服务器硬件信息校验 @@ -120,34 +118,20 @@ //1. 首先调用父类的validate方法 super.validate(content); //2. 然后校验自定义的License参数 //License中可被允许的参数信息 LicenseCheck expectedCheckModel = (LicenseCheck) content.getExtra(); //当前服务器真实的参数信息 LicenseCheck serverCheckModel = getServerInfos(); Object extra = content.getExtra(); LicenseCheck serverCheckModel = LicenseUtils.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{ if (serverCheckModel == null) { throw new LicenseContentException("不能获取服务器硬件信息"); } if (!LicenseBindingSupport.isV2Extra(extra)) { throw new LicenseContentException("许可证格式不支持"); } LicenseBindModel bindModel = LicenseBindingSupport.parseBindModel(extra); if (!LicenseBindingSupport.matches(bindModel, serverCheckModel)) { throw new LicenseContentException("当前服务器不在授权节点范围内"); } } @@ -182,60 +166,4 @@ return null; } /** * 获取当前服务器需要额外校验的License参数 */ private LicenseCheck 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 WindowsServerInfos(); } return abstractServerInfos.getServerInfos(); } /** * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/> * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true */ 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等)序列号是否在可允许范围内 */ private boolean checkSerial(String expectedSerial,String serverSerial){ if(!Cools.isEmpty(expectedSerial)){ if(!Cools.isEmpty(serverSerial)){ if(expectedSerial.equals(serverSerial)){ return true; } } return false; }else{ return true; } } } } src/main/java/com/zy/system/entity/license/LicenseBindModel.java
New file @@ -0,0 +1,21 @@ package com.zy.system.entity.license; import lombok.Data; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Data public class LicenseBindModel implements Serializable { private static final long serialVersionUID = 7064744215406459726L; private Integer version = 2; private String bindMode = "MULTI_NODE"; private String matchMode = "ANY"; private List<LicenseNodeCheck> nodes = new ArrayList<>(); } src/main/java/com/zy/system/entity/license/LicenseBindingSupport.java
New file @@ -0,0 +1,176 @@ package com.zy.system.entity.license; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import java.util.ArrayList; import java.util.Collections; import java.util.List; public final class LicenseBindingSupport { private LicenseBindingSupport() { } public static boolean isV2Extra(Object extra) { if (extra == null) { return false; } if (extra instanceof LicenseBindModel) { return true; } if (extra instanceof JSONObject) { return isV2Json((JSONObject) extra); } if (extra instanceof String) { String text = ((String) extra).trim(); if (isBlank(text) || !text.startsWith("{")) { return false; } return isV2Json(JSON.parseObject(text)); } return false; } public static LicenseBindModel parseBindModel(Object extra) { if (extra == null) { return null; } if (extra instanceof LicenseBindModel) { return normalizeBindModel((LicenseBindModel) extra); } if (extra instanceof JSONObject) { return normalizeBindModel(((JSONObject) extra).toJavaObject(LicenseBindModel.class)); } if (extra instanceof String) { String text = ((String) extra).trim(); if (isBlank(text)) { return null; } return normalizeBindModel(JSON.parseObject(text, LicenseBindModel.class)); } return normalizeBindModel(JSON.parseObject(JSON.toJSONString(extra), LicenseBindModel.class)); } public static boolean matches(LicenseBindModel licenseBind, LicenseCheck serverCheck) { if (licenseBind == null) { return false; } if ("UNLIMITED".equalsIgnoreCase(trimToEmpty(licenseBind.getBindMode()))) { return true; } List<LicenseNodeCheck> nodes = licenseBind.getNodes(); if (nodes == null || nodes.isEmpty()) { return true; } for (LicenseNodeCheck node : nodes) { if (matchesNode(node, serverCheck)) { return true; } } return false; } private static boolean isV2Json(JSONObject jsonObject) { if (jsonObject == null) { return false; } return jsonObject.containsKey("nodes") || jsonObject.containsKey("bindMode") || jsonObject.containsKey("matchMode") || Integer.valueOf(2).equals(jsonObject.getInteger("version")); } private static LicenseBindModel normalizeBindModel(LicenseBindModel source) { if (source == null) { return null; } LicenseBindModel target = new LicenseBindModel(); target.setVersion(source.getVersion() == null ? 2 : source.getVersion()); target.setBindMode(isBlank(source.getBindMode()) ? "MULTI_NODE" : source.getBindMode().trim()); target.setMatchMode(isBlank(source.getMatchMode()) ? "ANY" : source.getMatchMode().trim()); List<LicenseNodeCheck> normalizedNodes = new ArrayList<>(); if (source.getNodes() != null) { for (LicenseNodeCheck node : source.getNodes()) { if (node == null) { continue; } LicenseNodeCheck normalizedNode = new LicenseNodeCheck(); normalizedNode.setNodeId(trimToEmpty(node.getNodeId())); normalizedNode.setIpAddress(normalizeList(node.getIpAddress(), false)); normalizedNode.setMacAddress(normalizeList(node.getMacAddress(), true)); normalizedNode.setCpuSerial(trimToEmpty(node.getCpuSerial())); normalizedNode.setMainBoardSerial(trimToEmpty(node.getMainBoardSerial())); normalizedNodes.add(normalizedNode); } } target.setNodes(normalizedNodes); return target; } private static boolean matchesNode(LicenseNodeCheck node, LicenseCheck serverCheck) { if (node == null || serverCheck == null) { return false; } if (!checkList(node.getIpAddress(), serverCheck.getIpAddress())) { return false; } if (!checkList(node.getMacAddress(), serverCheck.getMacAddress())) { return false; } if (!checkSerial(node.getMainBoardSerial(), serverCheck.getMainBoardSerial())) { return false; } return checkSerial(node.getCpuSerial(), serverCheck.getCpuSerial()); } private static boolean checkList(List<String> expectedList, List<String> actualList) { if (expectedList == null || expectedList.isEmpty()) { return true; } if (actualList == null || actualList.isEmpty()) { return false; } for (String expected : expectedList) { if (!isBlank(expected) && actualList.contains(expected.trim())) { return true; } } return false; } private static boolean checkSerial(String expectedSerial, String actualSerial) { if (isBlank(expectedSerial)) { return true; } return !isBlank(actualSerial) && expectedSerial.equals(actualSerial); } private static List<String> normalizeList(List<String> values, boolean upperCase) { List<String> result = new ArrayList<>(); if (values == null || values.isEmpty()) { return result; } for (String value : values) { if (isBlank(value)) { continue; } String normalized = value.trim(); normalized = upperCase ? normalized.toUpperCase() : normalized.toLowerCase(); if (!result.contains(normalized)) { result.add(normalized); } } Collections.sort(result); return result; } private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private static String trimToEmpty(String value) { return value == null ? "" : value.trim(); } } src/main/java/com/zy/system/entity/license/LicenseCheckListener.java
@@ -1,6 +1,7 @@ package com.zy.system.entity.license; import com.core.common.Cools; import com.zy.system.entity.LicenseInfos; import com.zy.system.service.LicenseInfosService; import com.zy.system.timer.LicenseTimer; import de.schlichtherle.license.LicenseContent; import org.apache.logging.log4j.LogManager; @@ -12,7 +13,6 @@ import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; import java.io.File; import java.util.Date; /** @@ -40,19 +40,12 @@ @Value("${license.storePass}") private String storePass; /** * 证书生成路径 */ @Value("${license.licensePath}") private String licensePath; /** * 密钥库存储路径 */ @Value("${license.publicKeysStorePath}") private String publicKeysStorePath; @Autowired private LicenseTimer licenseTimer; @Autowired private LicenseInfosService licenseInfosService; @Override public void onApplicationEvent(ContextRefreshedEvent event) { @@ -65,53 +58,58 @@ //加载证书 public boolean loadLicense() { if(!Cools.isEmpty(licensePath)){ logger.info("++++++++ 开始加载许可证 ++++++++"); return loadLicense(true); } public boolean loadLicense(boolean fetchRemote) { logger.info("++++++++ 开始加载许可证 ++++++++"); if (fetchRemote) { try { String publicKeysStoreFileName = this.getClass().getClassLoader().getResource(publicKeysStorePath).getPath(); File publicKeysStoreFile = new File(publicKeysStoreFileName); String licensePathFileName = this.getClass().getClassLoader().getResource(licensePath).getPath(); File licensePathFile = new File(licensePathFileName); LicenseVerifyParam param = new LicenseVerifyParam(); param.setSubject(subject); param.setPublicAlias(publicAlias); param.setStorePass(storePass); param.setLicensePath(licensePath); param.setPublicKeysStorePath(publicKeysStorePath); LicenseVerify licenseVerify = new LicenseVerify(); //安装证书 LicenseContent install = licenseVerify.install(param); logger.info("++++++++ 许可证加载结束 ++++++++"); logger.info("++++++++ 许可证加载标记,搜索修改 ++++++++"); licenseTimer.setSystemSupport(true); // licenseTimer.setLicenseDays(9999); // return true; licenseTimer.setSystemSupport(install!=null); if (install != null) { Date start = new Date(); Date end = install.getNotAfter(); Long starTime = start.getTime(); Long endTime = end.getTime(); Long num = endTime - starTime;//时间戳相差的毫秒数 int day = (int) (num / 24 / 60 / 60 / 1000); licenseTimer.setLicenseDays(day); } return install != null; } catch (Exception e) { return false; licenseTimer.getRemoteLicense(); } catch (Exception ignored) { } } licenseTimer.setSystemSupport(false); return false; try { LicenseVerifyParam param = new LicenseVerifyParam(); param.setSubject(subject); param.setPublicAlias(publicAlias); param.setStorePass(storePass); param.setPublicKeysStorePath(publicKeysStorePath); LicenseVerify licenseVerify = new LicenseVerify(); String requestCode = LicenseUtils.buildRequestCode(subject); LicenseInfos latestLicense = licenseInfosService.getLatestLicenseByRequestCode(requestCode); LicenseContent install = null; if (latestLicense != null && latestLicense.getLicense() != null && !latestLicense.getLicense().trim().isEmpty()) { install = licenseVerify.install(param, latestLicense.getLicense().trim()); } if (install == null) { logger.info("许可证不存在"); licenseTimer.setSystemSupport(false); licenseTimer.setLicenseDays(0); return false; } logger.info("++++++++ 许可证加载结束 ++++++++"); licenseTimer.setSystemSupport(true); Date start = new Date(); Date end = install.getNotAfter(); Long starTime = start.getTime(); Long endTime = end.getTime(); Long num = endTime - starTime;//时间戳相差的毫秒数 int day = (int) (num / 24 / 60 / 60 / 1000); licenseTimer.setLicenseDays(day); return true; } catch (Exception e) { logger.error("许可证加载失败", e); licenseTimer.setSystemSupport(false); licenseTimer.setLicenseDays(0); return false; } } } } src/main/java/com/zy/system/entity/license/LicenseCreator.java
File was deleted src/main/java/com/zy/system/entity/license/LicenseCreatorParam.java
File was deleted src/main/java/com/zy/system/entity/license/LicenseNodeCheck.java
New file @@ -0,0 +1,22 @@ package com.zy.system.entity.license; import lombok.Data; import java.io.Serializable; import java.util.List; @Data public class LicenseNodeCheck implements Serializable { private static final long serialVersionUID = 3629488116939928951L; private String nodeId; private List<String> ipAddress; private List<String> macAddress; private String cpuSerial; private String mainBoardSerial; } src/main/java/com/zy/system/entity/license/LicenseUploadParam.java
New file @@ -0,0 +1,9 @@ package com.zy.system.entity.license; import lombok.Data; @Data public class LicenseUploadParam { private String license; } src/main/java/com/zy/system/entity/license/LicenseUtils.java
New file @@ -0,0 +1,121 @@ package com.zy.system.entity.license; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.List; public class LicenseUtils { public static LicenseCheck getServerInfos() { String osName = System.getProperty("os.name").toLowerCase(); AbstractServerInfos abstractServerInfos; if (osName.startsWith("windows")) { abstractServerInfos = new WindowsServerInfos(); } else if (osName.startsWith("linux")) { abstractServerInfos = new LinuxServerInfos(); } else { abstractServerInfos = new LinuxServerInfos(); } return abstractServerInfos.getServerInfos(); } public static String buildRequestCode(String subject) { return buildRequestCode(subject, getServerInfos()); } public static String buildRequestCode(String subject, LicenseCheck licenseCheck) { if (isBlank(subject)) { throw new IllegalArgumentException("许可证名称不能为空"); } LicenseCheck normalized = normalizeLicenseCheck(licenseCheck == null ? new LicenseCheck() : licenseCheck); JSONObject payload = new JSONObject(true); payload.put("version", 2); payload.put("subject", subject); payload.put("licenseBind", buildBindModel(normalized)); return Base64.getEncoder().encodeToString(JSON.toJSONString(payload).getBytes(StandardCharsets.UTF_8)); } private static JSONObject buildBindModel(LicenseCheck licenseCheck) { JSONObject bindModel = new JSONObject(true); bindModel.put("version", 2); bindModel.put("bindMode", "MULTI_NODE"); bindModel.put("matchMode", "ANY"); JSONObject node = new JSONObject(true); node.put("nodeId", getNodeId()); node.put("ipAddress", toJsonArray(licenseCheck.getIpAddress())); node.put("macAddress", toJsonArray(licenseCheck.getMacAddress())); node.put("cpuSerial", trimToEmpty(licenseCheck.getCpuSerial())); node.put("mainBoardSerial", trimToEmpty(licenseCheck.getMainBoardSerial())); JSONArray nodes = new JSONArray(); nodes.add(node); bindModel.put("nodes", nodes); return bindModel; } private static LicenseCheck normalizeLicenseCheck(LicenseCheck source) { LicenseCheck target = new LicenseCheck(); target.setIpAddress(normalizeList(source.getIpAddress(), false)); target.setMacAddress(normalizeList(source.getMacAddress(), true)); target.setCpuSerial(trimToEmpty(source.getCpuSerial())); target.setMainBoardSerial(trimToEmpty(source.getMainBoardSerial())); return target; } private static List<String> normalizeList(List<String> values, boolean upperCase) { List<String> result = new ArrayList<>(); if (values == null || values.isEmpty()) { return result; } for (String value : values) { if (isBlank(value)) { continue; } String normalized = value.trim(); normalized = upperCase ? normalized.toUpperCase() : normalized.toLowerCase(); if (!result.contains(normalized)) { result.add(normalized); } } Collections.sort(result); return result; } private static JSONArray toJsonArray(List<String> values) { JSONArray array = new JSONArray(); if (values != null && !values.isEmpty()) { array.addAll(values); } return array; } private static String getNodeId() { try { String hostName = InetAddress.getLocalHost().getHostName(); if (!isBlank(hostName)) { return hostName.trim(); } } catch (Exception ignored) { } return "node-1"; } private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } private static String trimToEmpty(String value) { return value == null ? "" : value.trim(); } } src/main/java/com/zy/system/entity/license/LicenseVerify.java
@@ -3,14 +3,15 @@ import de.schlichtherle.license.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tomcat.util.http.fileupload.IOUtils; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.prefs.Preferences; /** @@ -22,25 +23,19 @@ /** * 安装License证书 */ public synchronized LicenseContent install(LicenseVerifyParam param){ public synchronized LicenseContent install(LicenseVerifyParam param, String license) { LicenseContent result = null; DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //1. 安装证书 try{ try { LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param)); licenseManager.uninstall(); InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(param.getLicensePath()); File file = new File(param.getLicensePath()); try (FileOutputStream out = new FileOutputStream(file)) { IOUtils.copy(inputStream, out); } 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); File tempFileFromBase64 = createTempFileFromBase64(license); result = licenseManager.install(tempFileFromBase64); logger.info(MessageFormat.format("许可证加载成功,许可证有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter()))); } catch (Exception e) { logger.error("许可证加载失败!", e); } return result; @@ -53,11 +48,6 @@ try { LicenseManager licenseManager = LicenseManagerHolder.getInstance(null); DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if (!updateSystemTime()) { //时间更新失败,系统时间被更改 return false; } LicenseContent licenseContent = licenseManager.verify(); logger.info(MessageFormat.format("许可证校验通过,许可证有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter()))); @@ -73,11 +63,6 @@ */ public LicenseContent getVerifyInfo(){ LicenseManager licenseManager = LicenseManagerHolder.getInstance(null); if (!updateSystemTime()) { //时间更新失败,系统时间被更改 return null; } //校验证书 try { @@ -111,32 +96,17 @@ ,cipherParam); } /** * 更新时间到注册表中 */ private boolean updateSystemTime() { // 获取用户根节点 Preferences userRoot = Preferences.userRoot(); // 获取指定路径下的节点 Preferences node = userRoot.node("/zhongyang"); String key = "time"; // 读取注册表 String value = node.get(key, null); if (value != null) { long originTime = Long.parseLong(value); long now = System.currentTimeMillis(); long diff = now - originTime;//现在时间 - 源时间 = 时间差 if (diff > 0) { //时间差大于0才允许更新注册表时间 node.put(key, String.valueOf(System.currentTimeMillis())); return true; } }else { // 写入注册表 node.put(key, String.valueOf(System.currentTimeMillis())); return true; } return false; public File base64ToTempFile(String base64String, String filePrefix, String fileSuffix) throws IOException { byte[] decodedBytes = Base64.getDecoder().decode(base64String); Path tempPath = Files.createTempFile(filePrefix, fileSuffix); Files.write(tempPath, decodedBytes); tempPath.toFile().deleteOnExit(); return tempPath.toFile(); } } public File createTempFileFromBase64(String base64Data) throws IOException { return base64ToTempFile(base64Data, "temp_license_", ".bin"); } } src/main/java/com/zy/system/entity/license/LicenseVerifyParam.java
@@ -24,11 +24,6 @@ private String storePass; /** * 证书生成路径 */ private String licensePath; /** * 密钥库存储路径 */ private String publicKeysStorePath; @@ -37,11 +32,10 @@ } public LicenseVerifyParam(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) { public LicenseVerifyParam(String subject, String publicAlias, String storePass, String publicKeysStorePath) { this.subject = subject; this.publicAlias = publicAlias; this.storePass = storePass; this.licensePath = licensePath; this.publicKeysStorePath = publicKeysStorePath; } src/main/java/com/zy/system/entity/license/LinuxServerInfos.java
New file @@ -0,0 +1,65 @@ package com.zy.system.entity.license; import com.core.common.Cools; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.InetAddress; import java.util.List; import java.util.stream.Collectors; public class LinuxServerInfos extends AbstractServerInfos { @Override protected List<String> getIpAddress() throws Exception { List<InetAddress> inetAddresses = getLocalAllInetAddress(); if (inetAddresses == null || inetAddresses.isEmpty()) { return null; } return inetAddresses.stream() .map(InetAddress::getHostAddress) .distinct() .map(String::toLowerCase) .collect(Collectors.toList()); } @Override protected List<String> getMacAddress() throws Exception { List<InetAddress> inetAddresses = getLocalAllInetAddress(); if (inetAddresses == null || inetAddresses.isEmpty()) { return null; } return inetAddresses.stream() .map(this::getMacByInetAddress) .filter(mac -> mac != null && !mac.trim().isEmpty()) .distinct() .collect(Collectors.toList()); } @Override protected String getCPUSerial() throws Exception { return readFirstNonBlankLine(new String[]{"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"}); } @Override protected String getMainBoardSerial() throws Exception { return readFirstNonBlankLine(new String[]{"/bin/bash", "-c", "dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"}); } private String readFirstNonBlankLine(String[] shell) throws Exception { Process process = Runtime.getRuntime().exec(shell); process.getOutputStream().close(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (!Cools.isEmpty(line)) { return line; } } } return ""; } } src/main/java/com/zy/system/entity/license/WindowsServerInfos.java
@@ -1,8 +1,10 @@ package com.zy.system.entity.license; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.InetAddress; import java.nio.charset.Charset; import java.util.List; import java.util.Scanner; import java.util.stream.Collectors; /** @@ -33,7 +35,11 @@ if(inetAddresses != null && inetAddresses.size() > 0){ //2. 获取所有网络接口的Mac地址 result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList()); result = inetAddresses.stream() .map(this::getMacByInetAddress) .filter(mac -> mac != null && !mac.trim().isEmpty()) .distinct() .collect(Collectors.toList()); } return result; @@ -41,45 +47,72 @@ @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(); String serialNumber = readCommandValue("wmic", "cpu", "get", "processorid"); if (!isBlank(serialNumber)) { return serialNumber; } if(scanner.hasNext()){ serialNumber = scanner.next().trim(); } scanner.close(); return serialNumber; return readCommandValue("powershell", "-NoProfile", "-Command", "(Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty ProcessorId)"); } @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(); String serialNumber = readCommandValue("wmic", "baseboard", "get", "serialnumber"); if (!isBlank(serialNumber)) { return serialNumber; } if(scanner.hasNext()){ serialNumber = scanner.next().trim(); } scanner.close(); return serialNumber; return readCommandValue("powershell", "-NoProfile", "-Command", "(Get-CimInstance Win32_BaseBoard | Select-Object -First 1 -ExpandProperty SerialNumber)"); } } private String readCommandValue(String... command) { try { ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.redirectErrorStream(true); Process process = processBuilder.start(); process.getOutputStream().close(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), Charset.forName("GBK")))) { String line; while ((line = reader.readLine()) != null) { String value = normalizeCommandOutput(line); if (!isBlank(value)) { return value; } } } } catch (Exception ignored) { } return ""; } private String normalizeCommandOutput(String line) { if (line == null) { return ""; } String value = line.trim(); if (isBlank(value)) { return ""; } String lower = value.toLowerCase(); if ("processorid".equals(lower) || "serialnumber".equals(lower)) { return ""; } if (lower.contains("access denied") || lower.contains("拒绝访问") || lower.contains("get-ciminstance") || lower.contains("fullyqualifiederrorid") || lower.contains("at line:") || lower.contains("categoryinfo") || lower.contains("cimexception") || lower.contains("createprocess error")) { return ""; } return value; } private boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } } src/main/java/com/zy/system/mapper/LicenseInfosMapper.java
New file @@ -0,0 +1,14 @@ package com.zy.system.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.system.entity.LicenseInfos; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @Mapper @Repository public interface LicenseInfosMapper extends BaseMapper<LicenseInfos> { LicenseInfos getLatestLicenseByRequestCode(@Param("requestCode") String requestCode); } src/main/java/com/zy/system/service/LicenseInfosService.java
New file @@ -0,0 +1,9 @@ package com.zy.system.service; import com.baomidou.mybatisplus.service.IService; import com.zy.system.entity.LicenseInfos; public interface LicenseInfosService extends IService<LicenseInfos> { LicenseInfos getLatestLicenseByRequestCode(String requestCode); } src/main/java/com/zy/system/service/impl/LicenseInfosServiceImpl.java
New file @@ -0,0 +1,16 @@ package com.zy.system.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.system.entity.LicenseInfos; import com.zy.system.mapper.LicenseInfosMapper; import com.zy.system.service.LicenseInfosService; import org.springframework.stereotype.Service; @Service("licenseInfosService") public class LicenseInfosServiceImpl extends ServiceImpl<LicenseInfosMapper, LicenseInfos> implements LicenseInfosService { @Override public LicenseInfos getLatestLicenseByRequestCode(String requestCode) { return this.baseMapper.getLatestLicenseByRequestCode(requestCode); } } src/main/java/com/zy/system/timer/LicenseTimer.java
@@ -1,24 +1,142 @@ package com.zy.system.timer; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.core.common.Cools; import com.zy.common.utils.HttpHandler; import com.zy.system.entity.LicenseInfos; import com.zy.system.entity.license.LicenseUtils; import com.zy.system.entity.license.LicenseVerify; import com.zy.system.entity.license.LicenseVerifyParam; import com.zy.system.service.LicenseInfosService; import de.schlichtherle.license.LicenseContent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; @Component public class LicenseTimer { private static boolean SYSTEM_SUPPORT = true;//系统激活状态,默认关闭 private static boolean SYSTEM_SUPPORT = false;//系统激活状态,默认关闭 private static int LICENSE_DAYS = 0;//许可证天数 @Value("${license.subject}") private String subject; @Value("${license.publicAlias}") private String publicAlias; @Value("${license.storePass}") private String storePass; @Value("${license.publicKeysStorePath}") private String publicKeysStorePath; @Value("${license.remoteServerUrl:http://net.zoneyung.net:9999/license}") private String remoteServerUrl; @Autowired private LicenseInfosService licenseInfosService; //每天晚上11点更新系统激活状态 @Scheduled(cron = "0 0 23 * * ? ") public void timer() { // System.out.println(SYSTEM_SUPPORT); //验证许可证是否有效 try { getRemoteLicense(); } catch (Exception ignored) { } try { verify(); } catch (Exception ignored) { } } public void getRemoteLicense() { try { String requestCode = LicenseUtils.buildRequestCode(subject); JSONObject response = requestRemoteLicense(buildRequestCodePayload(requestCode)); if (isSuccess(response)) { String license = response.getString("data"); if (Cools.isEmpty(license)) { return; } LicenseInfos latestLicense = licenseInfosService.getLatestLicenseByRequestCode(requestCode); if (latestLicense != null && Cools.eq(latestLicense.getLicense(), license)) { return; } LicenseInfos licenseInfos = new LicenseInfos(); licenseInfos.setLicense(license); licenseInfos.setCreateTime(new Date()); licenseInfos.setLicenseTime(response.getString("licenseTime")); licenseInfos.setRequestCode(requestCode); licenseInfosService.insert(licenseInfos); } } catch (Exception e) { e.printStackTrace(); } } private JSONObject requestRemoteLicense(String json) { try { String response = new HttpHandler.Builder() .setUri(remoteServerUrl) .setPath("/remoteQueryLicense") .setJson(json) .build() .doPost(); if (response == null || response.trim().isEmpty()) { return null; } return JSON.parseObject(response); } catch (Exception e) { return null; } } private String buildRequestCodePayload(String requestCode) { HashMap<String, Object> map = new HashMap<>(); map.put("subject", subject); map.put("requestCode", requestCode); return JSON.toJSONString(map); } private boolean isSuccess(JSONObject jsonObject) { return jsonObject != null && "ok".equalsIgnoreCase(jsonObject.getString("result")); } public void verify() { LicenseVerifyParam param = new LicenseVerifyParam(); param.setSubject(subject); param.setPublicAlias(publicAlias); param.setStorePass(storePass); param.setPublicKeysStorePath(publicKeysStorePath); String requestCode = LicenseUtils.buildRequestCode(subject); LicenseInfos latestLicense = licenseInfosService.getLatestLicenseByRequestCode(requestCode); LicenseVerify licenseVerify = new LicenseVerify(); boolean verify = licenseVerify.verify(); setSystemSupport(verify);//更新系统激活状态 LicenseContent install = null; if (latestLicense != null && !Cools.isEmpty(latestLicense.getLicense())) { install = licenseVerify.install(param, latestLicense.getLicense()); } if (install != null) { Date start = new Date(); Date end = install.getNotAfter(); long num = end.getTime() - start.getTime(); int day = (int) (num / 24 / 60 / 60 / 1000); setLicenseDays(day); setSystemSupport(true); } else { setLicenseDays(0); setSystemSupport(false); } } public boolean getSystemSupport() { src/main/resources/application-dev.yml
@@ -48,10 +48,9 @@ #License相关配置 license: subject: jxhtasrs subject: jxhcasrs publicAlias: publicCert storePass: public_zhongyang_123456789 licensePath: license.lic publicKeysStorePath: publicCerts.keystore # 下位机配置 src/main/resources/application-prod.yml
@@ -63,7 +63,6 @@ subject: integrationasrs publicAlias: publicCert storePass: public_zhongyang_123456789 licensePath: license.lic publicKeysStorePath: publicCerts.keystore # 下位机配置 @@ -122,4 +121,4 @@ acctID: "647e849ab6fa0f" username: "llw" password: "666666" lcid: 2052 lcid: 2052 src/main/resources/license.licBinary files differ
src/main/resources/mapper/LicenseInfosMapper.xml
New file @@ -0,0 +1,17 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zy.system.mapper.LicenseInfosMapper"> <resultMap id="BaseResultMap" type="com.zy.system.entity.LicenseInfos"> <id column="id" property="id"/> <result column="license" property="license"/> <result column="license_time" property="licenseTime"/> <result column="request_code" property="requestCode"/> <result column="create_time" property="createTime"/> </resultMap> <select id="getLatestLicenseByRequestCode" resultMap="BaseResultMap"> select top 1 * from sys_license_infos where request_code = #{requestCode} order by create_time desc </select> </mapper> src/main/webapp/views/index.html
@@ -92,6 +92,26 @@ <script type="text/javascript" src="../static/js/common.js"></script> <script> // console.log('%c 中扬立库平台 %c 1.0.0','background-color:rgb(53,73,94);color: #fff;border-radius:2px 0 0 2px;padding:2px 4px;','background-color:rgb(25,190,107);color: #fff;border-radius:0 2px 2px 0;padding:2px 4px;font: 9pt "Apercu Regular", Georgia, "Times New Roman", Times, serif;'); function getResponseValue(res) { if (!res) { return ""; } if (typeof res.data !== "undefined" && res.data !== null && res.data !== "") { return res.data; } if (typeof res.msg !== "undefined" && res.msg !== null && res.msg !== "") { return res.msg; } return ""; } function redirectToLicensePage(message) { sessionStorage.setItem("licensePanelAutoOpen", "1"); sessionStorage.setItem("licenseInvalidReason", message || "系统当前许可证无效,请重新导入新许可证。"); localStorage.removeItem('token'); top.location.href = "login.html?license=invalid"; } $(function () { if ("" === localStorage.getItem('token')) { top.location.href = baseUrl + "/login"; @@ -146,13 +166,17 @@ method: 'POST', success: function (res) { if (res.code == 200) { let days = res.data let days = Number(getResponseValue(res)) if (days < 0) { redirectToLicensePage('系统当前未检测到可用许可证,请重新导入新的许可证。'); return; } if (days <= 30) { $("#licenseShow").show() $("#licenseDays").html(days) } }else { top.location.href = baseUrl + "/login"; redirectToLicensePage(res.msg || '许可证校验失败,请重新导入新的许可证。'); } } }); src/main/webapp/views/login.html
@@ -46,6 +46,178 @@ text-align: center; } .license-entry-text { width: 80%; margin: 0 auto 12px; color: #5e5e5e; font-size: 12px; line-height: 1.7; text-align: left; } .system-tool-shell { background: #ffffff; border-radius: 22px; overflow: hidden; } .system-tool-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; border-bottom: 1px solid #dbe4ef; } .system-tool-title { color: #243447; font-size: 18px; font-weight: 700; } .system-tool-close { color: #97a5b4; font-size: 24px; line-height: 1; cursor: pointer; user-select: none; } .system-tool-body { padding: 18px 20px 22px; background: #ffffff; } .system-tool-group + .system-tool-group { margin-top: 18px; padding-top: 18px; border-top: 1px solid #e8eef5; } .system-tool-group-title { color: #34495e; font-size: 14px; font-weight: 700; margin-bottom: 14px; } .system-tool-actions { display: flex; flex-wrap: wrap; gap: 12px 18px; } .system-tool-btn { min-width: 104px; height: 34px; padding: 0 18px; border: 1px solid #d7dfea; border-radius: 4px; background: #ffffff; color: #4d5d6d; font-size: 14px; cursor: pointer; box-sizing: border-box; } .system-tool-btn.primary { color: #ffffff; background: #5da8ff; border-color: #5da8ff; } .system-tool-btn.secondary { color: #7eb4ef; background: #d9ecff; border-color: #aed4ff; } .system-tool-desc { margin-top: 12px; color: #9aa8b6; font-size: 12px; line-height: 1.7; } .system-dialog-shell { background: #ffffff; border-radius: 18px; overflow: hidden; } .system-dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #e1e8f0; background: #f8fbff; } .system-dialog-title { color: #243447; font-size: 16px; font-weight: 700; } .system-dialog-close { color: #97a5b4; font-size: 22px; line-height: 1; cursor: pointer; user-select: none; } .system-dialog-body { padding: 18px; background: #ffffff; } .system-dialog-label { color: #43576b; font-size: 13px; font-weight: 700; margin-bottom: 8px; } .system-dialog-tip { margin-bottom: 10px; color: #8c9aac; font-size: 12px; line-height: 1.7; } .system-dialog-textarea { width: 100%; min-height: 220px; resize: none; border: 1px solid #d7dfea; border-radius: 8px; padding: 10px 12px; box-sizing: border-box; color: #4d5d6d; background: #fbfdff; font-size: 13px; line-height: 1.7; } .system-dialog-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; } body .system-tool-popup { border-radius: 22px !important; overflow: hidden !important; } body .system-tool-popup .layui-layer-content { overflow: hidden !important; background: transparent; } </style> </head> <body> @@ -90,10 +262,8 @@ <button class="login100-form-btn login-btn">Login</button> </div> <div class="container-login100-form-btn p-t-10" style="display: none;margin-top: 50px;" id="updateLicense"> <form enctype="multipart/form-data" style="display: none;"> <input id="license" type="file" name="file"> </form> <button class="login100-form-btn" id="submitLicense">更新许可证</button> <div class="license-entry-text" id="licenseEntryMessage">系统当前未检测到可用许可证,请打开系统工具处理许可证。</div> <button class="login100-form-btn" id="openLicenseTool" type="button">系统工具</button> </div> </div> </div> @@ -115,10 +285,417 @@ if (oldPass) { $('#password').val(oldPass); } autoShowLicenseTool(); }) window.onload = function () { document.getElementById("username").focus(); } var licenseToolState = { toolLayerIndex: null, textLayerIndex: null, uploadLayerIndex: null }; function getQueryValue(name) { var query = window.location.search.substring(1).split("&"); for (var i = 0; i < query.length; i++) { var item = query[i].split("="); if (item[0] === name) { return decodeURIComponent(item[1] || ""); } } return ""; } function autoShowLicenseTool() { var needOpen = getQueryValue("license") === "invalid" || sessionStorage.getItem("licensePanelAutoOpen") === "1"; if (!needOpen) { return; } var message = sessionStorage.getItem("licenseInvalidReason") || "系统当前未检测到可用许可证,请先导入新的许可证。"; showLicenseEntry(message); openLicenseTool(message); } function showLicenseEntry(message) { $("#updateLicense").show(); if (message) { $("#licenseEntryMessage").text(message); } } function escapeHtml(text) { return String(text || "") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function buildLicenseToolContent() { return '' + '<div class="system-tool-shell">' + ' <div class="system-tool-header">' + ' <div class="system-tool-title">系统工具</div>' + ' <span class="system-tool-close" id="toolClose">×</span>' + ' </div>' + ' <div class="system-tool-body">' + ' <div class="system-tool-group">' + ' <div class="system-tool-group-title">推荐操作</div>' + ' <div class="system-tool-actions">' + ' <button type="button" class="system-tool-btn primary" id="toolRequestCode">获取请求码</button>' + ' <button type="button" class="system-tool-btn secondary" id="toolActivate">一键激活</button>' + ' </div>' + ' <div class="system-tool-desc">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div>' + ' </div>' + ' <div class="system-tool-group">' + ' <div class="system-tool-group-title">其他工具</div>' + ' <div class="system-tool-actions">' + ' <button type="button" class="system-tool-btn" id="toolProjectName">获取项目名称</button>' + ' <button type="button" class="system-tool-btn" id="toolServerInfo">获取系统配置</button>' + ' <button type="button" class="system-tool-btn" id="toolInputLicense">录入许可证</button>' + ' </div>' + ' </div>' + ' </div>' + '</div>'; } function openLicenseTool(message) { var toolMessage = message || $("#licenseEntryMessage").text() || "系统当前未检测到可用许可证,请先导入新的许可证。"; var toolWidth = Math.min($(window).width() - 32, 760); showLicenseEntry(toolMessage); if (licenseToolState.toolLayerIndex !== null) { layer.close(licenseToolState.toolLayerIndex); } licenseToolState.toolLayerIndex = layer.open({ type: 1, title: false, closeBtn: 0, area: [Math.min(toolWidth, 560) + 'px', 'auto'], shadeClose: false, skin: 'system-tool-popup', content: buildLicenseToolContent(), success: function (layero, index) { licenseToolState.toolLayerIndex = index; bindLicenseToolEvents(layero); }, end: function () { licenseToolState.toolLayerIndex = null; } }); } function bindLicenseToolEvents(layero) { layero.find("#toolClose").on("click", function () { layer.close(licenseToolState.toolLayerIndex); }); layero.find("#toolRequestCode").on("click", function () { requestCode(); }); layero.find("#toolActivate").on("click", function () { activateLicense(); }); layero.find("#toolProjectName").on("click", function () { getProjectName(); }); layero.find("#toolServerInfo").on("click", function () { getServerInfo(); }); layero.find("#toolInputLicense").on("click", function () { openLicenseInputDialog(); }); } function getResponseValue(res) { if (!res) { return ""; } if (typeof res.data !== "undefined" && res.data !== null && res.data !== "") { return res.data; } if (typeof res.msg !== "undefined" && res.msg !== null && res.msg !== "") { return res.msg; } return ""; } function openTextDialog(title, label, text, tip) { var prettyText = ""; try { prettyText = typeof text === "string" ? text : JSON.stringify(text, null, 2); } catch (e) { prettyText = String(text || ""); } if (licenseToolState.textLayerIndex !== null) { layer.close(licenseToolState.textLayerIndex); } licenseToolState.textLayerIndex = layer.open({ type: 1, title: false, closeBtn: 0, area: [Math.min($(window).width() - 32, 720) + 'px', 'auto'], shadeClose: false, skin: 'system-tool-popup', content: '' + '<div class="system-dialog-shell">' + ' <div class="system-dialog-header">' + ' <div class="system-dialog-title">' + escapeHtml(title) + '</div>' + ' <span class="system-dialog-close" id="systemTextClose">×</span>' + ' </div>' + ' <div class="system-dialog-body">' + ' <div class="system-dialog-label">' + escapeHtml(label) + '</div>' + (tip ? '<div class="system-dialog-tip">' + escapeHtml(tip) + '</div>' : '') + ' <textarea class="system-dialog-textarea" id="systemDialogText" readonly></textarea>' + ' <div class="system-dialog-footer">' + ' <button type="button" class="system-tool-btn" id="systemTextCloseBtn">关闭</button>' + ' <button type="button" class="system-tool-btn primary" id="systemTextCopy">复制</button>' + ' </div>' + ' </div>' + '</div>', success: function (layero, index) { licenseToolState.textLayerIndex = index; layero.find("#systemDialogText").val(prettyText); layero.find("#systemTextClose, #systemTextCloseBtn").on("click", function () { layer.close(index); }); layero.find("#systemTextCopy").on("click", function () { copyRequestCodeText(prettyText); }); }, end: function () { licenseToolState.textLayerIndex = null; } }); } function openLicenseInputDialog() { if (licenseToolState.uploadLayerIndex !== null) { layer.close(licenseToolState.uploadLayerIndex); } licenseToolState.uploadLayerIndex = layer.open({ type: 1, title: false, closeBtn: 0, area: [Math.min($(window).width() - 32, 760) + 'px', 'auto'], shadeClose: false, skin: 'system-tool-popup', content: '' + '<div class="system-dialog-shell">' + ' <div class="system-dialog-header">' + ' <div class="system-dialog-title">录入许可证</div>' + ' <span class="system-dialog-close" id="licenseInputClose">×</span>' + ' </div>' + ' <div class="system-dialog-body">' + ' <div class="system-dialog-label">许可证 Base64</div>' + ' <div class="system-dialog-tip">将许可证服务端返回的 license 字段完整粘贴到这里。</div>' + ' <textarea class="system-dialog-textarea" id="licenseInputValue"></textarea>' + ' <div class="system-dialog-footer">' + ' <button type="button" class="system-tool-btn" id="licenseInputCancel">取消</button>' + ' <button type="button" class="system-tool-btn primary" id="licenseInputSubmit">提交</button>' + ' </div>' + ' </div>' + '</div>', success: function (layero, index) { licenseToolState.uploadLayerIndex = index; layero.find("#licenseInputClose, #licenseInputCancel").on("click", function () { layer.close(index); }); layero.find("#licenseInputSubmit").on("click", function () { submitLicense(layero.find("#licenseInputValue").val()); }); }, end: function () { licenseToolState.uploadLayerIndex = null; } }); } function requestCode() { fetchRequestCode(function (value, errorMsg) { if (value) { openTextDialog("获取请求码", "请求码", value, "请求码中已包含项目名称,直接发给许可证服务端即可。"); return; } layer.msg(errorMsg || '获取请求码失败', {time: 2000}); }); } function fetchRequestCode(callback) { $.ajax({ url: baseUrl + "/license/getRequestCode", method: 'GET', success: function (res) { var value = getResponseValue(res); if (res.code == 200) { callback(value ? String(value) : "", ""); return; } callback("", res.msg || '获取请求码失败'); }, error: function (xhr) { callback("", xhr && xhr.status === 404 ? "获取请求码接口不存在" : "获取请求码失败"); } }) } function getProjectName() { $.ajax({ url: baseUrl + "/license/getProjectName", method: 'GET', success: function (res) { var value = getResponseValue(res); if (res.code == 200 && value) { layer.alert(escapeHtml(String(value)), { title: '项目名称' }); return; } layer.msg(res.msg || '获取项目名称失败', {time: 2000}); }, error: function () { layer.msg('获取项目名称失败', {time: 2000}); } }) } function getServerInfo() { $.ajax({ url: baseUrl + "/license/getServerInfos", method: 'GET', success: function (res) { openTextDialog("获取系统配置", "系统配置信息", res, "新许可证模式下可用于排查当前节点硬件绑定信息。"); }, error: function () { fallbackServerInfoFromRequestCode(); } }) } function fallbackServerInfoFromRequestCode() { fetchRequestCode(function (requestCode, errorMsg) { if (!requestCode) { layer.msg(errorMsg || '获取系统配置信息失败', {time: 2000}); return; } try { var decoded = decodeRequestCodePayload(requestCode); var node = (((decoded || {}).licenseBind || {}).nodes || [])[0] || {}; var serverInfo = { subject: decoded.subject || "", nodeId: node.nodeId || "", ipAddress: node.ipAddress || [], macAddress: node.macAddress || [], cpuSerial: node.cpuSerial || "", mainBoardSerial: node.mainBoardSerial || "" }; openTextDialog("获取系统配置", "系统配置信息", serverInfo, "当前接口不可用,已按请求码解析出本机节点配置。"); } catch (e) { layer.msg('获取系统配置信息失败', {time: 2000}); } }); } function decodeRequestCodePayload(requestCode) { var normalized = String(requestCode || "").replace(/\s+/g, ""); if (!normalized) { return {}; } var binary = window.atob(normalized); var bytes = []; for (var i = 0; i < binary.length; i++) { bytes.push("%" + ("00" + binary.charCodeAt(i).toString(16)).slice(-2)); } return JSON.parse(decodeURIComponent(bytes.join(""))); } function activateLicense() { layer.confirm('确定执行一键激活吗?', { title: '提示' }, function (index) { layer.close(index); $.ajax({ url: baseUrl + "/license/activate", method: 'POST', success: function (res) { if (res.code == 200) { layer.msg('激活成功', {time: 1500}); return; } layer.msg(res.msg || '激活失败', {time: 2000}); }, error: function () { layer.msg('激活失败', {time: 2000}); } }) }); } function submitLicense(licenseBase64) { if (!licenseBase64 || !licenseBase64.trim()) { layer.msg('许可证内容不能为空', {time: 1500}); return; } $.ajax({ url: baseUrl + "/license/updateLicense", headers: {'Content-Type': 'application/json'}, data: JSON.stringify({license: licenseBase64.trim()}), method: 'POST', success: function (res) { if (res.code == 200) { sessionStorage.removeItem("licensePanelAutoOpen"); sessionStorage.removeItem("licenseInvalidReason"); if (licenseToolState.uploadLayerIndex !== null) { layer.close(licenseToolState.uploadLayerIndex); } layer.msg('许可证更新成功', {time: 1000}, function () { parent.location.reload(); }); return; } layer.msg(res.msg || '许可证更新失败', {time: 2000}); }, error: function (xhr) { if (xhr && xhr.status === 403) { layer.msg('许可证录入接口被拦截,请重启服务后重试', {time: 2500}); return; } layer.msg('许可证录入失败', {time: 2000}); } }); } function copyRequestCodeText(text) { if (!text) { layer.msg('请求码为空', {time: 1500}); return; } if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(function () { layer.msg('请求码已复制', {time: 1200}); }, function () { fallbackCopy(text); }); return; } fallbackCopy(text); } function fallbackCopy(text) { var copyInput = $("<textarea>").val(text).css({ position: "fixed", top: "-1000px" }).appendTo("body"); copyInput[0].select(); try { document.execCommand("copy"); layer.msg('请求码已复制', {time: 1200}); } catch (e) { layer.msg('复制失败,请手动复制', {time: 1500}); } copyInput.remove(); } $(document).on('click', '.login-btn', function () { @@ -151,7 +728,8 @@ layer.tips(res.msg, '#password', {tips: [4, '#ff0000']}); } else if (res.code == 20001) { layer.tips(res.msg, '.login-btn', {tips: [3, '#ff0000']}); $("#updateLicense").show() showLicenseEntry(res.msg) openLicenseTool(res.msg) } else { layer.tips(res.msg, '.login-btn', {tips: [3, '#ff0000']}); } @@ -165,37 +743,8 @@ } }); //更新许可证 $("#submitLicense").on("click", () => { $("#license").click() }) //上传并更新许可证 $("#license").on("change", (evt) => { var files = evt.target.files; let formData = new FormData(); formData.append("file", files[0]) $.ajax({ url: baseUrl + "/license/updateLicense", headers: {'token': localStorage.getItem('token')}, data: formData, method: 'POST', cache: false, processData: false, contentType: false, success: function (res) { if (res.code == 200) { layer.msg('更新成功', {time: 1000}, () => { parent.location.reload() }); } else { layer.msg(res.msg, {time: 2000}, () => { parent.location.reload() }) } } }) $("#openLicenseTool").on("click", () => { openLicenseTool($("#licenseEntryMessage").text()); }) </script> </body>