文章目录
前言
一、使用场景以及truelicense是什么
二、原理
三、使用Keytool命令生成密钥对
四、实现代码 - 证书生成
五、测试 - 证书生成
六、代码实现 - 证书安装和校验
七、测试 - 证书的安装和校验
八、执行代码期间遇到的问题
九、参考资料
前言
最近接到一个情况,公司平台有个授权使用的机制,之前负载这个事情的人走了,留在svn上的代码是无法通过授权的,所以让我看看什么情况
一、使用场景以及truelicense是什么
官网地址
TrueLicense是一个开源的证书管理引擎,使用场景:当项目交付给客户之后用签名来保证客户不能随意使用项目 默认校验了开始结束时间,可扩展增加mac地址校验等。 其中还有ftp的校验没有尝试,本demo详细介绍的是本地校验 license授权机制的原理: 生成密钥对,方法有很多。我们使用trueLicense来做软件产品的保护,我们主要使用它的LicenseManager类来生成证书文件、安装证书文件、验证证书文件.
二、原理
首先需要生成密钥对,方法有很多,JDK中提供的KeyTool即可生成。
授权者保留私钥,使用私钥对包含授权信息(如截止日期,MAC地址等)的license进行数字签名。
公钥交给使用者(放在验证的代码中使用),用于验证license是否符合使用条件。
三、使用Keytool命令生成密钥对
首先
Keytool 是一个Java 数据证书的管理工具 ,Keytool 将密钥(key)和证书(certificates)存在一个称为keystore的文件中 在keystore里,包含两种数据:
密钥实体(Key entity)——密钥(secret key)又或者是私钥和配对公钥(采用非对称加密)
可信任的证书实体(trusted certificate entries)——只包含公钥
在接触代码前,我们先来大概熟悉下密钥生成的流程吧
Tips: 以下命令详细的参数需要可查看参考资料.4
1、首先要用KeyTool工具来生成私匙库:(-alias别名 –validity 3650表示10年有效)
keytool -genkey -alias privatekey -keystore privateKeys.store -validity 3650
这里密码我使用123456q
注意!!!默认的密码策略是6未数字与字母,如果不遵守会报错 第五节第二点的错
这个时候,会在打开命令行的地方创建出一个文件,privateKeys.store()
2、然后把私匙库内的证书导出到一个文件当中:
keytool -export -alias privatekey -file certfile.cer -keystore privateKeys.store
生成certfile.cer(证书),生成公钥库后就没什么用了
3、然后再把这个证书文件导入到公匙库:
keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store
生成 publicCerts.store
privateKeys.keystore:私钥,这个我们自己留着,不能泄露给别人。
publicCerts.keystore:公钥,这个给客户用的。在我们程序里面就是用他配合license进行授权信息的校验的。
certfile.cer:这个文件没啥用,可以删掉。
最后自行将生成文件privateKeys.store、publicCerts.store拷贝出来备用。
四、实现代码 - 证书生成
maven依赖
<!-- truelicense 依赖-->
<!-- https://mvnrepository.com/artifact/de.schlichtherle.truelicense/truelicense-core -->
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
首先从整个流程上来讲,现在这步是证书生成,证书生成需要私钥库和证书参数
在这个引擎中,公/私钥库默认是存储在项目中的。** 但是,我们实际生产环境中,都是将配置文件等脱离项目部署的**,所以我们需要重写它获取公/私钥库的地方。
CustomKeyStoreParam.java
import de.schlichtherle.license.AbstractKeyStoreParam;
import org.springframework.util.ResourceUtils;
import java.io.*;
/**
* 自定义KeyStoreParam,用于将公私钥存储文件存放到其他磁盘位置而不是项目中。现场使用的时候公钥大部分都不会放在项目中的
*/
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));
File file = ResourceUtils.getFile(storePath);
if (file.exists()) {
return new FileInputStream(file);
} else {
throw new FileNotFoundException(storePath);
}
}
}
证书参数可以用配置文件配置,也可以写成类,这个方法用的就是类的方式
License.java
import cn.genm.license.dto.LicenseExtraModel;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* License生成类需要的参数
*/
@Data
public class License implements Serializable {
private static final long serialVersionUID = -7793154252684580872L;
/**
* 证书subject
*/
private String subject;
/**
* 私钥别称
*/
private String privateAlias;
/**
* 私钥密码(需要妥善保管,不能让使用者知道)
*/
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;
}
其中的扩展参数类
/**
* 自定义需要校验的License参数,可以增加一些额外需要校验的参数,比如项目信息,ip地址信息等等,待完善
*/
public class LicenseExtraModel {
// 这里可以添加一些往外的自定义信息,比如我们可以增加项目验证,客户电脑sn码的验证等等
}
由于引擎本身默认只验证了有效期,当我们需要自定义一个继承于LicenseManager的自定义证书管理器。
额外的信息的校验可以加在validate()方法里
CustomLicenseManager.java
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的校验,我们还可以在这个类里面添加额外的校验信息)
*/
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;
}
}
前面的所有可以说都是为了整个流程在铺垫,现在开始是真正开始生成License证书的代码
LicenseCreator.java
import cn.genm.license.dto.CustomKeyStoreParam;
import cn.genm.license.dto.CustomLicenseManager;
import cn.genm.license.model.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.prefs.Preferences;
/**
* License生成类 -- 用于license生成
*/
public class LicenseCreator {
private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=GENMER, OU=GENM, O=GENM, L=FUZHOU, ST=FUJIAN, C=CHINA");
private static Logger logger = LogManager.getLogger(LicenseCreator.class);
private License license;
public LicenseCreator(License license) {
this.license = license;
}
/**
* 生成License证书
*/
public boolean generateLicense() {
try {
LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());
LicenseContent licenseContent = initLicenseContent();
licenseManager.store(licenseContent, new File(license.getLicensePath()));
return true;
} catch (Exception e) {
logger.error(MessageFormat.format("证书生成失败:{0}", license), e);
return false;
}
}
/**
* 初始化证书生成参数
*/
private LicenseParam initLicenseParam() {
Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
//设置对证书内容加密的秘钥
CipherParam cipherParam = new DefaultCipherParam(license.getStorePass());
KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
, license.getPrivateKeysStorePath()
, license.getPrivateAlias()
, license.getStorePass()
, license.getKeyPass());
return new DefaultLicenseParam(license.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(license.getSubject());
licenseContent.setIssued(license.getIssuedTime());
licenseContent.setNotBefore(license.getIssuedTime());
licenseContent.setNotAfter(license.getExpiryTime());
licenseContent.setConsumerType(license.getConsumerType());
licenseContent.setConsumerAmount(license.getConsumerAmount());
licenseContent.setInfo(license.getDescription());
//扩展校验,这里可以自定义一些额外的校验信息(也可以用json字符串保存)
if (license.getLicenseExtraModel() != null) {
licenseContent.setExtra(license.getLicenseExtraModel());
}
return licenseContent;
}
}
五、测试 - 证书生成
环境工具类都准备好了,接下来直接开始测试,看看能否生成
@Test
void generateLicense() {
// 生成license需要的一些参数
License param = new License();
// 证书授权主体
param.setSubject("licenseTest");
// 私钥别名
param.setPrivateAlias("privateKey");
// 私钥密码(需要妥善保管,不能让使用者知道)
param.setKeyPass("123456q");
// 访问私钥库的密码
param.setStorePass("123456q");
// 证书存储地址
param.setLicensePath("E:\\license2\\license.lic");
// 私钥库所在地址
param.setPrivateKeysStorePath("E:\\license\\privateKeys.store");
// 证书生效时间
Calendar issueCalendar = Calendar.getInstance();
param.setIssuedTime(issueCalendar.getTime());
// 证书失效时间
Calendar expiryCalendar = Calendar.getInstance();
// 设置当前时间
expiryCalendar.setTime(new Date());
// 往后延长一年 = 授权一年时间
expiryCalendar.add(Calendar.YEAR,1);
param.setExpiryTime(expiryCalendar.getTime());
// 用户类型
param.setConsumerType("user");
// 用户数量
param.setConsumerAmount(1);
// 描述
param.setDescription("测试");
LicenseCreator licenseCreator = new LicenseCreator(param);
// 生成license
licenseCreator.generateLicense();
}
结果
六、代码实现 - 证书安装和校验
就像证书生成,验证也需要一个专门的类
LicenseVerify.java
import cn.genm.license.dto.CustomKeyStoreParam;
import cn.genm.license.dto.CustomLicenseManager;
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校验类
*/
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;
}
}
}
我们之前说了,除了项目许多的配置文件我们一般是需要放在服务器单独的路径下的,除了公钥和私钥库,还有我们验证需要配置的一些参数
application.yml
#License相关配置
license:
subject: licenseTest #主体 - 注意主体要与生成证书的主体一致一致,不然验证通过不了
publicAlias: publicCert #公钥别称
storePass: 123456q #访问公钥的密码
licensePath: E:\license2\license.lic #license位置
publicKeysStorePath: E:\license\publicCerts.store #公钥位置
这些参数代码获取如下
LicenseConfig.java
import cn.genm.license.server.LicenseVerify;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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);
}
}
以上代码是读取yml的配置,以及将LicenseConfig加入Spring容器,在加入Spring容器的同时,执行licenseVerify里的安装方法
这样,程序就会在启动时,自动安装证书,校验时就可以用了
至于这个安装方法,我会单独加一篇博客整理阐述Spring里的那些能在服务启动时执行自定义操作的方法
七、测试 - 证书的安装和校验
最后的最后,验证
Tips: 记得测试的方法要创建启动类,因为licenseConfig要加入Spring容器,如果没有
LicenseApplication.java
/**
* @author By genmer
* @version V1.0.0
* @date 2021/07/9 13:35
*/
@SpringBootApplication
public class LicenseApplication {
public static void main(String[] args) {
SpringApplication.run(LicenseApplication.class, args);
}
}
校验代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class LicenseVerifyTest {
private LicenseVerify licenseVerify;
@Autowired
public void setLicenseVerify(LicenseVerify licenseVerify) {
this.licenseVerify = licenseVerify;
}
@Test
public void licenseVerify() {
System.out.println("licese是否有效:" + licenseVerify.verify());
}
}
结果
八、执行代码期间遇到的问题
七月 08, 2021 4:01:39 下午 java.util.prefs.WindowsPreferences <init> WARNING: Could not open/create prefs root node Software\JavaSoft\Prefs at root 0x80000002. Windows RegCreateKeyEx(...) returned error code 5. Exception in thread "main" de.schlichtherle.
注册表中找不到prefs
2
解决方法:win+R 输入 regedit
找到这个路径,新建项HKEY_LOCAL_MACHINE\Software\JavaSoft
2. 密码策略要6位置数字与字母
Exception in thread "main" de.schlichtherle.license.IllegalPasswordException: The password does not match the default policy: At least six characters consisting of letters and digits!
certificate is not yet valid
注意properties里设置的时间要合理就好了,比如我今天7.8 ,我发布和有效都是7.25,有效期时间可能是这个问题的一个产生原因,一开始我认为还是密码为题,一直找
很明显,找不到这个文件,写带盘符的绝对路径就好了
Exception in thread "main" java.io.FileNotFoundException: /privateKeys.store
也很明显,不要放在中文路径下 。
Exception in thread "main" java.io.FileNotFoundException: E:\license许å¯æµ‹è¯•/privateKeys.store
我在执行参考资料1的时候遇到的,虽然网上说只是个读取到结尾的标志,是正常的,可以我遇到了,并且证书不生成了,不太确定是代码问题还是异常引起的生成识别,还没解决
Exception in thread "main" java.io.EOFException
很明显,就是Subject参数在测试用例的时候的值和yml配置不一样,这样其实就算是验证失败的一种情况。
de.schlichtherle.license.LicenseContentException: exc.invalidSubject