License的概念
License(许可证)是一种用来控制软件产品使用权限的许可文件。在商业软件中,License通常用于限制软件的试用期、功能使用、并发用户数等,以便开发者能够收取相应的费用。下面我们将通过Java实现一个简单的License生成和验证系统。
License商用逻辑
确定自己需要校验的属性,用来标识License使用方即表示谁用了你的软件、平台或者是库。这个标识可以是machine也可以是people,一经生成,便不能修改和删除。例如根据Mac地址、主板序列号、CPU序列号、IP等等,用来确定授权对象唯一。
我们可以在属性中定义时间、授权次数等限制超过定义的时间或者授权则无法继续使用。
我们在写代码之前需要制作一对密钥,私钥对授权内容进行签名,公钥给授权费校验License文件是否正确有效
秘钥制作
当前我们使用JDK自带的KeyTool工具进行制作
逐行执行下面命令
keytool:Java提供的用于管理密钥库和证书的工具。
-genkey:生成一个新密钥对。
-alias privatekeys:给生成的密钥对设置一个别名为privatekeys。
-keyalg DSA:使用DSA算法生成密钥对。
-keysize 1024:密钥长度为1024位。
-keystore privateKeys.store:将生成的密钥对保存在名为privateKeys.store的密钥库文件中。
-validity 3650:指定密钥的有效期为3650天,即10年。
命令行:keytool -genkey -alias privatekeys -keyalg DSA -keysize 1024 -keystore privateKeys.store -validity 3650
-export:导出指定别名的证书或公钥。
-alias privatekeys:指定要导出的证书或公钥的别名为privatekeys。
-file certfile.cer:指定要导出的证书的输出文件为certfile.cer。
-keystore privateKeys.store:指定要使用的密钥库文件为privateKeys.store。
命令行:keytool -export -alias privatekeys -file certfile.cer -keystore privateKeys.store
-import:导入一个证书或公钥。
-alias publiccert:指定导入的证书或公钥的别名为publiccert。
-file certfile.cer:指定要导入的证书文件为certfile.cer。
-keystore publicCerts.store:指定要保存导入证书或公钥的密钥库文件为publicCerts.store。
命令行:keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store
最后生成的文件privateKeys.store(私钥)和publicCerts.store(公钥)拷贝出来备用。
License运行流程
License申请流程
代码描述
下面代码中只有核心代码,如需详细查看,请直接点开源文件,有详细注释。
分为3个模块 core模块、creator模块和verify模块
core:为核心模块、creator:为创建License模块、verify:为校验模块
使用过程:
将私钥放入创建License模块的资源文件夹内、将公钥放入License校验文件夹内。
运行ServerInfo可以获得本机代码的激活码 加密后=“”。
将本机代码的激活码和license开始时间及结束时间,当作参数传入creater模块的creater类中。生成license
可以使用verify中的verifyMain检验License。
我们配合我上篇文章中的Java代码混淆和加密再配合上License即可完美实现闭环。
代码书写
首先引入关于License的Jar包
<dependency>
<groupId>de.schlichtherle.truelicense</groupId>
<artifactId>truelicense-core</artifactId>
<version>1.33</version>
</dependency>
定义证书相关类
public class LicenseCreatorParam implements Serializable {
private static final long serialVersionUID = -7793154252684580872L;
/**
* 证书主题
*/
private String subject;
/**
* 私钥别名
*/
private String privateAlias;
/**
* 私钥密码(需要妥善保管,不能让使用者知道
*/
private String keyPass;
/**
* 私钥库存储路径
*/
private String privateKeysStorePath;
/**
* 访问私钥库的密码
*/
private String storePass;
/**
* 证书生成路径
*/
private String licensePath;
/**
* 证书生效时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date issuedTime = new Date();
/**
* 证书失效时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date expiryTime;
/**
* 用户类型
*/
private String consumerType = "user";
/**
* 用户数量
*/
private Integer consumerAmount = 1;
/**
* 描述信息
*/
private String description = "";
/**
* 额外的服务器硬件校验信息(或者其他的信息都可以放)
*/
private LicenseExtraParam licenseCheck;
/**
* 证书下载地址 == 一旦证书create成功,这个值就会填充上
*/
private String licUrl;
}
License初始化和生成
public class ParamInitHelper {
/**
* 证书的发行者和主体字段信息
*/
private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=a, OU=a, O=a, L=a, ST=a, C=a");
/**
* <p>初始化证书生成参数</p>
*
* @param param GxLicenseCreatorParam 生成证书参数
* @return LicenseParam 证书参数
*/
public static LicenseParam initLicenseParam(LicenseCreatorParam param) {
Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
/** 设置对证书内容加密的秘钥 */
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
KeyStoreParam privateStoreParam = new DefaultKeyStoreParam(LicenseCreator.class
, param.getPrivateKeysStorePath()
, param.getPrivateAlias()
, param.getStorePass()
, param.getKeyPass());
return new DefaultLicenseParam(param.getSubject(), preferences, privateStoreParam, cipherParam);
}
/**
* <p>初始化证书内容信息对象</p>
*
* @param param GxLicenseCreatorParam 生成证书参数
* @return LicenseContent 证书内容
*/
public static LicenseContent initLicenseContent(LicenseCreatorParam param) {
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());
/** 设置证书扩展信息(对象 -- 额外的ip、mac、cpu等信息) */
licenseContent.setExtra(param.getLicenseCheck());
return licenseContent;
}
/**
* <p>初始化证书生成参数</p>
*
* @param param License校验类需要的参数
*/
public static LicenseParam initLicenseParam(LicenseVerifyParam param) {
Preferences preferences = Preferences.userNodeForPackage(LicenseVerifyManager.class);
CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
KeyStoreParam publicStoreParam = new DefaultKeyStoreParam(LicenseVerifyManager.class
/** 公钥库存储路径 */
, param.getPublicKeysStorePath()
/** 公匙别名 */
, param.getPublicAlias()
/** 公钥库访问密码 */
, param.getStorePass()
, null);
return new DefaultLicenseParam(param.getSubject(), preferences, publicStoreParam, cipherParam);
}
}
校验即将生成的License
/**
* @Author: WangYao
* @description: 自定义LicenseManager,用于增加额外的服务器硬件信息校验
* @date: 2024-05-08 11:16
*/
public class LicenseCustomManager extends LicenseManager {
/**
* XML编码
*/
private static final String XML_CHARSET = "UTF-8";
/**
* 默认BUFF_SIZE
*/
private static final int DEFAULT_BUFF_SIZE = 8 * 1024;
public LicenseCustomManager() {
}
public LicenseCustomManager(LicenseParam param) {
super(param);
}
/**
* <p>重写LicenseManager的create方法</p>
*
* @param content LicenseContent 证书信息
* @param notary notary 公正信息
* @return byte[]
* @throws Exception 默认异常
*/
@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);
}
/**
* <p>重写install方法</p>
*
* @param key 密匙
* @param notary 公正信息
* @return LicenseContent 证书信息
* @throws Exception 默认异常
*/
@Override
protected synchronized LicenseContent install(final byte[] key, final LicenseNotary notary) throws Exception {
final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
notary.verify(certificate);
final LicenseContent licenseContent = (LicenseContent) this.load(certificate.getEncoded());
/** 增加额外的自己的license校验方法,校验ip、mac、cpu序列号等 */
this.validate(licenseContent);
setLicenseKey(key);
setCertificate(certificate);
return licenseContent;
}
/**
* <p>重写verify方法</p>
*
* @param notary 公正信息
* @return LicenseContent 证书信息
* @throws Exception 默认异常
*/
@Override
protected synchronized LicenseContent verify(final LicenseNotary notary) throws Exception {
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());
/** 增加额外的自己的license校验方法,校验ip、mac、cpu序列号等 */
this.validate(content);
setCertificate(certificate);
return content;
}
/**
* <p>校验生成证书的参数信息</p>
*
* @param content LicenseContent 证书内容
* @throws LicenseContentException 证书内容错误异常
*/
protected synchronized void validateCreate(final LicenseContent content) throws LicenseContentException {
// 当前时间
final Date now = new Date();
// 生效时间
final Date notBefore = content.getNotBefore();
// 失效时间
final Date notAfter = content.getNotAfter();
if (null != notAfter && now.after(notAfter)) {
String message = "证书失效时间不能早于当前时间";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
if (null != notBefore && null != notAfter && notAfter.before(notBefore)) {
String message = "证书生效时间不能晚于证书失效时间";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
final String consumerType = content.getConsumerType();
if (null == consumerType) {
String message = "用户类型不能为空";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
}
/**
* <p>重写validate方法,增加ip地址、mac地址、cpu序列号等其他信息的校验</p>
*
* @param content LicenseContent 证书内容
* @throws LicenseContentException 证书内容错误异常
*/
@Override
protected synchronized void validate(final LicenseContent content) throws LicenseContentException {
// 当前时间
final Date now = new Date();
final Date notAfter = content.getNotAfter();
if (now.after(notAfter)) {
throw new LicenseContentException("系统证书过期,当前时间已超过证书有效期 -- " +
DateUtils.date2Str(content.getNotAfter()) + "");
}
// 1、 首先调用父类的validate方法
super.validate(content);
// 2、 然后校验自定义的License参数 License中可被允许的参数信息
LicenseExtraParam expectedCheck = (LicenseExtraParam) content.getExtra();
// 当前服务器真实的参数信息
LicenseExtraParam serverCheckModel = AServerInfos.getServer(null).getServerInfos();
if (expectedCheck != null && serverCheckModel != null) {
// 校验IP地址
if (expectedCheck.isIpCheck() && !checkIpAddress(expectedCheck.getIpAddress(), serverCheckModel.getIpAddress())) {
String message = "系统证书无效,当前服务器的IP没在授权范围内";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
// 校验Mac地址
if (expectedCheck.isMacCheck() && !checkIpAddress(expectedCheck.getMacAddress(), serverCheckModel.getMacAddress())) {
String message = "系统证书无效,当前服务器的Mac地址没在授权范围内";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
// 校验主板序列号
if (expectedCheck.isBoardCheck() && !checkSerial(expectedCheck.getMainBoardSerial(), serverCheckModel.getMainBoardSerial())) {
String message = "系统证书无效,当前服务器的主板序列号没在授权范围内";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
// 校验CPU序列号
if (expectedCheck.isCpuCheck() && !checkSerial(expectedCheck.getCpuSerial(), serverCheckModel.getCpuSerial())) {
String message = "系统证书无效,当前服务器的CPU序列号没在授权范围内";
System.out.println("message = " + message);
throw new LicenseContentException(message);
}
} else {
System.out.println("不能获取服务器硬件信息");
throw new LicenseContentException("不能获取服务器硬件信息");
}
}
/**
* <p>重写XMLDecoder解析XML</p>
*/
private Object load(String encoded) {
BufferedInputStream inputStream = null;
XMLDecoder decoder = null;
try {
inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));
decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFF_SIZE), 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) {
System.out.println("XMLDecoder解析XML失败 ");
e.printStackTrace();
}
}
return null;
}
/**
* 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/>
* 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
*/
private boolean checkIpAddress(List<String> expectedList, List<String> serverList) {
/** 如果期望的IP列表空直接返回false,因为既然验证ip,这一项必须要有元素 */
if (CommonUtils.isEmpty(expectedList)) {
return false;
}
/** 如果当前服务器的IP列表空直接返回false,因为服务器不可能获取不到ip,没有的话验证个锤子 */
if (CommonUtils.isEmpty(serverList)) {
return false;
}
for (String expected : expectedList) {
if (serverList.contains(expected.trim())) {
return true;
}
}
return false;
}
/**
* <p>校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内</p>
*
* @param expectedSerial 主板信息
* @param serverSerial 服务器信息
* @return boolean
*/
private boolean checkSerial(String expectedSerial, String serverSerial) {
if (CommonUtils.isNotEmpty(expectedSerial)) {
if (CommonUtils.isNotEmpty(serverSerial)) {
return expectedSerial.equals(serverSerial);
}
return false;
} else {
return true;
}
}
}
获取windos硬件信息
public class LinuxServerInfos extends AServerInfos {
private final String[] CPU_SHELL = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
private final String[] MAIN_BOARD_SHELL = {"/bin/bash", "-c", "dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
@Override
protected String getCPUSerial() throws Exception {
String result = "";
String CPU_ID_CMD = "dmidecode";
BufferedReader bufferedReader = null;
Process p = null;
try {
p = Runtime.getRuntime().exec(new String[]{"sh", "-c", CPU_ID_CMD});// 管道
bufferedReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
int index = -1;
while ((line = bufferedReader.readLine()) != null) {
// 寻找标示字符串[hwaddr]
index = line.toLowerCase().indexOf("uuid");
if (index >= 0) {// 找到了
// 取出mac地址并去除2边空格
result = line.substring(index + "uuid".length() + 1).trim();
break;
}
}
} catch (IOException e) {
System.out.println("获取cpu硬件信息失败 " + e);
}
return result.trim();
// return GxServerSerialHelper.getLinuxSerial(CPU_SHELL);
}
@Override
protected String getMainBoardSerial() throws Exception {
String result = "";
String maniBord_cmd = "dmidecode | grep 'Serial Number' | awk '{print $3}' | tail -1";
Process p;
try {
p = Runtime.getRuntime().exec(new String[]{"sh", "-c", maniBord_cmd});// 管道
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
result += line;
break;
}
br.close();
} catch (IOException e) {
System.out.println("获取主板信息错误" + e);
}
return result;
// return GxServerSerialHelper.getLinuxSerial(MAIN_BOARD_SHELL);
}
}
安装并且校验License
package com.appleyk.core.model;
import com.appleyk.core.helper.ParamInitHelper;
import com.appleyk.core.utils.DateUtils;
import de.schlichtherle.license.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
/**
* <p>License校验类</p>
*/
public class LicenseVerifyManager {
/**
* <p>安装License证书</p>
*
* @param param License校验类需要的参数
*/
public synchronized LicenseResult install(LicenseVerifyParam param) {
try {
/** 1、初始化License证书参数 */
LicenseParam licenseParam = ParamInitHelper.initLicenseParam(param);
/** 2、创建License证书管理器对象 */
// LicenseManager licenseManager =new LicenseManager(licenseParam);
// 走自定义的Lic管理
LicenseCustomManager licenseManager = new LicenseCustomManager(licenseParam);
/** 3、获取要安装的证书文件 */
File licenseFile = new File(param.getLicensePath());
/** 4、如果之前安装过证书,先卸载之前的证书 == 给null */
licenseManager.uninstall();
/** 5、开始安装 */
LicenseContent content = licenseManager.install(licenseFile);
String message = MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",
DateUtils.date2Str(content.getNotBefore()), DateUtils.date2Str(content.getNotAfter()));
System.out.println("message = " + message);
return new LicenseResult(message, content);
} catch (LicenseContentException contentExc) {
String message = contentExc.getMessage();
System.out.println("message = " + message);
return new LicenseResult(false, message, contentExc);
} catch (Exception e) {
e.printStackTrace();
return new LicenseResult(false, e.getMessage(), e);
}
}
/**
* <p>校验License证书</p>
*
* @param param License校验类需要的参数
*/
public LicenseResult verify(LicenseVerifyParam param) {
/** 1、初始化License证书参数 */
LicenseParam licenseParam = ParamInitHelper.initLicenseParam(param);
/** 2、创建License证书管理器对象 */
LicenseManager licenseManager = new LicenseCustomManager(licenseParam);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/** 3、开始校验证书 */
try {
/**这里要判断下lic文件是不是被用户恶意删除了*/
String licensePath = param.getLicensePath();
if (licensePath == null || licensePath == "") {
String msg = "license.lic路径未指定,验证不通过!";
System.out.println("msg = " + msg);
return new LicenseResult(false, msg, new Exception(msg));
}
/**下面两个检测如果文件不存在会抛异常,然后会被捕获到*/
if (licensePath.contains("classpath:")) {
/**检测下当前应用的classes路径下有没有lic文件*/
// ResourceUtils.getFile(licensePath);
} else {
/**直接构建file对象检测lic文件是否存在*/
new File(licensePath);
}
LicenseContent licenseContent = licenseManager.verify();
String message = MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",
format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter()));
System.out.println("message = " + message);
return new LicenseResult(message, licenseContent);
} catch (NoLicenseInstalledException ex) {
String message = "证书未安装!";
System.out.println("message = " + message);
return new LicenseResult(false, message, ex);
} catch (LicenseContentException cex) {
cex.printStackTrace();
return new LicenseResult(false, cex.getMessage(), cex);
} catch (FileNotFoundException fnfe) {
String msg = String.format("license.lic文件(%s)不存在,验证失败!", param.getLicensePath());
System.out.println("msg = " + msg);
return new LicenseResult(false, msg, fnfe);
} catch (Exception e) {
String message = "证书校验失败!";
System.out.println("message = " + message);
return new LicenseResult(false, message, e);
}
}
}
源码文件路径
gitee:https://gitee.com/a1374469523/license.git