2022-11-16
后端
00
请注意,本文编写于 701 天前,最后修改于 150 天前,其中某些信息可能已经过时。

目录

一、简介
二、keytool生产密钥对
三、SpringBoot整合TrueLicense
1、pom.xml添加依赖
2、代码封装
2.1 CustomKeyStoreParam
2.2 LicenseExtraModel
2.3 CustomLicenseManager
2.4 LicenseCreatorParam
2.5 LicenseCreator (此类不能放到客户部署的项目中)
3、客户产品代码需格外封装
3.1 LicenseVerify
3.2 LicenseConfig
3.3 application.yml文件需要的配置
四、验证

一、简介

官网 TrueLicense 是一个 Java 软件许可证管理框架,它提供了一种简单而强大的方式来管理和控制您的软件产品的使用。通过 TrueLicense,您可以为您的应用程序添加试用期、限制使用次数、指定特定的硬件环境等功能,以确保您的软件在被非法使用或滥用时得到保护。

二、keytool生产密钥对

keytool是jdk里面自带的命令

## 1. 生成私匙库 # validity:私钥的有效期多少天 # alias:私钥别称 # keystore: 指定私钥库文件的名称(生成在当前目录) # storepass:指定私钥库的密码(获取keystore信息所需的密码) # keypass:指定别名条目的密码(私钥的密码) C:\java\jdk1.8.0_241\bin\keytool -genkeypair -keysize 1024 -validity 3650 -alias privateKey -keystore "privateKeys.keystore" -storepass "a123456" -keypass "a123456" ## 2. 把私匙库内的公匙导出到一个文件当中 # alias:私钥别称 # keystore:指定私钥库的名称(在当前目录查找) # storepass: 指定私钥库的密码 # file:证书名称 C:\java\jdk1.8.0_241\bin\keytool -exportcert -alias privateKey -keystore "privateKeys.keystore" -storepass "a123456" -file "certfile.cer" ## 3. 再把这个证书文件导入到公匙库 # alias:公钥别称 # file:证书名称 # keystore:公钥文件名称 # storepass:指定私钥库的密码 C:\java\jdk1.8.0_241\bin\keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "a123456"

通过上面三行代码执行,当前目录产生4个文件

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

三、SpringBoot整合TrueLicense

1、pom.xml添加依赖

<!-- lisence验证 --> <dependency> <groupId>de.schlichtherle.truelicense</groupId> <artifactId>truelicense-core</artifactId> <version>1.33</version> </dependency>

2、代码封装

整体代码如图封装

image-20230605150058859

2.1 CustomKeyStoreParam

自定义KeyStoreParam类CustomKeyStoreParam类继承AbstractKeyStoreParam,实现里面一些该实现的方法。并且重写getStream()获取文件内容的方法,改成从磁盘位置读取。

package com.example.demo.license; import de.schlichtherle.license.AbstractKeyStoreParam; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * 自定义KeyStoreParam,用于将公私钥存储文件存放到其他磁盘位置而不是项目中。现场使用的时候公钥大部分都不会放在项目中的 * @author Chen Shaohua * @date 2022/11/15 15:28 */ public class CustomKeyStoreParam extends AbstractKeyStoreParam { /** * 公钥/私钥在磁盘上的存储路径 */ private String storePath; private String alias; private String storePwd; private String keyPwd; public CustomKeyStoreParam(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 public String getAlias() { return alias; } @Override public String getStorePwd() { return storePwd; } @Override public String getKeyPwd() { return keyPwd; } /** * AbstractKeyStoreParam里面的getStream()方法默认文件是存储的项目中。 * 用于将公私钥存储文件存放到其他磁盘位置而不是项目中 */ @Override public InputStream getStream() throws IOException { return new FileInputStream(new File(storePath)); } }
2.2 LicenseExtraModel

自定义一个LicenseExtraModel类。我们可以在这里添加一些额外的验证信息(TrueLicense默认只帮我们验证了时间),比如我们可以验证客户的机器码啥的。后面实现。

注:客户端和服务端必须包名一致,否则报错,目前没找到别的办法,应该是证书序列化所以包名也一致。

package com.example.demo.license; /** * 自定义需要校验的License参数,可以增加一些额外需要校验的参数,比如项目信息,ip地址信息等等,待完善 * @author Chen Shaohua * @date 2022/11/15 15:29 */ public class LicenseExtraModel { }
2.3 CustomLicenseManager

继承LicenseManager类,增加我们额外信息的验证(TrueLicense默认只给我们验证了时间)。大家需要根据自己的需求在validate()里面增加额外的验证。

package com.example.demo.license; import de.schlichtherle.license.*; import de.schlichtherle.xml.GenericCertificate; import de.schlichtherle.xml.XMLConstants; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.util.Date; /** * 自定义LicenseManager,用于增加额外的信息校验(除了LicenseManager的校验,我们还可以在这个类里面添加额外的校验信息) * @author Chen Shaohua * @date 2022/11/15 15:31 */ public class CustomLicenseManager extends LicenseManager { private static Logger logger = LogManager.getLogger(CustomLicenseManager.class); public CustomLicenseManager(LicenseParam param) { super(param); } /** * 复写create方法 */ @Override protected synchronized byte[] create(LicenseContent content, LicenseNotary notary) throws Exception { initialize(content); this.validateCreate(content); final GenericCertificate certificate = notary.sign(content); return getPrivacyGuard().cert2key(certificate); } /** * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息 */ @Override protected synchronized LicenseContent install(final byte[] key, final LicenseNotary notary) throws Exception { final GenericCertificate certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded()); this.validate(content); setLicenseKey(key); setCertificate(certificate); return content; } /** * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息 */ @Override protected synchronized LicenseContent verify(final LicenseNotary notary) throws Exception { // Load license key from preferences, final byte[] key = getLicenseKey(); if (null == key) { throw new NoLicenseInstalledException(getLicenseParam().getSubject()); } GenericCertificate certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded()); this.validate(content); setCertificate(certificate); return content; } /** * 校验生成证书的参数信息 */ protected synchronized void validateCreate(final LicenseContent content) throws LicenseContentException { final LicenseParam param = getLicenseParam(); final Date now = new Date(); final Date notBefore = content.getNotBefore(); final Date notAfter = content.getNotAfter(); if (null != notAfter && now.after(notAfter)) { throw new LicenseContentException("证书失效时间不能早于当前时间"); } if (null != notBefore && null != notAfter && notAfter.before(notBefore)) { throw new LicenseContentException("证书生效时间不能晚于证书失效时间"); } final String consumerType = content.getConsumerType(); if (null == consumerType) { throw new LicenseContentException("用户类型不能为空"); } } /** * 复写validate方法,用于增加我们额外的校验信息 */ @Override protected synchronized void validate(final LicenseContent content) throws LicenseContentException { //1. 首先调用父类的validate方法 super.validate(content); //2. 然后校验自定义的License参数,去校验我们的license信息 LicenseExtraModel expectedCheckModel = (LicenseExtraModel) content.getExtra(); // 做我们自定义的校验 } /** * 重写XMLDecoder解析XML */ private Object load(String encoded) { BufferedInputStream inputStream = null; XMLDecoder decoder = null; try { inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XMLConstants.XML_CHARSET))); decoder = new XMLDecoder(new BufferedInputStream(inputStream, XMLConstants.DEFAULT_BUFSIZE), null, null); return decoder.readObject(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } finally { try { if (decoder != null) { decoder.close(); } if (inputStream != null) { inputStream.close(); } } catch (Exception e) { logger.error("XMLDecoder解析XML失败", e); } } return null; } }
2.4 LicenseCreatorParam

生成证书需要的参数实体类

package com.example.demo.license; import java.io.Serializable; import java.util.Date; import lombok.Data; /** * License生成类需要的参数 * @author Chen Shaohua * @date 2022/11/15 15:32 */ @data public class LicenseCreatorParam implements Serializable { private static final long serialVersionUID = -7793154252684580872L; /** * 证书subject */ private String subject; /** * 私钥别称 */ private String privateAlias; /** * 私钥别称 */ private String keyalg="RSA"; /** * 私钥密码(需要妥善保管,不能让使用者知道) */ private String keyPass; /** * 访问私钥库的密码 */ private String storePass; /** * 证书生成路径 */ private String licensePath; /** * 私钥库存储路径 */ private String privateKeysStorePath; /** * 证书生效时间 */ private Date issuedTime = new Date(); /** * 证书失效时间 */ private Date expiryTime; /** * 用户类型 */ private String consumerType = "user"; /** * 用户数量 */ private Integer consumerAmount = 1; /** * 描述信息 */ private String description = ""; /** * 额外的服务器硬件校验信息 */ private LicenseExtraModel licenseExtraModel; }
2.5 LicenseCreator (此类不能放到客户部署的项目中)

license生成,会生成一个license.lic文件,与公钥文件一并放入客户机器用于校检。

package com.example.demo.license; import de.schlichtherle.license.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.security.auth.x500.X500Principal; import java.io.File; import java.text.MessageFormat; import java.util.Calendar; import java.util.prefs.Preferences; /** * License生成类 -- 用于license生成 * @author Chen Shaohua * @date 2022/11/15 15:33 */ public class LicenseCreator { private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"); private static Logger logger = LogManager.getLogger(LicenseCreator.class); private LicenseCreatorParam param; public LicenseCreator(LicenseCreatorParam param) { this.param = param; } /** * 生成License证书 */ public boolean generateLicense() { try { LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam()); LicenseContent licenseContent = initLicenseContent(); licenseManager.store(licenseContent, new File(param.getLicensePath())); return true; } catch (Exception e) { logger.error(MessageFormat.format("证书生成失败:{0}", param), e); return false; } } /** * 初始化证书生成参数 */ private LicenseParam initLicenseParam() { Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class); //设置对证书内容加密的秘钥 CipherParam cipherParam = new DefaultCipherParam(param.getStorePass()); KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class , param.getPrivateKeysStorePath() , param.getPrivateAlias() , param.getStorePass() , param.getKeyPass()); return new DefaultLicenseParam(param.getSubject() , preferences , privateStoreParam , cipherParam); } /** * 设置证书生成正文信息 */ private LicenseContent initLicenseContent() { LicenseContent licenseContent = new LicenseContent(); licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER); licenseContent.setIssuer(DEFAULT_HOLDER_AND_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()); //扩展校验,这里可以自定义一些额外的校验信息(也可以用json字符串保存) if (param.getLicenseExtraModel() != null) { licenseContent.setExtra(param.getLicenseExtraModel()); } return licenseContent; } public static void main(String[] args) { // 生成license需要的一些参数 LicenseCreatorParam param = new LicenseCreatorParam(); param.setSubject("ioserver"); param.setPrivateAlias("privateKey"); param.setKeyPass("a123456"); param.setStorePass("a123456"); param.setLicensePath("D:\\my-computer\\Desktop\\新建文件夹 (2)\\ceshi\\license1.lic"); param.setPrivateKeysStorePath("D:\\my-computer\\Desktop\\新建文件夹 (2)\\ceshi\\privateKeys.keystore"); Calendar issueCalendar = Calendar.getInstance(); param.setIssuedTime(issueCalendar.getTime()); Calendar expiryCalendar = Calendar.getInstance(); expiryCalendar.set(2023, Calendar.DECEMBER, 31, 23, 59, 59); param.setExpiryTime(expiryCalendar.getTime()); param.setConsumerType("user"); param.setConsumerAmount(1); param.setDescription("测试"); LicenseCreator licenseCreator = new LicenseCreator(param); // 生成license boolean b = licenseCreator.generateLicense(); System.out.println(b); } }

3、客户产品代码需格外封装

3.1 LicenseVerify

license验证的具体实现

package com.zr.ams.config.license; import de.schlichtherle.license.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.prefs.Preferences; /** * License校验类 * @author Chen Shaohua * @date 2022/11/15 16:17 */ public class LicenseVerify { private static Logger logger = LogManager.getLogger(LicenseVerify.class); /** * 证书subject */ private String subject; /** * 公钥别称 */ private String publicAlias; /** * 访问公钥库的密码 */ private String storePass; /** * 证书生成路径 */ private String licensePath; /** * 密钥库存储路径 */ private String publicKeysStorePath; /** * LicenseManager */ private LicenseManager licenseManager; /** * 标识证书是否安装成功 */ private boolean installSuccess; public LicenseVerify(String subject, String publicAlias, String storePass, String licensePath, String publicKeysStorePath) { this.subject = subject; this.publicAlias = publicAlias; this.storePass = storePass; this.licensePath = licensePath; this.publicKeysStorePath = publicKeysStorePath; } /** * 安装License证书,读取证书相关的信息, 在bean加入容器的时候自动调用 */ public void installLicense() { try { Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class); CipherParam cipherParam = new DefaultCipherParam(storePass); KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class, publicKeysStorePath, publicAlias, storePass, null); LicenseParam licenseParam = new DefaultLicenseParam(subject, preferences, publicStoreParam, cipherParam); licenseManager = new CustomLicenseManager(licenseParam); licenseManager.uninstall(); LicenseContent licenseContent = licenseManager.install(new File(licensePath)); DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); installSuccess = true; logger.info("------------------------------- 证书安装成功 -------------------------------"); logger.info(MessageFormat.format("证书有效期:{0} - {1}", format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter()))); } catch (Exception e) { installSuccess = false; logger.error("------------------------------- 证书安装成功 -------------------------------"); logger.error(e); } } /** * 卸载证书,在bean从容器移除的时候自动调用 */ public void unInstallLicense() { if (installSuccess) { try { licenseManager.uninstall(); } catch (Exception e) { // ignore } } } /** * 校验License证书 */ public boolean verify() { try { LicenseContent licenseContent = licenseManager.verify(); return true; } catch (Exception e) { return false; } } }
3.2 LicenseConfig

把LicenseVerify类添加到Spring容器里面去。在LicenseVerify添加到spring容器的时候顺便调用LicenseVerify对象里面的installLicense()方法加载证书。也就是放入到yml配置中。

package com.zr.ams.config.license; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author Chen Shaohua * @date 2022/11/15 16:19 */ @Configuration public class LicenseConfig { /** * 证书subject */ @Value("${license.subject}") private String subject; /** * 公钥别称 */ @Value("${license.publicAlias}") private String publicAlias; /** * 访问公钥库的密码 */ @Value("${license.storePass}") private String storePass; /** * 证书生成路径 */ @Value("${license.licensePath}") private String licensePath; /** * 密钥库存储路径 */ @Value("${license.publicKeysStorePath}") private String publicKeysStorePath; @Bean(initMethod = "installLicense", destroyMethod = "unInstallLicense") public LicenseVerify licenseVerify() { return new LicenseVerify(subject, publicAlias, storePass, licensePath, publicKeysStorePath); } }
3.3 application.yml文件需要的配置
#License相关配置 license: subject: ioserver #主题 publicAlias: publicCert #公钥别称 storePass: a123456 #访问公钥的密码 licensePath: D:\license.lic #license位置 publicKeysStorePath: D:publicCerts.keystore #公钥位置

四、验证

客户产品启动会自动验证license,

package com.zr.ams.controller; import com.zr.ams.config.license.LicenseVerify; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Chen Shaohua * @date 2022/11/15 16:21 */ @RestController public class LicenseTest { private LicenseVerify licenseVerify; @Autowired public void setLicenseVerify(LicenseVerify licenseVerify) { this.licenseVerify = licenseVerify; } @RequestMapping(value = "/licenseVerify") public void licenseVerify() { System.out.println("licese是否有效:" + licenseVerify.verify()); } }
10:13:58.816 [main] INFO com.zr.ams.config.license.LicenseVerify - ------------------------------- 证书安装成功 ------------------------------- 10:13:58.817 [main] INFO com.zr.ams.config.license.LicenseVerify - 证书有效期:2022-11-15 16:30:12 - 2023-12-31 23:59:59

本文作者:酷少少

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!