欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于 TrueLicense 的项目证书验证

程序员文章站 2022-06-13 07:53:59
一、简述 开发的软件产品在交付使用的时候,往往有一段时间的试用期,这期间我们不希望自己的代码被客户二次拷贝,这个时候 license 就派上用场了,license 的功能包括设定有效期、绑定 ip、绑定 mac 等。授权方直接生成一个 license 给使用方使用,如果需要延长试用期,也只需要重新生 ......

一、简述

开发的软件产品在交付使用的时候,往往有一段时间的试用期,这期间我们不希望自己的代码被客户二次拷贝,这个时候 license 就派上用场了,license 的功能包括设定有效期、绑定 ip、绑定 mac 等。授权方直接生成一个 license 给使用方使用,如果需要延长试用期,也只需要重新生成一份 license 即可,无需手动修改源代码。

truelicense 是一个开源的证书管理引擎,详细介绍见

首先介绍下 license 授权机制的原理:

  1. 生成密钥对,包含私钥和公钥。
  2. 授权者保留私钥,使用私钥对授权信息诸如使用截止日期,mac 地址等内容生成 license 签名证书。
  3. 公钥给使用者,放在代码中使用,用于验证 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);
        }
    }
}