refactor(license): 优化 License 模块部分代码

This commit is contained in:
2025-04-29 22:31:36 +08:00
parent 06f5a0f346
commit 7d97026480
35 changed files with 350 additions and 473 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.continew</groupId>
<artifactId>continew-starter-license</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-license-generator</artifactId>
<description>ContiNew Starter License 模块 - 生成器</description>
<dependencies>
<!-- License 模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-license-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.license.autoconfigure;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import top.continew.license.service.LicenseCreateService;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* license 生成模块 自动配置
*
* @author loach
* @since 2.11.0
*/
@AutoConfiguration
@EnableConfigurationProperties(LicenseGenerateProperties.class)
@ConditionalOnProperty(prefix = PropertiesConstants.LICENSE_GENERATOR, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
public class LicenseGenerateAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(LicenseGenerateAutoConfiguration.class);
/**
* license 生成服务接口
*/
@Bean
@ConditionalOnMissingBean
public LicenseCreateService licenseCreateService() {
return LicenseCreateService.getInstance();
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'License-Generator' completed initialization.");
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.license.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* license 生成模块配置属性
*
* @author Jasmine
* @since 2.11.0
*/
@ConfigurationProperties(PropertiesConstants.LICENSE_GENERATOR)
public class LicenseGenerateProperties {
/**
* 是否启用
*/
private boolean enabled = true;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.license.manager;
import de.schlichtherle.license.*;
import de.schlichtherle.xml.GenericCertificate;
import top.continew.license.exception.LicenseException;
import java.util.Date;
/**
* 自定义服务端证书管理类(生成证书)
*
* @author loach
* @author echo
* @since 2.11.0
*/
public class ServerLicenseManager extends LicenseManager {
public ServerLicenseManager(LicenseParam param) {
super(param);
}
/**
* 证书生成参数验证
*
* @param content 内容
*/
protected synchronized void validateCreate(final LicenseContent content) {
Date now = new Date();
Date notBefore = content.getNotBefore();
Date notAfter = content.getNotAfter();
if (notBefore != null && now.before(notBefore)) {
throw new LicenseException("证书尚未生效,无法生成");
}
if (notAfter != null && now.after(notAfter)) {
throw new LicenseException("证书已过期,无法生成");
}
if (notBefore != null && notAfter != null && notBefore.after(notAfter)) {
throw new LicenseException("证书生效时间晚于失效时间,无法生成");
}
}
/**
* 重写生成证书的方法,增加生成参数验证
*
* @param content 内容
* @param notary 公证人
* @return {@link byte[] }
* @throws Exception 例外
*/
@Override
protected synchronized byte[] create(LicenseContent content, LicenseNotary notary) throws Exception {
initialize(content);
validateCreate(content);
final GenericCertificate genericCertificate = notary.sign(content);
return getPrivacyGuard().cert2key(genericCertificate);
}
}

View File

@@ -0,0 +1,301 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.license.service;
import cn.hutool.core.util.IdUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.schlichtherle.license.*;
import net.lingala.zip4j.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import top.continew.license.exception.LicenseException;
import top.continew.license.manager.ServerLicenseManager;
import top.continew.license.model.*;
import top.continew.license.util.ExecCmdUtil;
import top.continew.license.util.ServerInfoUtils;
import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.prefs.Preferences;
/**
* 证书生成接口 实现类
*
* @author loach
* @since 2.11.0
*/
public class LicenseCreateService {
private static final Logger log = LoggerFactory.getLogger(LicenseCreateService.class);
private static volatile LicenseCreateService instance;
private static final X500Principal DEFAULT_HOLDER_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");
private LicenseCreateService() {
}
/**
* 获取实例
*
* @return {@link LicenseCreateService }
*/
public static LicenseCreateService getInstance() {
if (instance == null) {
synchronized (LicenseCreateService.class) {
if (instance == null) {
instance = new LicenseCreateService();
}
}
}
return instance;
}
/**
* 获取服务器信息
*
* @return {@link LicenseExtraModel }
*/
public LicenseExtraModel getServerInfo() {
return ServerInfoUtils.getServerInfos();
}
/**
* 生成一个证书
*
* @param paramVO param vo
* @throws Exception 例外
*/
public void generateLicense(LicenseCreatorParamVO paramVO) throws Exception {
BuildCreatorResp buildCreatorResp = buildCreator(paramVO);
LicenseCreatorParam param = buildCreatorResp.getParam();
ZipFile clientZipFile = buildCreatorResp.getClientZipFile();
try {
LicenseParam licenseParam = initLicenseParam(param);
LicenseManager licenseManager = new ServerLicenseManager(licenseParam);
LicenseContent licenseContent = initLicenseContent(param);
licenseManager.store(licenseContent, new File(param.getLicensePath()));
log.info("{} 证书生成成功 路径: {}", param.getSubject(), param.getLicensePath());
clientZipFile.addFile(param.getLicensePath());
} catch (Exception e) {
throw new LicenseException("生成证书失败:" + param.getSubject(), e);
}
}
/**
* 构建 License 创建者对象及客户端配置压缩包。
*
* @param paramVO 创建参数封装对象,包含客户名、密码、描述、扩展信息等。
* @return Map 包含 LicenseCreatorParamcreator 和 生成的客户端 Zip 文件clientZipFile
* @throws Exception 命令执行或文件操作过程中出现异常
*/
private BuildCreatorResp buildCreator(LicenseCreatorParamVO paramVO) throws Exception {
String customerName = paramVO.getCustomerName();
String privateAlias = customerName + "-private-alias";
String publicAlias = customerName + "-public-alias";
String currentCustomerDir = relativePath(paramVO) + customerName + IdUtil.fastSimpleUUID() + File.separator;
File customerDirFile = new File(currentCustomerDir);
if (!customerDirFile.exists() && !customerDirFile.mkdirs()) {
throw new IOException("Failed to create directory: " + currentCustomerDir);
}
String privateKeystore = currentCustomerDir + "privateKeys.keystore";
String publicKeystore = currentCustomerDir + "publicCerts.keystore";
String certFilePath = currentCustomerDir + "certfile.cer";
String licensePath = currentCustomerDir + "license.lic";
LicenseCreatorParam param = new LicenseCreatorParam();
param.setSubject(customerName);
param.setPrivateAlias(privateAlias);
param.setKeyPass(paramVO.getKeyPass());
param.setStorePass(paramVO.getStorePass());
param.setLicensePath(licensePath);
param.setPrivateKeysStorePath(privateKeystore);
param.setExpiryTime(paramVO.getExpireTime());
param.setDescription(paramVO.getDescription());
param.setLicenseExtraModel(paramVO.getLicenseExtraModel());
int validity = getValidity(param.getIssuedTime(), paramVO.getExpireTime());
String dname = "\"CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN\"";
// 生成私钥库
String keyAlgOption = checkJavaVersion() ? "-keyalg DSA" : ""; // JDK>=17 要指定 keyalg
String genKeyCmd = String
.format("keytool -genkeypair %s -keysize 1024 -validity %d -alias %s -keystore %s -storepass %s -keypass %s -dname %s", keyAlgOption, validity, privateAlias, privateKeystore, paramVO
.getStorePass(), paramVO.getKeyPass(), dname);
// 导出证书
String exportCertCmd = String
.format("keytool -exportcert -alias %s -keystore %s -storepass %s -file \"%s\"", privateAlias, privateKeystore, paramVO
.getStorePass(), certFilePath);
// 导入到公钥库
String importCertCmd = String
.format("keytool -noprompt -import -alias %s -file \"%s\" -keystore %s -storepass %s", publicAlias, certFilePath, publicKeystore, paramVO
.getStorePass());
// 执行命令
ExecCmdUtil.exec(genKeyCmd);
ExecCmdUtil.exec(exportCertCmd);
ExecCmdUtil.exec(importCertCmd);
// 生成客户端配置文件
ZipFile clientZipFile = generateClientConfig(param, currentCustomerDir, publicAlias);
return new BuildCreatorResp(param, clientZipFile);
}
/**
* 校验JDK版本
*
* @return boole T 17 版本 F 非 17 版本
* @throws Exception 例外
*/
private boolean checkJavaVersion() throws Exception {
String version = System.getProperty("java.version");
int currentVersion = 0;
if (version.startsWith("1.")) {
currentVersion = Integer.parseInt(version.split("\\.")[1]);
} else {
currentVersion = Integer.parseInt(version.split("\\.")[0]);
}
return currentVersion >= 17;
}
private ZipFile generateClientConfig(LicenseCreatorParam param,
String currentCustomerDir,
String publicAlias) throws Exception {
ZipFile clientLicense = new ZipFile(currentCustomerDir + "clientLicense.zip");
File config = new File(currentCustomerDir + "clientConfig.json");
ConfigParam configParam = new ConfigParam();
configParam.setPublicAlias(publicAlias);
configParam.setStorePass(param.getStorePass());
configParam.setSubject(param.getSubject());
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(configParam);
FileOutputStream out = null;
try {
out = new FileOutputStream(config);
out.write(json.getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (Exception e) {
throw new LicenseException("密钥文件生成失败", e);
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
throw new LicenseException("文件流关闭失败", e);
}
}
}
List<File> files = new ArrayList<>();
files.add(config);
files.add(new File(currentCustomerDir + "publicCerts.keystore"));
clientLicense.addFiles(files);
return clientLicense;
}
/**
* 将有效时间转换成天
*
* @param issuedTime 签发时间
* @param expireTime 过期时间
* @return int
*/
private int getValidity(Date issuedTime, Date expireTime) {
long issued = issuedTime.getTime();
long expire = expireTime.getTime();
long differ = expire - issued;
long remaining = differ % (24L * 3600L * 1000L);
long validity = differ / (24L * 3600L * 1000L);
if (remaining > 0) {
validity++;
}
return (int)validity;
}
/**
* 是否是 Windows
*
* @return boolean
*/
private boolean isWindows() {
String os = System.getProperty("os.name");
return os.toLowerCase().contains("windows");
}
/**
* 证书生成路径
*
* @param paramVO param vo
* @return {@link String }
*/
private String relativePath(LicenseCreatorParamVO paramVO) {
if (paramVO.getLicenseSavePath() != null) {
return paramVO.getLicenseSavePath();
}
if (isWindows()) {
return "C:/license/";
}
return "/data/license/";
}
/**
* 设置证书生成参数
*/
private LicenseParam initLicenseParam(LicenseCreatorParam param) {
Preferences preferences = Preferences.userNodeForPackage(LicenseCreateService.class);
//设置密钥
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreateService.class, param
.getPrivateKeysStorePath(), param.getPrivateAlias(), param.getStorePass(), param.getKeyPass());
return new DefaultLicenseParam(param.getSubject(), preferences, privateStoreParam, cipherParam);
}
/**
* 设置证书生成内容
*
* @param param 参数
* @return {@link LicenseContent }
*/
private LicenseContent initLicenseContent(LicenseCreatorParam param) {
LicenseContent licenseContent = new LicenseContent();
licenseContent.setHolder(DEFAULT_HOLDER_ISSUER);
licenseContent.setIssuer(DEFAULT_HOLDER_ISSUER);
licenseContent.setSubject(param.getSubject());
licenseContent.setIssued(param.getIssuedTime());
licenseContent.setNotBefore(param.getIssuedTime());
licenseContent.setNotAfter(param.getExpiryTime());
licenseContent.setConsumerType(param.getConsumerType());
licenseContent.setConsumerAmount(param.getConsumerAmount());
licenseContent.setInfo(param.getDescription());
if (param.getLicenseExtraModel() != null) {
licenseContent.setExtra(param.getLicenseExtraModel());
}
return licenseContent;
}
}

View File

@@ -0,0 +1 @@
top.continew.license.autoconfigure.LicenseGenerateAutoConfiguration