springBoot 整合 TureLicense 证书安装及验证

发布于:2025-08-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

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 证书验证模块
  1. GlobalLicenseAspect 全局接口许可证验证切面,对所有接口进行证书验证,除非标记了@SkipLicense注解。
  2. 标记不需要进行许可证验证的接口注解,使用方法方式:
    /**
     * 登录
     *
     * @param loginDTO 登录请求参数
     * @return 登录结果
     */
    @PostMapping("/doLogin")
    @SkipLicense(reason = "公开接口无需验证")
    public R<String> login(@RequestBody LoginDTO loginDTO) {
        return sysLoginService.login(loginDTO);
    }
  1. WebMvcConfig 项目启动时安装并验证证书。
  2. LicenseVerify 证书封装安装、验证方法对象。
  • csm-server 生成证书服务

    业务项目不需要引用该模块,单独使用

  1. LicenseCreatorController 接口类,分为获取客户机信息接口,包含mac 地址、主板序列号、cpu 序列号以及 IP 地址;生成 license 许可证证书接口:/generateLicense。

  2. 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 证书

  1. 在 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"
  1. 调用 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: 主板序列号,非必填

后续有效期时间过期后只需修改有效期重新生成即可。

证书验证

  1. 在主项目的 pom.xml 文件中引用生成拦截模块:
<!-- license 拦截模块 -->
<dependency>
    <groupId>com.ynyc</groupId>
    <artifactId>csm-client</artifactId>
    <version>1.0.0</version>
</dependency>
  1. 将 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


网站公告

今日签到

点亮在社区的每一天
去签到