增加许可证认证
This commit is contained in:
parent
023ea78d18
commit
f577cbcbe5
|
|
@ -13,3 +13,19 @@ INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `ord
|
|||
INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2012, '客户列表数据导出', 2001, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:customerExport:export', '#', 'admin', '2026-02-07 15:39:03', '', NULL, '');
|
||||
|
||||
INSERT INTO `excel-handle`.`sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (3000, '企业信息', 2000, 4, 'corpInfo', 'wecom/corpinfo/index', NULL, '', 1, 0, 'C', '0', '0', 'wecom:corpInfo:list', 'download', 'admin', '2026-02-07 15:39:03', '', NULL, '企业信息');
|
||||
|
||||
|
||||
|
||||
|
||||
INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息查看', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:add', '#', 'admin', '2026-02-07 15:39:03', '', NULL, '');
|
||||
|
||||
|
||||
|
||||
INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息查看', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:query', '#', 'admin', '2026-02-07 15:39:03', '', NULL, '');
|
||||
|
||||
|
||||
INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息编辑', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:edit', '#', 'admin', '2026-02-07 15:39:03', '', NULL, '');
|
||||
|
||||
|
||||
|
||||
INSERT INTO `excel-handle`.`sys_menu` ( `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES ('企业信息删除', 3000, 2, '#', '', NULL, '', 1, 0, 'F', '0', '0', 'wecom:corp:remove', '#', 'admin', '2026-02-07 15:39:03', '', NULL, '');
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
druid:
|
||||
# 主库数据源
|
||||
master:
|
||||
url: jdbc:mysql://host.docker.internal:3316/excel-handle?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: jiong1114
|
||||
# 从库数据源
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
enabled: false
|
||||
url:
|
||||
username:
|
||||
password:
|
||||
# 初始连接数
|
||||
initialSize: 5
|
||||
# 最小连接池数量
|
||||
minIdle: 10
|
||||
# 最大连接池数量
|
||||
maxActive: 20
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置连接超时时间
|
||||
connectTimeout: 30000
|
||||
# 配置网络超时时间
|
||||
socketTimeout: 60000
|
||||
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
|
||||
timeBetweenEvictionRunsMillis: 60000
|
||||
# 配置一个连接在池中最小生存的时间,单位是毫秒
|
||||
minEvictableIdleTimeMillis: 300000
|
||||
# 配置一个连接在池中最大生存的时间,单位是毫秒
|
||||
maxEvictableIdleTimeMillis: 900000
|
||||
# 配置检测连接是否有效
|
||||
validationQuery: SELECT 1 FROM DUAL
|
||||
testWhileIdle: true
|
||||
testOnBorrow: false
|
||||
testOnReturn: false
|
||||
webStatFilter:
|
||||
enabled: true
|
||||
statViewServlet:
|
||||
enabled: true
|
||||
# 设置白名单,不填则允许所有访问
|
||||
allow:
|
||||
url-pattern: /druid/*
|
||||
# 控制台管理用户名和密码
|
||||
login-username: ruoyi
|
||||
login-password: 123456
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
# 慢SQL记录
|
||||
log-slow-sql: true
|
||||
slow-sql-millis: 1000
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
|
||||
redis:
|
||||
# 地址
|
||||
host: host.docker.internal
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# 密码
|
||||
password:
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 8
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
|
@ -58,4 +58,25 @@ spring:
|
|||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
multi-statement-allow: true
|
||||
redis:
|
||||
# 地址
|
||||
host: localhost
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# 密码
|
||||
password:
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 8
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
|
|
|||
|
|
@ -58,4 +58,25 @@ spring:
|
|||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
multi-statement-allow: true
|
||||
redis:
|
||||
# 地址
|
||||
host: redis
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# 密码
|
||||
password: ash@szmp
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 8
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
|
@ -66,28 +66,6 @@ spring:
|
|||
restart:
|
||||
# 热部署开关
|
||||
enabled: true
|
||||
# redis 配置
|
||||
redis:
|
||||
# 地址
|
||||
host: redis
|
||||
# 端口,默认为6379
|
||||
port: 6379
|
||||
# 数据库索引
|
||||
database: 0
|
||||
# 密码
|
||||
password: ash@szmp
|
||||
# 连接超时时间
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
# 连接池中的最小空闲连接
|
||||
min-idle: 0
|
||||
# 连接池中的最大空闲连接
|
||||
max-idle: 8
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
|
||||
# token配置
|
||||
token:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.ruoyi.common.core.domain.entity;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
|
||||
/**
|
||||
* 系统许可证对象 sys_license(加密版)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
public class SysLicense
|
||||
{
|
||||
/** 加密的许可证密钥(包含激活时间、到期时间) */
|
||||
@Excel(name = "许可证密钥")
|
||||
private String licenseKey;
|
||||
|
||||
|
||||
public String getLicenseKey() {
|
||||
return licenseKey;
|
||||
}
|
||||
|
||||
public void setLicenseKey(String licenseKey) {
|
||||
this.licenseKey = licenseKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package com.ruoyi.common.utils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 许可证加密工具类
|
||||
* 使用AES加密算法生成和验证许可证密钥
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
public class LicenseEncryptUtil {
|
||||
// AES加密密钥(生产环境应该使用更复杂的密钥,并妥善保管)
|
||||
private static final String SECRET_KEY = "RuoYi@2026#License$Key!Secure*9527";
|
||||
|
||||
// 加密算法
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
// 字段分隔符
|
||||
private static final String SEPARATOR = "|";
|
||||
|
||||
/**
|
||||
* 生成许可证密钥
|
||||
*
|
||||
* @param activationTime 激活时间(时间戳)
|
||||
* @param expiryTime 到期时间(时间戳)
|
||||
* @param trialDays 试用天数
|
||||
* @return 加密后的许可证密钥
|
||||
*/
|
||||
public static String generateLicenseKey(long activationTime, long expiryTime, int trialDays) {
|
||||
try {
|
||||
// 构建原始数据:激活时间|到期时间|机器码|试用天数|校验码
|
||||
String rawData = activationTime + SEPARATOR +
|
||||
expiryTime + SEPARATOR +
|
||||
trialDays + SEPARATOR +
|
||||
generateChecksum(activationTime, expiryTime, trialDays);
|
||||
|
||||
// AES加密
|
||||
byte[] encrypted = encrypt(rawData);
|
||||
|
||||
// Base64编码
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("生成许可证密钥失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析许可证密钥
|
||||
*
|
||||
* @param licenseKey 加密的许可证密钥
|
||||
* @return 包含许可证信息的Map,如果解析失败返回null
|
||||
*/
|
||||
public static Map<String, Object> parseLicenseKey(String licenseKey) {
|
||||
try {
|
||||
// Base64解码
|
||||
byte[] decoded = Base64.getDecoder().decode(licenseKey);
|
||||
|
||||
// AES解密
|
||||
String rawData = decrypt(decoded);
|
||||
|
||||
// 分割数据
|
||||
String[] parts = rawData.split("\\" + SEPARATOR);
|
||||
if (parts.length != 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long activationTime = Long.parseLong(parts[0]);
|
||||
long expiryTime = Long.parseLong(parts[1]);
|
||||
int trialDays = Integer.parseInt(parts[2]);
|
||||
String checksum = parts[3];
|
||||
|
||||
// 验证校验码
|
||||
String expectedChecksum = generateChecksum(activationTime, expiryTime, trialDays);
|
||||
if (!checksum.equals(expectedChecksum)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回解析结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("activationTime", activationTime);
|
||||
result.put("expiryTime", expiryTime);
|
||||
result.put("trialDays", trialDays);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证许可证密钥是否有效
|
||||
*
|
||||
* @param licenseKey 许可证密钥
|
||||
* @return true-有效,false-无效
|
||||
*/
|
||||
public static boolean validateLicenseKey(String licenseKey) {
|
||||
if (licenseKey == null || licenseKey.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, Object> licenseInfo = parseLicenseKey(licenseKey);
|
||||
if (licenseInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
long expiryTime = (Long) licenseInfo.get("expiryTime");
|
||||
return System.currentTimeMillis() <= expiryTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取许可证剩余天数
|
||||
*
|
||||
* @param licenseKey 许可证密钥
|
||||
* @return 剩余天数,如果已过期返回0,解析失败返回-1
|
||||
*/
|
||||
public static int getRemainingDays(String licenseKey) {
|
||||
Map<String, Object> licenseInfo = parseLicenseKey(licenseKey);
|
||||
if (licenseInfo == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
long expiryTime = (Long) licenseInfo.get("expiryTime");
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
if (currentTime >= expiryTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long remainingMillis = expiryTime - currentTime;
|
||||
return (int) (remainingMillis / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
/**
|
||||
* AES加密
|
||||
*/
|
||||
private static byte[] encrypt(String data) throws Exception {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(getAESKey(), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||
return cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密(从Base64字符串)
|
||||
*
|
||||
* @param encryptedKey Base64编码的加密字符串
|
||||
* @return 解密后的原始数据,失败返回null
|
||||
*/
|
||||
public static String decrypt(String encryptedKey) {
|
||||
try {
|
||||
// Base64解码
|
||||
byte[] decoded = Base64.getDecoder().decode(encryptedKey);
|
||||
// AES解密
|
||||
return decrypt(decoded);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES解密(从字节数组)
|
||||
*/
|
||||
private static String decrypt(byte[] data) throws Exception {
|
||||
SecretKeySpec keySpec = new SecretKeySpec(getAESKey(), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
byte[] decrypted = cipher.doFinal(data);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AES密钥(16字节)
|
||||
*/
|
||||
private static byte[] getAESKey() {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] hash = md.digest(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
|
||||
return hash; // MD5产生16字节
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("生成AES密钥失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成校验码(防止数据被篡改)- 公共方法
|
||||
*
|
||||
* @param activationTime 激活时间
|
||||
* @param expiryTime 到期时间
|
||||
* @param trialDays 试用天数
|
||||
* @return 校验码
|
||||
*/
|
||||
public static String generateChecksum(long activationTime, long expiryTime, int trialDays) {
|
||||
try {
|
||||
String data = activationTime + "" + expiryTime + trialDays + SECRET_KEY;
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(hash).substring(0, 16);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("生成校验码失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package com.ruoyi.common.utils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* 简化版许可证密钥生成工具
|
||||
* 用于生成指定到期时间的许可证密钥
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
public class LicenseKeyGeneratorSimple {
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public static void main(String[] args) {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
System.out.println("=== 许可证密钥生成工具(简化版)===\n");
|
||||
|
||||
while (true) {
|
||||
System.out.println("请选择操作:");
|
||||
System.out.println("1. 生成指定天数的许可证");
|
||||
System.out.println("2. 生成指定到期日期的许可证");
|
||||
System.out.println("3. 验证许可证密钥");
|
||||
System.out.println("0. 退出");
|
||||
System.out.print("\n请输入选项: ");
|
||||
|
||||
String choice = scanner.nextLine().trim();
|
||||
|
||||
switch (choice) {
|
||||
case "1":
|
||||
generateByDays(scanner);
|
||||
break;
|
||||
case "2":
|
||||
generateByDate(scanner);
|
||||
break;
|
||||
case "3":
|
||||
validateKey(scanner);
|
||||
break;
|
||||
case "0":
|
||||
System.out.println("退出程序");
|
||||
scanner.close();
|
||||
return;
|
||||
default:
|
||||
System.out.println("无效选项,请重新输入\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定天数的许可证
|
||||
*/
|
||||
private static void generateByDays(Scanner scanner) {
|
||||
System.out.print("\n请输入许可证有效天数(例如:30、90、365): ");
|
||||
String input = scanner.nextLine().trim();
|
||||
|
||||
try {
|
||||
int days = Integer.parseInt(input);
|
||||
|
||||
// 当前时间作为激活时间
|
||||
Date now = new Date();
|
||||
long activationTime = now.getTime();
|
||||
|
||||
// 计算到期时间
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(now);
|
||||
calendar.add(Calendar.DAY_OF_MONTH, days);
|
||||
Date expiryDate = calendar.getTime();
|
||||
long expiryTime = expiryDate.getTime();
|
||||
|
||||
// 生成许可证密钥
|
||||
String encryptedKey = LicenseEncryptUtil.generateLicenseKey(activationTime, expiryTime, days);
|
||||
|
||||
// 输出结果
|
||||
System.out.println("\n========================================");
|
||||
System.out.println("许可证密钥生成成功!");
|
||||
System.out.println("========================================");
|
||||
System.out.println("激活时间: " + DATE_FORMAT.format(now));
|
||||
System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate));
|
||||
System.out.println("有效天数: " + days + " 天");
|
||||
System.out.println("\n许可证密钥(请复制以下内容):");
|
||||
System.out.println("----------------------------------------");
|
||||
System.out.println(encryptedKey);
|
||||
System.out.println("----------------------------------------\n");
|
||||
|
||||
// 解密并验证许可证
|
||||
String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey);
|
||||
|
||||
// 解析许可证数据:格式为 "激活时间|到期时间|试用天数|校验码"
|
||||
String[] parts = decryptedData.split("\\|");
|
||||
|
||||
try {
|
||||
long activationTimeMs = Long.parseLong(parts[0]);
|
||||
long expiryTimeMs = Long.parseLong(parts[1]);
|
||||
int trialDays = Integer.parseInt(parts[2]);
|
||||
String checksum = parts[3];
|
||||
|
||||
// 验证校验码
|
||||
String expectedChecksum = LicenseEncryptUtil.generateChecksum(
|
||||
activationTimeMs, expiryTimeMs, trialDays);
|
||||
if (!checksum.equals(expectedChecksum)) {
|
||||
System.out.println("许可证校验码验证失败");
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
Date expiryTime2 = new Date(expiryTimeMs);
|
||||
|
||||
if (now.after(expiryTime2)) {
|
||||
System.out.println("许可证已过期,到期时间: {}" + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime2));
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("许可证验证通过,到期时间: {}" + DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime2));
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println("许可证数据解析失败" + e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证生成的密钥
|
||||
System.out.println("密钥验证:✓ 有效");
|
||||
System.out.println("========================================\n");
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println("输入格式错误,请输入数字\n");
|
||||
} catch (Exception e) {
|
||||
System.out.println("生成失败: " + e.getMessage() + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定到期日期的许可证
|
||||
*/
|
||||
private static void generateByDate(Scanner scanner) {
|
||||
System.out.print("\n请输入到期日期(格式:yyyy-MM-dd,例如:2027-12-31): ");
|
||||
String dateInput = scanner.nextLine().trim();
|
||||
|
||||
try {
|
||||
// 解析日期(设置为当天的23:59:59)
|
||||
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Date expiryDate = inputFormat.parse(dateInput);
|
||||
|
||||
// 设置为当天的23:59:59
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(expiryDate);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 23);
|
||||
calendar.set(Calendar.MINUTE, 59);
|
||||
calendar.set(Calendar.SECOND, 59);
|
||||
expiryDate = calendar.getTime();
|
||||
long expiryTime = expiryDate.getTime();
|
||||
|
||||
// 当前时间作为激活时间
|
||||
Date now = new Date();
|
||||
long activationTime = now.getTime();
|
||||
|
||||
// 计算天数
|
||||
long diffMillis = expiryTime - activationTime;
|
||||
int days = (int) (diffMillis / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (days <= 0) {
|
||||
System.out.println("到期日期必须在当前日期之后\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成许可证密钥
|
||||
String licenseKey = LicenseEncryptUtil.generateLicenseKey(activationTime, expiryTime, days);
|
||||
|
||||
// 输出结果
|
||||
System.out.println("\n========================================");
|
||||
System.out.println("许可证密钥生成成功!");
|
||||
System.out.println("========================================");
|
||||
System.out.println("激活时间: " + DATE_FORMAT.format(now));
|
||||
System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate));
|
||||
System.out.println("有效天数: " + days + " 天");
|
||||
System.out.println("\n许可证密钥(请复制以下内容):");
|
||||
System.out.println("----------------------------------------");
|
||||
System.out.println(licenseKey);
|
||||
System.out.println("----------------------------------------\n");
|
||||
|
||||
// 验证生成的密钥
|
||||
boolean isValid = LicenseEncryptUtil.validateLicenseKey(licenseKey);
|
||||
System.out.println("密钥验证: " + (isValid ? "✓ 有效" : "✗ 无效"));
|
||||
System.out.println("========================================\n");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("日期格式错误或生成失败: " + e.getMessage() + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证许可证密钥
|
||||
*/
|
||||
private static void validateKey(Scanner scanner) {
|
||||
System.out.print("\n请输入要验证的许可证密钥: ");
|
||||
String licenseKey = scanner.nextLine().trim();
|
||||
|
||||
if (licenseKey.isEmpty()) {
|
||||
System.out.println("许可证密钥不能为空\n");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析许可证
|
||||
Map<String, Object> licenseInfo = LicenseEncryptUtil.parseLicenseKey(licenseKey);
|
||||
|
||||
if (licenseInfo == null) {
|
||||
System.out.println("\n========================================");
|
||||
System.out.println("✗ 许可证密钥无效(解析失败或校验码错误)");
|
||||
System.out.println("========================================\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取信息
|
||||
long activationTime = (Long) licenseInfo.get("activationTime");
|
||||
long expiryTime = (Long) licenseInfo.get("expiryTime");
|
||||
int trialDays = (Integer) licenseInfo.get("trialDays");
|
||||
|
||||
Date activationDate = new Date(activationTime);
|
||||
Date expiryDate = new Date(expiryTime);
|
||||
Date now = new Date();
|
||||
|
||||
boolean isValid = LicenseEncryptUtil.validateLicenseKey(licenseKey);
|
||||
int remainingDays = LicenseEncryptUtil.getRemainingDays(licenseKey);
|
||||
|
||||
// 输出结果
|
||||
System.out.println("\n========================================");
|
||||
System.out.println("许可证信息");
|
||||
System.out.println("========================================");
|
||||
System.out.println("激活时间: " + DATE_FORMAT.format(activationDate));
|
||||
System.out.println("到期时间: " + DATE_FORMAT.format(expiryDate));
|
||||
System.out.println("有效天数: " + trialDays + " 天");
|
||||
System.out.println("当前时间: " + DATE_FORMAT.format(now));
|
||||
System.out.println("\n状态: " + (isValid ? "✓ 有效" : "✗ 已过期"));
|
||||
if (isValid) {
|
||||
System.out.println("剩余天数: " + remainingDays + " 天");
|
||||
}
|
||||
System.out.println("========================================\n");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("\n验证失败: " + e.getMessage() + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.framework.config;
|
|||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
|
||||
import com.ruoyi.framework.interceptor.LicenseInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
|
@ -27,6 +28,9 @@ public class ResourcesConfig implements WebMvcConfigurer
|
|||
@Autowired
|
||||
private RepeatSubmitInterceptor repeatSubmitInterceptor;
|
||||
|
||||
@Autowired
|
||||
private LicenseInterceptor licenseInterceptor;
|
||||
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry)
|
||||
|
|
@ -47,6 +51,8 @@ public class ResourcesConfig implements WebMvcConfigurer
|
|||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry)
|
||||
{
|
||||
// 许可证验证拦截器
|
||||
registry.addInterceptor(licenseInterceptor).addPathPatterns("/**");
|
||||
// 防重复提交拦截器
|
||||
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package com.ruoyi.framework.interceptor;
|
||||
|
||||
import com.ruoyi.system.service.ISysLicenseService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 许可证验证拦截器
|
||||
* 在用户登录后验证系统许可证是否有效
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
@Component
|
||||
public class LicenseInterceptor implements HandlerInterceptor
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(LicenseInterceptor.class);
|
||||
|
||||
@Autowired
|
||||
private ISysLicenseService sysLicenseService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
|
||||
{
|
||||
// 获取请求URI
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// 排除登录、登出、静态资源等请求
|
||||
if (uri.contains("/login") || uri.contains("/logout") ||
|
||||
uri.contains("/captchaImage") || uri.contains("/static") ||
|
||||
uri.contains("/css") || uri.contains("/js") || uri.contains("/img") ||
|
||||
uri.contains("/fonts") || uri.contains("/ajax"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证许可证
|
||||
boolean isValid = sysLicenseService.validateLicense();
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
log.warn("许可证验证失败,拒绝访问: {}", uri);
|
||||
|
||||
// 设置响应状态码和错误信息
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
|
||||
String errorMessage = "{\"code\":601,\"msg\":\"系统许可证已过期或无效。\"}";
|
||||
|
||||
response.getWriter().write(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.ruoyi.system.mapper;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysLicense;
|
||||
|
||||
/**
|
||||
* 系统许可证Mapper接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
public interface SysLicenseMapper
|
||||
{
|
||||
|
||||
/**
|
||||
* 查询系统许可证(获取第一条记录)
|
||||
*
|
||||
* @return 系统许可证
|
||||
*/
|
||||
public SysLicense selectSysLicense();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.ruoyi.system.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysLicense;
|
||||
|
||||
/**
|
||||
* 系统许可证Service接口
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
public interface ISysLicenseService
|
||||
{
|
||||
/**
|
||||
* 查询系统许可证(获取第一条记录)
|
||||
*
|
||||
* @return 系统许可证
|
||||
*/
|
||||
public SysLicense selectSysLicense();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 验证许可证是否有效
|
||||
*
|
||||
* @return true=有效 false=无效
|
||||
*/
|
||||
public boolean validateLicense();
|
||||
|
||||
/**
|
||||
* 获取许可证状态信息
|
||||
*
|
||||
* @return 许可证状态信息
|
||||
*/
|
||||
public String getLicenseStatusInfo();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysLicense;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.LicenseEncryptUtil;
|
||||
import com.ruoyi.system.mapper.SysLicenseMapper;
|
||||
import com.ruoyi.system.service.ISysLicenseService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 系统许可证Service业务层处理(加密版)
|
||||
*
|
||||
* @author ruoyi
|
||||
* @date 2026-02-09
|
||||
*/
|
||||
@Service
|
||||
public class SysLicenseServiceImpl implements ISysLicenseService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(SysLicenseServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private SysLicenseMapper sysLicenseMapper;
|
||||
|
||||
|
||||
/**
|
||||
* 查询系统许可证(获取第一条记录)
|
||||
*
|
||||
* @return 系统许可证
|
||||
*/
|
||||
@Override
|
||||
public SysLicense selectSysLicense()
|
||||
{
|
||||
return sysLicenseMapper.selectSysLicense();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证许可证是否有效(解密验证)
|
||||
*
|
||||
* @return true=有效 false=无效
|
||||
*/
|
||||
@Override
|
||||
public boolean validateLicense()
|
||||
{
|
||||
try
|
||||
{
|
||||
SysLicense license = selectSysLicense();
|
||||
if (license == null)
|
||||
{
|
||||
log.error("许可证不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
String encryptedKey = license.getLicenseKey();
|
||||
if (encryptedKey == null || encryptedKey.trim().isEmpty())
|
||||
{
|
||||
log.error("许可证密钥为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解密并验证许可证
|
||||
String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey);
|
||||
if (decryptedData == null)
|
||||
{
|
||||
log.error("许可证密钥解密失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析许可证数据:格式为 "激活时间|到期时间|试用天数|校验码"
|
||||
String[] parts = decryptedData.split("\\|");
|
||||
if (parts.length != 4)
|
||||
{
|
||||
log.error("许可证数据格式错误");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
long activationTimeMs = Long.parseLong(parts[0]);
|
||||
long expiryTimeMs = Long.parseLong(parts[1]);
|
||||
int trialDays = Integer.parseInt(parts[2]);
|
||||
String checksum = parts[3];
|
||||
|
||||
// 验证校验码
|
||||
String expectedChecksum = LicenseEncryptUtil.generateChecksum(
|
||||
activationTimeMs, expiryTimeMs, trialDays);
|
||||
if (!checksum.equals(expectedChecksum))
|
||||
{
|
||||
log.error("许可证校验码验证失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
Date now = new Date();
|
||||
Date expiryTime = new Date(expiryTimeMs);
|
||||
|
||||
if (now.after(expiryTime))
|
||||
{
|
||||
log.warn("许可证已过期,到期时间: {}", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime));
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("许可证验证通过,到期时间: {}", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime));
|
||||
return true;
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
log.error("许可证数据解析失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("验证许可证时发生异常", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取许可证状态信息
|
||||
*
|
||||
* @return 许可证状态信息
|
||||
*/
|
||||
@Override
|
||||
public String getLicenseStatusInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
SysLicense license = selectSysLicense();
|
||||
if (license == null)
|
||||
{
|
||||
return "许可证不存在";
|
||||
}
|
||||
|
||||
String encryptedKey = license.getLicenseKey();
|
||||
if (encryptedKey == null || encryptedKey.trim().isEmpty())
|
||||
{
|
||||
return "许可证密钥为空";
|
||||
}
|
||||
|
||||
// 解密许可证
|
||||
String decryptedData = LicenseEncryptUtil.decrypt(encryptedKey);
|
||||
if (decryptedData == null)
|
||||
{
|
||||
return "许可证密钥无效";
|
||||
}
|
||||
|
||||
String[] parts = decryptedData.split("\\|");
|
||||
if (parts.length != 5)
|
||||
{
|
||||
return "许可证数据格式错误";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
long expiryTimeMs = Long.parseLong(parts[1]);
|
||||
Date expiryTime = new Date(expiryTimeMs);
|
||||
Date now = new Date();
|
||||
|
||||
long diffInMillies = expiryTime.getTime() - now.getTime();
|
||||
long diffInDays = diffInMillies / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffInDays < 0)
|
||||
{
|
||||
return "试用期已过期,请联系管理员续费";
|
||||
}
|
||||
else if (diffInDays == 0)
|
||||
{
|
||||
return "试用期今天到期,请尽快续费";
|
||||
}
|
||||
else
|
||||
{
|
||||
return String.format("试用期剩余 %d 天(到期时间: %s)",
|
||||
diffInDays,
|
||||
DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, expiryTime));
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
log.error("解析许可证时间失败", e);
|
||||
return "许可证数据解析失败";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("获取许可证状态信息时发生异常", e);
|
||||
return "获取许可证状态失败";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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.ruoyi.system.mapper.SysLicenseMapper">
|
||||
|
||||
<resultMap type="SysLicense" id="SysLicenseResult">
|
||||
<result property="licenseKey" column="license_key" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectSysLicenseVo">
|
||||
select license_key
|
||||
from sys_license
|
||||
</sql>
|
||||
|
||||
<select id="selectSysLicense" resultMap="SysLicenseResult">
|
||||
<include refid="selectSysLicenseVo"/>
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build:prod": "vue-cli-service build",
|
||||
"build:dev": "vue-cli-service build --mode development",
|
||||
"build:stage": "vue-cli-service build --mode staging",
|
||||
"preview": "node build/index.js --preview"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ service.interceptors.response.use(res => {
|
|||
return Promise.reject(new Error(msg))
|
||||
} else if (code === 601) {
|
||||
Message({ message: msg, type: 'warning' })
|
||||
return Promise.reject('error')
|
||||
return Promise.reject(new Error(msg))
|
||||
} else if (code !== 200) {
|
||||
Notification.error({ title: msg })
|
||||
return Promise.reject('error')
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module.exports = {
|
|||
// 部署生产环境和开发环境下的URL。
|
||||
// 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
|
||||
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
|
||||
publicPath: process.env.NODE_ENV === "production" ? "" : "/",
|
||||
publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
|
||||
// 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
|
||||
outputDir: 'dist',
|
||||
// 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
|
||||
drop table if exists sys_license;
|
||||
create table sys_license (
|
||||
licenseKey varchar(200) comment '许可认证'
|
||||
) engine=innodb auto_increment=200 comment = '许可认证表';
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- 1、部门表
|
||||
-- ----------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
-- ----------------------------
|
||||
-- 系统许可证表(加密版本)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_license`;
|
||||
CREATE TABLE `sys_license` (
|
||||
`license_key` TEXT NOT NULL COMMENT '加密的许可证密钥(包含激活时间、到期时间等信息)',
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统许可证表(加密版)';
|
||||
|
||||
-- ----------------------------
|
||||
-- 注意:首次启动时,系统会自动生成加密的许可证密钥
|
||||
-- 不需要手动插入数据
|
||||
-- 许可证密钥是加密字符串,包含:激活时间、到期时间、机器码、试用天数、校验码
|
||||
-- 即使修改数据库中的密钥,也无法通过后端的解密验证
|
||||
-- ----------------------------
|
||||
Loading…
Reference in New Issue