SpringBoot 实现 License 认证:快速校验有效期的简洁方案

作者:微信小助手

发布时间:2024-12-30T12:43:51


SpringBoot 实现 License 认证:快速校验有效期的简洁方案

在现代软件开发中,特别是商业软件领域,License认证是一种至关重要的技术,用于限制软件的使用范围和授权模式。它不仅保护开发者的利益,也为用户提供合法使用的依据。License认证通常涉及生成密钥对、签发证书以及验证有效性等步骤,这些过程虽然复杂,但通过适当的工具和技术可以简化实现。在本篇文章中,我们将基于 SpringBoot 框架,结合 TrueLicense 开源工具,展示如何快速实现 License 认证,尤其是围绕“有效期校验”的核心功能。通过阅读本文,您将掌握如何生成密钥对、创建 License 证书以及如何在项目中集成并校验证书的有效性,助力您构建更加安全可靠的软件系统。

License 简介

License 即版权许可证书,用于为收费软件提供访问许可证明,常见于以下场景:

  • 应用场景

    部署在客户内网环境,开发者无法控制其网络状态,需通过离线方式校验证书有效性。
  • 授权原理

    利用证书管理工具生成密钥对,私钥用于创建 License 证书,公钥验证其合法性和有效期。

授权者生成密钥对

授权者通过 Keytool 工具生成私钥和公钥,并完成证书库的管理。

# 生成私钥库
keytool -genkeypair-keysize2048-validity3650-alias"privateKey"-keystore"privateKeys.keystore"-storepass"myStorePass123"-keypass"myKeyPass123"-dname"CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"

# 导出公钥到文件
keytool -exportcert-alias"privateKey"-keystore"privateKeys.keystore"-storepass"myStorePass123"-file"certfile.cer"

# 导入证书到公钥库
keytool -import-alias"publicCert"-file"certfile.cer"-keystore"publicCerts.keystore"-storepass"myStorePass123"

授权者生成 License 文件

Maven 依赖

在项目的 pom.xml 文件中添加 TrueLicense 的核心库依赖:

<dependency>
    <groupId>de.schlichtherle.truelicensegroupId>
    <artifactId>truelicense-coreartifactId>
    <version>1.33version>
dependency>

License 生成类

以下代码实现了 License 文件的生成逻辑。

package com.icoderoad.license;

importde.schlichtherle.license.*;
importlombok.extern.slf4j.Slf4j;

importjavax.security.auth.x500.X500Principal;
importjava.io.File;
importjava.util.Date;
importjava.util.prefs.Preferences;

@Slf4j
publicclassLicenseCreator{

    privatestaticfinalX500PrincipalDEFAULT_HOLDER_AND_ISSUER=
            newX500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");

    publicstaticvoidmain(String[] args){
        LicenseCreatorParam param =newLicenseCreatorParam();
        param.setSubject("license");
        param.setPrivateAlias("privateKey");
        param.setKeyPass("myKeyPass123");
        param.setStorePass("myStorePass123");
        param.setLicensePath("license.lic");
        param.setPrivateKeysStorePath("privateKeys.keystore");
        param.setIssuedTime(newDate());
        param.setExpiryTime(newDate(System.currentTimeMillis()+10L*365*24*60*60*1000));
        param.setConsumerType("user");
        param.setConsumerAmount(1);
        param.setDescription("This is a license file.");

        try{
            newLicenseCreator().generateLicense(param);
            log.info("License file generated successfully at: {}", param.getLicensePath());
        }catch(Exception e){
            log.error("Failed to generate license file", e);
        }
    }

    publicvoidgenerateLicense(LicenseCreatorParam param){
        try{
            LicenseManager licenseManager =newLicenseManager(initLicenseParam(param));
            LicenseContent licenseContent =initLicenseContent(param);
            licenseManager.store(licenseContent,newFile(param.getLicensePath()));
        }catch(Exception e){
            log.error("License generation failed", e);
        }
    }

    privatestaticLicenseParaminitLicenseParam(LicenseCreatorParam param){
        Preferences preferences =Preferences.userNodeForPackage(LicenseCreator.class);
        CipherParam cipherParam =newDefaultCipherParam(param.getStorePass());
        KeyStoreParam privateStoreParam =newCustomKeyStoreParam(
                LicenseCreator.class,
                param.getPrivateKeysStorePath(),
                param.getPrivateAlias(),
                param.getStorePass(),
                param.getKeyPass()
        );
        returnnewDefaultLicenseParam(param.getSubject(), preferences, privateStoreParam, cipherParam);
    }

    privatestaticLicenseContentinitLicenseContent(LicenseCreatorParam param){
        LicenseContent content =newLicenseContent();
        content.setHolder(DEFAULT_HOLDER_AND_ISSUER);
        content.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
        content.setSubject(param.getSubject());
        content.setIssued(param.getIssuedTime());
        content.setNotBefore(param.getIssuedTime());
        content.setNotAfter(param.getExpiryTime());
        content.setConsumerType(param.getConsumerType());
        content.setConsumerAmount(param.getConsumerAmount());
        content.setInfo(param.getDescription());
        return content;
    }
}

LicenseCreatorParam 类

package com.icoderoad.license;

importcom.fasterxml.jackson.annotation.JsonFormat;
importlombok.Data;

importjava.util.Date;

@Data
publicclassLicenseCreatorParam{
    privateString subject;
    privateString privateAlias;
    privateString keyPass;
    privateString storePass;
    privateString licensePath;
    privateString privateKeysStorePath;
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone ="GMT+8")
    privateDate issuedTime;
    @JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone ="GMT+8")
    privateDate expiryTime;
    privateString consumerType;
    privateInteger consumerAmount;
    privateString description;
}

CustomKeyStoreParam 类

package com.icoderoad.license;

importde.schlichtherle.license.AbstractKeyStoreParam;

publicclassCustomKeyStoreParamextendsAbstractKeyStoreParam{

    privatefinalString storePath;
    privatefinalString alias;
    privatefinalString storePwd;
    privatefinalString keyPwd;

    publicCustomKeyStoreParam(Class<?> clazz,String storePath,String alias,String storePwd,String keyPwd){
        super(clazz, storePath);
        this.storePath = storePath;
        this.alias = alias;
        this.storePwd = storePwd;
        this.keyPwd = keyPwd;
    }

    @Override
    publicStringgetAlias(){
        return alias;
    }

    @Override
    publicStringgetStorePwd(){
        return storePwd;
    }

    @Override
    publicStringgetKeyPwd(){
        return keyPwd;
    }
}

使用者配置验证

使用者需在项目中加载 publicKeys.keystore  license.lic 文件,并在应用启动时进行校验。

  • 校验逻辑
    :通过公钥验证证书的合法性及有效期。
  • 安全建议
    :妥善保护私钥文件,不得泄露给使用者。

基于 TrueLicense 的 License 验证与安装优化

本文将详细介绍如何在项目中通过 TrueLicense 实现 License 的验证与安装。以下优化后的代码和配置旨在满足不同使用场景的需求。

使用者的 pom.xml 配置


<dependency>
    <groupId>de.schlichtherle.truelicensegroupId>
    <artifactId>truelicense-coreartifactId>
    <version>1.33version>
dependency>

License 校验类

创建 com.icoderoad.license.LicenseVerify 类:

package com.icoderoad.license;

importde.schlichtherle.license.*;
importlombok.Data;
importlombok.extern.slf4j.Slf4j;

importjava.io.File;
importjava.text.DateFormat;
importjava.text.MessageFormat;
importjava.text.SimpleDateFormat;
importjava.util.prefs.Preferences;

@Slf4j
publicclassLicenseVerify{

    // 安装 License 证书
    publicsynchronizedLicenseContentinstall(LicenseVerifyParam param){
        LicenseContent result =null;
        DateFormat format =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        try{
            LicenseManager licenseManager =LicenseManagerHolder.getInstance(initLicenseParam(param));
            licenseManager.uninstall();
            result = licenseManager.install(newFile(param.getLicensePath()));
            log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",
                     format.format(result.getNotBefore()), format.format(result.getNotAfter())));
        }catch(Exception e){
            log.error("证书安装失败: {}", e.getMessage());
        }
        return result;
    }

    // 校验 License 证书
    publicbooleanverify(){
        LicenseManager licenseManager =LicenseManagerHolder.getInstance(null);
        DateFormat format =newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        try{
            LicenseContent licenseContent = licenseManager.verify();
            log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",
                     format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter())));
            returntrue;
        }catch(Exception e){
            log.error("证书校验失败: {}", e.getMessage());
            returnfalse;
        }
    }

    // 初始化证书生成参数
    privateLicenseParaminitLicenseParam(LicenseVerifyParam param){
        Preferences preferences =Preferences.userNodeForPackage(LicenseVerify.class);
        CipherParam cipherParam =newDefaultCipherParam(param.getStorePass());
        KeyStoreParam publicStoreParam =newCustomKeyStoreParam(LicenseVerify.class,
                param.getPublicKeysStorePath(),
                param.getPublicAlias(),
                param.getStorePass(),
                null);

        returnnewDefaultLicenseParam(param.getSubject(), preferences, publicStoreParam, cipherParam);
    }
}

License 校验类需要的参数

package com.icoderoad.license;

@Data
publicclassLicenseVerifyParam{
    privateString subject;
    privateString publicAlias;
    privateString storePass;
    privateString licensePath;
    privateString publicKeysStorePath;
}

自定义 KeyStoreParam

package com.icoderoad.license;

importde.schlichtherle.license.AbstractKeyStoreParam;

importjava.io.IOException;
importjava.io.InputStream;
importjava.nio.file.Files;
importjava.nio.file.Paths;

publicclassCustomKeyStoreParamextendsAbstractKeyStoreParam{
    privatefinalString storePath;
    privatefinalString alias;
    privatefinalString storePwd;
    privatefinalString keyPwd;

    publicCustomKeyStoreParam(Class clazz,String resource,String alias,String storePwd,String keyPwd){
        super(clazz, resource);
        this.storePath = resource;
        this.alias = alias;
        this.storePwd = storePwd;
        this.keyPwd = keyPwd;
    }

    @Override
    publicStringgetAlias(){
        return alias;
    }

    @Override
    publicStringgetStorePwd(){
        return storePwd;
    }

    @Override
    publicStringgetKeyPwd(){
        return keyPwd;
    }

    @Override
    publicInputStreamgetStream()throwsIOException{
        returnFiles.newInputStream(Paths.get(storePath));
    }
}

LicenseManager 的单例

package com.icoderoad.license;

importde.schlichtherle.license.LicenseManager;
importde.schlichtherle.license.LicenseParam;

publicclassLicenseManagerHolder{
    privatestaticvolatileLicenseManagerLICENSE_MANAGER;

    publicstaticLicenseManagergetInstance(LicenseParam param){
        if(LICENSE_MANAGER==null){
            synchronized(LicenseManagerHolder.class){
                if(LICENSE_MANAGER==null){
                    LICENSE_MANAGER=newLicenseManager(param);
                }
            }
        }
        returnLICENSE_MANAGER;
    }
}

项目启动时安装证书

application.properties 配置
# License 配置
license.subject=license
license.publicAlias=myKeyPass123
license.storePass=myStorePass123
LicenseCheckRunner 
package com.icoderoad.license;

importlombok.extern.slf4j.Slf4j;
importorg.springframework.beans.factory.annotation.Value;
importorg.springframework.boot.ApplicationArguments;
importorg.springframework.boot.ApplicationRunner;
importorg.springframework.stereotype.Component;

@Slf4j
@Component
publicclassLicenseCheckRunnerimplementsApplicationRunner{

    @Value("${license.subject}")
    privateString subject;

    @Value("${license.publicAlias}")
    privateString publicAlias;

    @Value("${license.storePass}")
    privateString storePass;

    @Override
    publicvoidrun(ApplicationArguments args)throwsException{
        log.info("++++++++ 开始安装证书 ++++++++");
        LicenseVerifyParam param =newLicenseVerifyParam();
        param.setSubject(subject);
        param.setPublicAlias(publicAlias);
        param.setStorePass(storePass);

        String resourcePath =getClass().getClassLoader().getResource("").getPath();
        param.setLicensePath(resourcePath +"license.lic");
        param.setPublicKeysStorePath(resourcePath +"publicCerts.keystore");

        LicenseVerify licenseVerify =newLicenseVerify();
        licenseVerify.install(param);
        log.info("++++++++ 证书安装结束 ++++++++");
    }
}

配置拦截器

LicenseCheckInterceptor
package com.icoderoad.interceptor;

importcom.icoderoad.license.LicenseVerify;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.web.servlet.HandlerInterceptor;

importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
importjava.util.HashMap;
importjava.util.Map;

@Slf4j
publicclassLicenseCheckInterceptorimplementsHandlerInterceptor{
    @Override
    publicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{
        LicenseVerify licenseVerify =newLicenseVerify();
        boolean verifyResult = licenseVerify.verify();

        if(verifyResult){
            returntrue;
        }else{
            response.setCharacterEncoding("utf-8");
            Map<String, String> result =newHashMap<>(1);
            result.put("result","您的证书无效,请核查服务器是否取得授权或重新申请证书!");
            response.getWriter().write(JSON.toJSONString(result));
            returnfalse;
        }
    }
}
注册拦截器
package com.icoderoad.config;

importcom.icoderoad.interceptor.LicenseCheckInterceptor;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.web.servlet.config.annotation.InterceptorRegistry;
importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
publicclassWebMvcConfigimplementsWebMvcConfigurer{
    @Override
    publicvoidaddInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(newLicenseCheckInterceptor());
    }
}

总结

本文系统性地阐述了在 SpringBoot 项目中实现 License 认证的简洁方案。从背景概述到实现细节,我们深入探讨了如何通过生成密钥对和证书实现高效的 License 校验机制。

通过使用 TrueLicense 工具,开发者能够快速生成符合项目需求的证书,而在项目中引入 License 验证功能后,可以有效提升应用的安全性和合规性。此外,本文所介绍的方案灵活性强,适用于多种使用场景,尤其是在复杂的企业内网部署环境中具有显著优势。
未来,您可以根据项目的特定需求,进一步扩展 License 认证功能,例如:绑定硬件信息、增加多级权限控制等,以实现更加精细化的授权管理。希望本篇文章能够为您提供切实可行的技术思路,助力构建专业的 License 认证体系。