TureLicense 使用
License,也就是版权许可证书,一般用于收费软件给付费用户提供的访问许可证明。
当前方案针对应用部署在客户的内网环境。因为这种情况开发者无法控制客户的网络环境,也不能保证应用所在服务器可以访问外网,因此通常的做法是使用服务器许可文件,在应用启动的时候加载证书,然后在登录或者其他关键操作的地方校验证书的有效性(本文介绍的就是这种)。
- 注意:任何加密都有反编译、破解、跳过的手段。
license授权机制的原理
TrueLicense是一个开源的证书管理引擎。
- 生成密钥对,使用Keytool生成公私钥证书库。
- 授权者保留私钥,使用私钥对包含授权信息(如使用截止日期,MAC地址等)的license进行数字签名。
- 公钥给使用者(放在验证的代码中使用),用于验证 license 是否符合使用条件,使用场景(项目启动时安装证书验证、接口访问时有限期验证)。
生成公钥密钥
项目主要文件结构介绍
backend
│ README.md
└───csm-client
│ │ GlobalLicenseAspect.java
│ │ SkipLicense.java
│ │ WebMvcConfig.java
│ │ LicenseVerify.java
│ │ ...
└───csm-server
│ │ LicenseCreatorController.java
│ │ application.yml
│ │ ...
└───start
│ │ application-dev.yml
│ │ ...
当前结构仅供参考,为本人自建项目。
- csm-client 证书验证模块
- GlobalLicenseAspect 全局接口许可证验证切面,对所有接口进行证书验证,除非标记了@SkipLicense注解。
- 标记不需要进行许可证验证的接口注解,使用方法方式:
/**
* 登录
*
* @param loginDTO 登录请求参数
* @return 登录结果
*/
@PostMapping("/doLogin")
@SkipLicense(reason = "公开接口无需验证")
public R<String> login(@RequestBody LoginDTO loginDTO) {
return sysLoginService.login(loginDTO);
}
- WebMvcConfig 项目启动时安装并验证证书。
- LicenseVerify 证书封装安装、验证方法对象。
csm-server 生成证书服务
业务项目不需要引用该模块,单独使用
LicenseCreatorController 接口类,分为获取客户机信息接口,包含mac 地址、主板序列号、cpu 序列号以及 IP 地址;生成 license 许可证证书接口:/generateLicense。
application.yml 存放生成后的证书地址,与 start 中的配置保持一致。
license:
licensePath: /home/license/license.lic
- start 实际业务项目,需要引用 csm-client 模块。
application-dev.yml 配置文件,需配置证书生成地址以及公钥存放地址等。
# 许可证配置
license:
subject: license_cognition ## 项目名称
publicAlias: publicCert ## 公钥证书别名
storePass: Lic12345 ## 公钥密码
licensePath: /home/license/license.lic ## 许可证书存放地址
publicKeysStorePath: /home/license/publicCerts.keystore ## 公钥存放地址
enabled: true ## 是否启动
生成安装 license 证书
- 在 D 盘下新建文件夹 license, 借助 jdk keytool 命令生成公钥私钥使用,注意:公钥为客户使用密码,私钥为我们必须保管好的密码,不可泄漏且不可与公钥密码一致。
## 1. 生成私匙库
# validity:私钥的有效期多少天
# alias:私钥别称
# keystore: 指定私钥库文件的名称(生成在当前目录)
# storepass:指定私钥库的密码(获取keystore信息所需的密码)
# keypass:指定别名条目的密码(私钥的密码)
keytool -genkeypair -keysize 1024 -validity 3650 -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -keypass "private_password1234" -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"
## 2. 把私匙库内的公匙导出到一个文件当中
# alias:私钥别称
# keystore:指定私钥库的名称(在当前目录查找)
# storepass: 指定私钥库的密码
# file:证书名称
keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "private_password1234" -file "certfile.cer" -rfc
## 3. 再把这个证书文件导入到公匙库
# alias:公钥别称
# file:证书名称
# keystore:公钥文件名称
# storepass:指定私钥库的密码
keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "public_password1234"
- 调用 csm-server 服务中的生成证书接口:/generateLicense。
接口参数为:
{
"subject": "license_cognition",
"privateAlias": "privateKey",
"keyPass": "private_password1234",
"storePass": "public_password1234",
"licensePath": "D:/license/license.lic",
"privateKeysStorePath": "D:/license/privateKeys.keystore",
"issuedTime": "2025-08-13 00:00:00",
"expiryTime": "2025-08-13 11:00:00",
"consumerType": "User",
"consumerAmount": 1,
"description": "这是证书描述信息",
"licenseCheckModel": {
"ipAddress": ["10.140.127.27"],
"macAddress": ["20-16-B9-AF-0E-0B"],
"cpuSerial": "BFEBFBFF000906EA",
"mainBoardSerial": "MMG5S00000285884P00RB"
}
}
接口字段释义:
subject: 项目名称
privateAlias: 私钥证书别名
keyPass: 私钥密码(妥善保管)
storePass: 公钥密码
licensePath: 生成证书地址
privateKeysStorePath: 私钥文件存放地址
issuedTime:有效期开始时间
expiryTime:有效期结束时间
consumerType:验证类型
consumerAmount: 验证数量
description:证书描述
licenseCheckModel: 其他验证对象
ipAddress: IP 地址,可多个,非必填
macAddress: MAC 地址,可多个,非必填
cpuSerial: cpu 序列号,非必填
mainBoardSerial: 主板序列号,非必填
后续有效期时间过期后只需修改有效期重新生成即可。
证书验证
- 在主项目的 pom.xml 文件中引用生成拦截模块:
<!-- license 拦截模块 -->
<dependency>
<groupId>com.ynyc</groupId>
<artifactId>csm-client</artifactId>
<version>1.0.0</version>
</dependency>
- 将 application-dev.yml 文件配置中的 license enabled 属性修改为 true。
- 部分拦截类展示:
GlobalLicenseAspect.java 接口拦截切面:
package com.ynyc.config;
import com.ynyc.license.LicenseVerify;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* 全局接口许可证验证切面
* 对所有接口进行验证,除非标记了@SkipLicense注解
*/
@Aspect
@Component
public class GlobalLicenseAspect {
/**
* 证书是否使用
*/
@Value("${license.enabled}")
private Boolean enabled;
// 切点:拦截所有Controller的接口方法
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) || " +
"within(@org.springframework.stereotype.Controller *)")
public void controllerPointcut() {}
@Around("controllerPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
if(!enabled){
// 跳过验证,直接执行原方法
return joinPoint.proceed();
}
// 1. 检查是否需要跳过验证
if (isSkipLicense(joinPoint)) {
// 跳过验证,直接执行原方法
return joinPoint.proceed();
}
// 2. 执行许可证验证
boolean valid = validateLicense();
// 3. 验证失败处理
if (!valid) {
handleInvalidLicense();
return null; // 验证失败,不执行原方法
}
// 4. 验证成功,继续执行原接口方法
return joinPoint.proceed();
}
/**
* 判断是否需要跳过许可证验证
* 类或方法上有@SkipLicense注解则跳过
*/
private boolean isSkipLicense(ProceedingJoinPoint joinPoint) {
// 检查类上的注解
Class<?> targetClass = joinPoint.getTarget().getClass();
if (targetClass.isAnnotationPresent(SkipLicense.class)) {
return true;
}
// 检查方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
return method.isAnnotationPresent(SkipLicense.class);
}
/**
* 执行许可证验证
*/
private boolean validateLicense() {
try {
LicenseVerify licenseVerify = new LicenseVerify();
return licenseVerify.verify();
} catch (Exception e) {
// 验证过程中发生异常,视为验证失败
return false;
}
}
/**
* 处理许可证无效的情况
*/
private void handleInvalidLicense() throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletResponse response = attributes.getResponse();
if (response != null) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"许可证无效或已过期,请联系管理员\"}");
response.setStatus(403);
}
}
}
}
WebMvcConfig 项目启动拦截器,安装正式以及验证有效期:
package com.ynyc.config;
import com.ynyc.license.LicenseCheckInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @ProjectName WebMvcConfig
* @author Administrator
* @version 1.0.0
* @Description 注册拦截器
* @createTime 2022/4/30 0030 21:11
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 证书是否使用
*/
@Value("${license.enabled}")
private Boolean enabled;
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
if(enabled){
registry.addInterceptor(new LicenseCheckInterceptor()).addPathPatterns("/check");
}
}
}
SkipLicense 忽略拦截注解类:
package com.ynyc.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记不需要进行许可证验证的接口
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SkipLicense {
// 可选:添加理由说明
String reason() default "";
}
附上本文示例代码:
https://www.aliyundrive.com/s/2zzpRqTC988