基于 TrueLicense 的项目证书验证
一、简述
开发的软件产品在交付使用的时候,往往有一段时间的试用期,这期间我们不希望自己的代码被客户二次拷贝,这个时候 license 就派上用场了,license 的功能包括设定有效期、绑定 ip、绑定 mac 等。授权方直接生成一个 license 给使用方使用,如果需要延长试用期,也只需要重新生成一份 license 即可,无需手动修改源代码。
truelicense 是一个开源的证书管理引擎,详细介绍见
首先介绍下 license 授权机制的原理:
- 生成密钥对,包含私钥和公钥。
- 授权者保留私钥,使用私钥对授权信息诸如使用截止日期,mac 地址等内容生成 license 签名证书。
- 公钥给使用者,放在代码中使用,用于验证 license 签名证书是否符合使用条件。
二、生成密钥对
以下命令在 window cmd 命令窗口执行,注意当前执行目录,最后生成的密钥对即在该目录下:
1、首先要用 keytool 工具来生成私匙库:(-alias别名 -validity 3650 表示10年有效)
keytool -genkey -alias privatekey -keysize 1024 -keystore privatekeys.store -validity 3650
2、然后把私匙库内的证书导出到一个文件当中
keytool -export -alias privatekey -file certfile.cer -keystore privatekeys.store
3、然后再把这个证书文件导入到公匙库
keytool -import -alias publiccert -file certfile.cer -keystore publiccerts.store
最后生成的文件 privatekeys.store(私钥)、publiccerts.store(公钥)拷贝出来备用。
三、准备工作
首先,我们需要引入 truelicense 的 jar 包,用于实现我们的证书管理。
<dependency> <groupid>de.schlichtherle.truelicense</groupid> <artifactid>truelicense-core</artifactid> <version>1.33</version> </dependency>
然后,我们建立一个单例模式下的证书管理器。
public class licensemanagerholder { private static volatile licensemanager licensemanager = null; private licensemanagerholder() { } public static licensemanager getlicensemanager(licenseparam param) { if (licensemanager == null) { synchronized (licensemanagerholder.class) { if (licensemanager == null) { licensemanager = new licensemanager(param); } } } return licensemanager; } }
四、利用私钥生成证书
利用私钥生成证书,我们需要两部分内容,一部分是私钥的配置信息(私钥的配置信息在生成私钥库的过程中获得),一部分是自定义的项目证书信息。如下展示:
########## 私钥的配置信息 ########### # 私钥的别名 private.key.alias=privatekey # privatekeypwd(该密码是生成密钥对的密码 — 需要妥善保管,不能让使用者知道) private.key.pwd=123456 # keystorepwd(该密码是访问密钥库的密码 — 使用 keytool 生成密钥对时设置,使用者知道该密码) key.store.pwd=123456 # 项目的唯一识别码 subject=demo # 密钥库的地址(放在 resource 目录下) pripath=/privatekeys.store ########## license content ########### # 发布日期 issuedtime=2019-09-12 # 有效开始日期 notbefore=2019-09-12 # 有效截止日期 notafter=2019-12-30 # ip 地址 ipaddress=192.168.31.25 # mac 地址 macaddress=5c-c5-d4-3e-ca-a6 # 使用者类型,用户(user)、电脑(computer)、其他(else) consumertype=user # 证书允许使用的消费者数量 consumeramount=1 # 证书说明 info=power by xiamen yungu #生成证书的地址 licpath=d:\\license.lic
接下来,就是如何生成证书的实操部分了
@slf4j public class createlicense { /** * x500princal 是一个证书文件的固有格式,详见api */ private final static x500principal default_holderand_issuer = new x500principal("cn=duke, ou=javasoft, o=sun microsystems, c=us"); private string prialias; private string privatekeypwd; private string keystorepwd; private string subject; private string pripath; private string issued; private string notbefore; private string notafter; private string ipaddress; private string macaddress; private string consumertype; private int consumeramount; private string info; private string licpath; /** * 构造器,参数初始化 * * @param confpath 参数配置文件路径 */ public createlicense(string confpath) { // 获取参数 properties prop = new properties(); try (inputstream in = getclass().getresourceasstream(confpath)) { prop.load(in); } catch (ioexception e) { log.error("createlicense properties load inputstream error.", e); } //common param prialias = prop.getproperty("private.key.alias"); privatekeypwd = prop.getproperty("private.key.pwd"); keystorepwd = prop.getproperty("key.store.pwd"); subject = prop.getproperty("subject"); pripath = prop.getproperty("pripath"); // license content issued = prop.getproperty("issuedtime"); notbefore = prop.getproperty("notbefore"); notafter = prop.getproperty("notafter"); ipaddress = prop.getproperty("ipaddress"); macaddress = prop.getproperty("macaddress"); consumertype = prop.getproperty("consumertype"); consumeramount = integer.valueof(prop.getproperty("consumeramount")); info = prop.getproperty("info"); licpath = prop.getproperty("licpath"); } /** * 生成证书,在证书发布者端执行 * * @throws exception */ public void create() throws exception { licensemanager licensemanager = licensemanagerholder.getlicensemanager(initlicenseparams()); licensemanager.store(buildlicensecontent(), new file(licpath)); log.info("------ 证书发布成功 ------"); } /** * 初始化证书的相关参数 * * @return */ private licenseparam initlicenseparams() { class<createlicense> clazz = createlicense.class; preferences preferences = preferences.usernodeforpackage(clazz); // 设置对证书内容加密的对称密码 cipherparam cipherparam = new defaultcipherparam(keystorepwd); // 参数 1,2 从哪个class.getresource()获得密钥库; // 参数 3 密钥库的别名; // 参数 4 密钥库存储密码; // 参数 5 密钥库密码 keystoreparam privatestoreparam = new defaultkeystoreparam(clazz, pripath, prialias, keystorepwd, privatekeypwd); // 返回生成证书时需要的参数 return new defaultlicenseparam(subject, preferences, privatestoreparam, cipherparam); } /** * 通过外部配置文件构建证书的的相关信息 * * @return * @throws parseexception */ public licensecontent buildlicensecontent() throws parseexception { licensecontent content = new licensecontent(); simpledateformat formate = new simpledateformat("yyyy-mm-dd"); content.setconsumeramount(consumeramount); content.setconsumertype(consumertype); content.setholder(default_holderand_issuer); content.setissuer(default_holderand_issuer); content.setissued(formate.parse(issued)); content.setnotbefore(formate.parse(notbefore)); content.setnotafter(formate.parse(notafter)); content.setinfo(info); // 扩展字段 map<string, string> map = new hashmap<>(4); map.put("ip", ipaddress); map.put("mac", macaddress); content.setextra(map); return content; } }
最后,来尝试生成一份证书吧!
public static void main(string[] args) throws exception { createlicense clicense = new createlicense("/licensecreateparam.properties"); clicense.create(); }
四、利用公钥验证证书
利用公钥生成证书,我们需要有公钥库、license 证书等信息。
########## 公钥的配置信息 ########### # 公钥别名 public.alias=publiccert # 该密码是访问密钥库的密码 — 使用 keytool 生成密钥对时设置,使用者知道该密码 key.store.pwd=123456 # 项目的唯一识别码 — 和私钥的 subject 保持一致 subject = yungu # 证书路径(我这边配置在了 linux 根路径下,即 /license.lic ) license.dir=/license.lic # 公共库路径(放在 resource 目录下) public.store.path=/publiccerts.store
接下来就是怎么用公钥验证 license 证书,怎样验证 ip、mac 地址等信息的过程了~
@slf4j public class verifylicense { private string pubalias; private string keystorepwd; private string subject; private string licdir; private string pubpath; public verifylicense() { // 取默认配置 setconf("/licenseverifyparam.properties"); } public verifylicense(string confpath) { setconf(confpath); } /** * 通过外部配置文件获取配置信息 * * @param confpath 配置文件路径 */ private void setconf(string confpath) { // 获取参数 properties prop = new properties(); inputstream in = getclass().getresourceasstream(confpath); try { prop.load(in); } catch (ioexception e) { log.error("verifylicense properties load inputstream error.", e); } this.subject = prop.getproperty("subject"); this.pubalias = prop.getproperty("public.alias"); this.keystorepwd = prop.getproperty("key.store.pwd"); this.licdir = prop.getproperty("license.dir"); this.pubpath = prop.getproperty("public.store.path"); } /** * 安装证书证书 */ public void install() { try { licensemanager licensemanager = getlicensemanager(); licensemanager.install(new file(licdir)); log.info("安装证书成功!"); } catch (exception e) { log.error("安装证书失败!", e); runtime.getruntime().halt(1); } } private licensemanager getlicensemanager() { return licensemanagerholder.getlicensemanager(initlicenseparams()); } /** * 初始化证书的相关参数 */ private licenseparam initlicenseparams() { class<verifylicense> clazz = verifylicense.class; preferences pre = preferences.usernodeforpackage(clazz); cipherparam cipherparam = new defaultcipherparam(keystorepwd); keystoreparam pubstoreparam = new defaultkeystoreparam(clazz, pubpath, pubalias, keystorepwd, null); return new defaultlicenseparam(subject, pre, pubstoreparam, cipherparam); } /** * 验证证书的合法性 */ public boolean vertify() { try { licensemanager licensemanager = getlicensemanager(); licensecontent verify = licensemanager.verify(); log.info("验证证书成功!"); map<string, string> extra = (map) verify.getextra(); string ip = extra.get("ip"); inetaddress inetaddress = inetaddress.getlocalhost(); string localip = inetaddress.tostring().split("/")[1]; if (!objects.equals(ip, localip)) { log.error("ip 地址验证不通过"); return false; } string mac = extra.get("mac"); string localmac = getlocalmac(inetaddress); if (!objects.equals(mac, localmac)) { log.error("mac 地址验证不通过"); return false; } log.info("ip、mac地址验证通过"); return true; } catch (licensecontentexception ex) { log.error("证书已经过期!", ex); return false; } catch (exception e) { log.error("验证证书失败!", e); return false; } } /** * 得到本机 mac 地址 * * @param inetaddress * @throws socketexception */ private string getlocalmac(inetaddress inetaddress) throws socketexception { //获取网卡,获取地址 byte[] mac = networkinterface.getbyinetaddress(inetaddress).gethardwareaddress(); stringbuffer sb = new stringbuffer(); for (int i = 0; i < mac.length; i++) { if (i != 0) { sb.append("-"); } //字节转换为整数 int temp = mac[i] & 0xff; string str = integer.tohexstring(temp); if (str.length() == 1) { sb.append("0" + str); } else { sb.append(str); } } return sb.tostring().touppercase(); } }
有了公钥的验证过程了,等下!事情还没结束呢!我们需要在项目启动的时候,安装 licnese 证书,然后验证ip、mac 等信息。如果校验不通过,就阻止项目启动!
@component public class licensecheck { @postconstruct public void init() { verifylicense vlicense = new verifylicense(); vlicense.install(); if (!vlicense.vertify()) { runtime.getruntime().halt(1); } } }