license版权证书生成与验证

简介: License,即版权许可证,一般用于收费软件给付费用户提供的访问许可证明。根据应用部署位置的不同,一般可以分为以下两种情况讨论:- 应用部署在开发者自己的云服务器上,如现在的saas模式的软件供应商就是这样部署的。这种情况下用户通过账号登录的形式远程访问,因此只需要在账号登录的时候校验目标账号的有效期、访问权限等信息即可。- 应用部署在客户的内网环境,即本地化部署。因为这种情况开发者无法控制客户的网络环境,也不能保证应用所在服务器可以访问外网,因此通常的做法是使用服务器许可文件,在应用启动的时候加载

1.背景

License,即版权许可证,一般用于收费软件给付费用户提供的访问许可证明。根据应用部署位置的不同,一般可以分为以下两种情况讨论:

  • 应用部署在开发者自己的云服务器上,如现在的saas模式的软件供应商就是这样部署的。这种情况下用户通过账号登录的形式远程访问,因此只需要在账号登录的时候校验目标账号的有效期、访问权限等信息即可。
  • 应用部署在客户的内网环境,即本地化部署。因为这种情况开发者无法控制客户的网络环境,也不能保证应用所在服务器可以访问外网,因此通常的做法是使用服务器许可文件,在应用启动的时候加载证书,然后在登录或者其他关键操作的地方校验证书的有效性。

接下来进入今天的主题:基于TrueLicense实现license证书生成与检验。

TrueLicense是一个开源的主流证书管理引擎,可以用于license的生成和有效性的验证

2.TrueLicense

使用TrueLicense相对简单,首先我们需要keytool生产密钥对以供后续生成、验证证书使用。

keytool是jdk里面自带的命令。我们直接用keytool命令来生成密钥对。需要执行的命令如下:

## 1. 生成私匙库
# validity:私钥的有效期多少天
# alias:私钥别称
# keystore: 指定私钥库文件的名称(生成在当前目录)
# storepass:指定私钥库的密码(获取keystore信息所需的密码) 
# keypass:指定别名条目的密码(私钥的密码) 
keytool -genkeypair -keysize 1024 -validity 3650 -alias "privateKey" -keystore "privateKeys.keystore" -storepass "123456" -keypass "123456" -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 "123456" -file "certfile.cer"

## 3. 再把这个证书文件导入到公匙库
# alias:公钥别称
# file:证书名称
# keystore:公钥文件名称
# storepass:指定私钥库的密码
keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "123456"

在任意目录下执行完上述三个命令之后。我们会在当前目录下面得到三个文件:privateKeys.keystore、publicCerts.keystore、certfile.cer。

  • privateKeys.keystore:私钥,这个我们自己留着,不能泄露给别人。
  • publicCerts.keystore:公钥,这个给客人用的。在我们程序里面就是用他来解析license文件里面的信息的。
  • certfile.cer:这个文件没啥用,可以删掉。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

3.springboot整合TrueLicense

通过上面已经生成好了密钥对(私钥、公钥)。接下来就是生成license证书,以及验证证书的合法性了

3.1 引入TrueLicense依赖

  <!-- License -->
  <dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
  </dependency>

3.2 生成license证书

这里我们提供了一个restful接口LicenseController直接返回流生成并下载license证书文件。

@RestController
@Api(tags = "license管理")
@RequestMapping("/license")
public class LicenseController {
   
   

    @Resource
    private LicenseCreator licenseCreator;
    @Resource
    private LicenseProperties licenseProperties;

    @PostMapping
    @ApiOperation("生成license")
    public void create(LicenseCreatorParam creatorParam, HttpServletResponse response) throws IOException {
   
   
        boolean flag = licenseCreator.generateLicense(creatorParam);
        List<String> list = StrUtil.split(licenseProperties.getLicensePath(), "/");
        String fileName = list.get(list.size()-1);
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        BufferedInputStream inputStream = FileUtil.getInputStream(licenseProperties.getLicensePath());
        IoUtil.copy(inputStream, response.getOutputStream());
    }
}

这里的生成证书入参LicenseCreatorParam核心参数:设置证书有效日期和服务器系统信息

  /**
     * 证书失效时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date expiryTime;

    /**
     * 服务器系统信息
     */
    private SystemInfo systemInfo;

我们知道只验证license的证书的合法性、有效期,还是有被破解的风险,因为别人可以找到该证书文件copy到其他服务器相同路径下就可以了。所以为了加强证书验证的严谨性,我们添加对服务器系统信息验证,针对系统唯一标识、cpu序列号等进行验证,从而达到同一证书在不同服务器之间是不能共用的

这里需要提一下:我们并没有根据系统服务器的mac地址进行验证,因为目前一般使用主流的云原生容器化技术部署java服务,java服务对应的容器的mac地址是不固定的,所以这里就不使用macId了,当然如果一定要校验服务器宿主机的mac地址,可以将mac地址配置到容器的环境变量中,然后java代码从环境变量中获取mac地址,也能实现基于mac地址验证

生成license证书核心逻辑LicenseCreator

@Component
public class LicenseCreator {
   
   
    @Resource
    private LicenseProperties licenseProperties;

    private static Logger logger = LogManager.getLogger(LicenseCreator.class);
    private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");


    /**
     * 生成License证书
     */
    public boolean generateLicense(LicenseCreatorParam param){
   
   
        try {
   
   
            LicenseManager licenseManager = new LicenseManager(initLicenseParam());
            LicenseContent licenseContent = initLicenseContent(param);
            licenseManager.store(licenseContent, new File(licenseProperties.getLicensePath()));
            return true;
        }catch (Exception e){
   
   
            logger.error("证书生成失败:", e);
            throw new BizException("生成license证书失败");
        }
    }

    /**
     * 初始化证书生成参数
     */
    private LicenseParam initLicenseParam(){
   
   
        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);

        //设置对证书内容加密的秘钥
        CipherParam cipherParam = new DefaultCipherParam(licenseProperties.getStorePass());

        KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
                ,licenseProperties.getPrivateKeysStorePath()
                ,licenseProperties.getPrivateAlias()
                ,licenseProperties.getStorePass()
                ,licenseProperties.getKeyPass());

        LicenseParam licenseParam = new DefaultLicenseParam(licenseProperties.getSubject()
                ,preferences
                ,privateStoreParam
                ,cipherParam);

        return licenseParam;
    }

    /**
     * 设置证书生成正文信息
     */
    private LicenseContent initLicenseContent(LicenseCreatorParam param){
   
   
        LicenseContent licenseContent = new LicenseContent();
        licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);
        licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);

        licenseContent.setSubject(licenseContent.getSubject());
        licenseContent.setIssued(param.getIssuedTime());
        licenseContent.setNotBefore(param.getIssuedTime());
        licenseContent.setNotAfter(param.getExpiryTime() == null ? addYears(new Date(), 10) : param.getExpiryTime());
        licenseContent.setConsumerType(param.getConsumerType());
        licenseContent.setConsumerAmount(param.getConsumerAmount());
        licenseContent.setInfo(param.getDescription());
        licenseContent.setExtra(param.getSystemInfo());
        return licenseContent;
    }

    public  Date addYears(Date date, int n) {
   
   
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.add(Calendar.YEAR, n);
        return cal.getTime();
    }
}

通过以上操作流程我们就可以生产license证书文件了,接下来就是对证书的检验。

3.3 license证书验证

这里我们支持两种情况下的证书验证:

  • 服务启动时对证书校验:这时候会根据license配置进行证书的合法性、有效性、服务器系统信息验证。
  • 接口层面使用@License注解进行证书检验:如登录接口验证license的有效性,如果已过期,及时告知客户。

验证流程图如下所示:

通过流程图可知,相关检验都可以通过license配置属性开关控制,做到高度可插拔。

通过实现ApplicationRunner在服务启动时进行license验证

@Order(OrderConstant.RUNNER_LICENSE)
public class LicenseCheckApplicationRunner implements ApplicationRunner {
   
   
    @Resource
    private LicenseVerify licenseVerify;

    @Override
    public void run(ApplicationArguments args) throws Exception {
   
   
        LicenseContent content = licenseVerify.install();
    }
}

通过license配置属性,条件装配注入组件:

    @Bean
    @ConditionalOnProperty(name = "ptc.license.start-check", havingValue = "true", matchIfMissing = true)
    public LicenseCheckApplicationRunner licenseCheckApplicationRunner() {
   
   
        return new LicenseCheckApplicationRunner();
    }

通过注解@License实现接口层面的license检验切面

@Aspect
@Order(OrderConstant.AOP_LICENSE)
public class LicenseAspect extends AbstractAspectSupport {
   
   

    @Resource
    private LicenseVerify licenseVerify;

    // 指定切入点为License注解
    @Pointcut("@annotation(com.plasticene.boot.license.core.anno.License)")
    public void licenseAnnotationPointcut() {
   
   
    }

    // 环绕通知
    @Around("licenseAnnotationPointcut()")
    public Object aroundLicense(ProceedingJoinPoint pjp) throws Throwable {
   
   
        boolean b = licenseVerify.verify();
        if (b) {
   
   
            return pjp.proceed();
        }
        return null;
    }

}

license证书验证核心逻辑:LicenseVerify

@Component
public class LicenseVerify {
   
   
    @Resource
    private LicenseProperties licenseProperties;

    private static Logger logger = LogManager.getLogger(LicenseVerify.class);
    private static final  DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    /**
     * 安装License证书
     * 项目服务启动时候安装证书,检验合法性
     * 此时根据开关验证服务器系统信息
     */
    public synchronized LicenseContent install() {
   
   
        LicenseContent result = null;
        try{
   
   
            LicenseManager licenseManager = new LicenseManager(initLicenseParam());
            licenseManager.uninstall();
            result = licenseManager.install(new File(licenseProperties.getLicensePath()));
            verifySystemInfo(result);
            logger.info("证书安装成功,证书有效期:{} - {}", df.format(result.getNotBefore()),
                    df.format(result.getNotAfter()));
        }catch (Exception e){
   
   
            logger.error("证书安装失败:", e);
            throw new BizException("证书安装失败");
        }
        return result;
    }

    /**
     * 校验License证书, 在接口使用{@link com.plasticene.boot.license.core.anno.License}
     * 时候进入license切面时候调用,此时无需再验证服务器系统信息,验证证书和有效期即可
     */
    public boolean verify() {
   
   
        try {
   
   
            LicenseManager licenseManager = new LicenseManager(initLicenseParam());
            LicenseContent licenseContent = licenseManager.verify();
            verifyExpiry(licenseContent);
            return true;
        }catch (Exception e){
   
   
            logger.error("证书校验失败:", e);
            throw new BizException("证书检验失败");
        }
    }

    /**
     * 初始化证书生成参数
     */
    private LicenseParam initLicenseParam(){
   
   
        Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);

        CipherParam cipherParam = new DefaultCipherParam(licenseProperties.getStorePass());

        KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
                ,licenseProperties.getPublicKeysStorePath()
                ,licenseProperties.getPublicAlias()
                ,licenseProperties.getStorePass()
                ,null);

        return new DefaultLicenseParam(licenseProperties.getSubject()
                ,preferences
                ,publicStoreParam
                ,cipherParam);
    }

    // 验证证书有效期
    private void verifyExpiry(LicenseContent licenseContent) {
   
   
        Date expiry = licenseContent.getNotAfter();
        Date current = new Date();
        if (current.after(expiry)) {
   
   
            throw new BizException("证书已过期");
        }
    }

    private void verifySystemInfo(LicenseContent licenseContent) {
   
   
        if (licenseProperties.getVerifySystemSwitch()) {
   
   
            SystemInfo systemInfo = (SystemInfo) licenseContent.getExtra();
            VerifySystemType verifySystemType = licenseProperties.getVerifySystemType();
            switch (verifySystemType) {
   
   
                case CPU_ID:
                    checkCpuId(systemInfo.getCpuId());
                    break;
                case SYSTEM_UUID:
                    checkSystemUuid(systemInfo.getUuid());
                    break;
                default:
            }
        }
    }


    private void checkCpuId(String cpuId) {
   
   
        cpuId = cpuId.trim().toUpperCase();
        String systemCpuId = DmcUtils.getCpuId().trim().toUpperCase();
        logger.info("配置cpuId = {},  系统cpuId = {}", cpuId, systemCpuId);
        if (!Objects.equals(cpuId, systemCpuId)) {
   
   
            throw new BizException("license检验cpuId不一致");
        }
    }

    private void checkSystemUuid(String uuid) {
   
   
        uuid = uuid.trim().toUpperCase();
        String systemUuid = DmcUtils.getSystemUuid().trim().toUpperCase();
        logger.info("配置uuid = {},  系统uuid= {}", uuid, systemUuid);
        if (!Objects.equals(uuid, systemUuid)) {
   
   
            throw new BizException("license检验uuid不一致");
        }
    }

}

至此,关于使用 TrueLicense 生成和验证License就结束了,完整项目代码请看:https://github.com/plasticene/plasticene-boot-starter-parent/tree/main/plasticene-boot-starter-license

目录
相关文章
|
9月前
|
iOS开发 开发者
📝 App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
在iOS应用程序开发过程中,进行App备案并获取公钥及证书SHA-1指纹是至关重要的步骤。本文将介绍如何通过appuploader工具获取iOS云管理式证书 Distribution Managed 公钥及证书SHA-1指纹,帮助开发者更好地理解和应用该过程。
|
11月前
|
安全 Java Android开发
DigiCert代码签名证书
DigiCert代码签名证书
212 0
|
28天前
|
iOS开发
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
81 0
App备案与iOS云管理式证书 ,公钥及证书SHA-1指纹的获取方法
|
28天前
|
Ubuntu 应用服务中间件 网络安全
如何从商业证书颁发机构安装 SSL 证书
如何从商业证书颁发机构安装 SSL 证书
29 0
|
4月前
|
Android开发 iOS开发 开发者
App备案-iOS云管理式证书 Distribution Managed 公钥及证书SHA-1指纹的获取方法
App备案-iOS云管理式证书 Distribution Managed 公钥及证书SHA-1指纹的获取方法
323 0
|
算法 网络安全 数据安全/隐私保护
证书转换-SSL证书生成:cer,jks文件 韩俊强的博客
一.生成.jks文件 资料:HTTPS-老司机手把手教你SSL证书申购-TrustAsia证书 HTTPS时代已来,手把手指导申请免费SSL证书 1、keystore的生成: 分阶段生成: keytool -genkey -alias yushan(...
7798 0
|
4月前
|
存储 弹性计算 运维
给多个用户颁发证书
【4月更文挑战第30天】
19 1
|
4月前
|
弹性计算 运维 安全
给一个用户颁发证书
【4月更文挑战第30天】
27 0
CNVD原创证书获取姿势(视频版)
CNVD原创证书获取姿势(视频版)
162 1
|
安全 JavaScript 前端开发
CNVD原创证书获取姿势
挖CNVD证书是在准备出去实习的时候,想着挖个CNVD证书丰富一下自己的简历,再到后来挖CNVD证书完全是因为众测实在是太卷了,出洞率低,重洞率高,导致自己经常挖完众测之后就emo,然后去挖CNVD找一些存在感。很多师傅感觉挖CNVD证书很难,要挖掘0day出来。确实,要挖0day,听起来确实是挺困难的,但是挖CNVD证书不像是众测有限定的资产,互联网上存在的系统数不胜数,我们只需要利用一些小技巧,找到满足发放证书的系统,然后在这里面去寻找相对薄弱的系统来进行挖掘,这样相对来说挖掘到证书的概率还是相对来说大上不少的。
663 0