软件产品license的简单实现java
作者:mmseoamin日期:2023-12-18

目录

  • 软件License简介
  • License控制内容
  • 实现方案
  • 代码示例讲解
  • 注意事项
  • 源码

    软件License简介

    我们在使用一些需要购买版权的软件产品时,或者我们做的商业软件需要进行售卖,为了收取费用,一般需要一个软件使用许可证,然后输入这个许可到软件里就能够使用软件。简单的是一串序列码或者一个许可证文件,复杂的是一个定制化插件包。于是有的小伙伴就开始好奇这个许可是怎么实现的,特别是在离线情况下它是怎么给软件授权,同时又能避免被破解的。

    License控制内容

    本文主要介绍的是许可证形式的授权。

    1. 如何控制只在指定服务器上使用

    如果不控制指定设备,那么下发了许可证,只要把软件复制多份安装则可到处使用,不利于版权维护,每个设备都有唯一标识:mac地址,ip地址,主板序列号等,在许可证中指定唯一标识则只能指定设备使用。

    2. 如何控制软件使用期限

    为了版权可持续性收益,对软件使用设置期限,到期续费等,则需要在许可证中配置使用起止日期。

    3. 如何控制按收费等级分模块使用功能

    售卖的软件产品可能进行分级收费,不同的收费标准可使用的软件功能范围不同。许可证中配置可使用的功能模块节点,前提是软件产品在设计开发时,需要有对应的模块结构代码。

    实现方案

    一、流程设计

    • 形式:许可证以文件形式下发,放在服务器指定位置
    • 内容:以上控制内容以dom节点形式放在文件中
    • 流程:将控制项加密后写入license文件节点,部署到客户机器,客户机使用时再读取license文件内容与客户机实际参数进行匹配校验
    • 工具:

      私用工具:包含生成秘钥对和通过秘钥生成加密内容

      客户工具:包含获取客户机唯一标识内容、校验license内容、守护线程获取及存储结果

    • 时序图

      软件产品license的简单实现java,license使用时序图,第1张

      二、文件防破解

    • 防止篡改:文件内容加密,使用AES加密,但是AES加密解密都是使用同一个key;使用非对称公私钥(本文使用的RSA)对内容加密解密,但是对内容长度有限制;综合方案,将AES的key(内部定义)用RSA加密,公钥放在加密工具中,内部持有,私钥放在解密工具中,引入软件产品解密使用。
    • 防止修改系统时间绕过许可证使用时间:许可证带上发布时间戳,并定时修改运行时间记录到文件,如果系统时间小于这个时间戳,就算大于许可证限制的起始时间也无法使用
    • 提高破解难度:懂技术的可以将代码反编译过来修改代码文件直接绕过校验,所以需要进行代码混淆,有测试过xjar的混淆效果比较好。

      实现流程,直接上图!!!

      软件产品license的简单实现java,license实现流程,第2张

      代码示例讲解

      私用工具端:

      RSA秘钥对生成

      package com.license.tools.licensecreate.utils;
      import java.security.KeyPair;
      import java.security.KeyPairGenerator;
      import java.security.NoSuchAlgorithmException;
      import java.security.interfaces.RSAPrivateKey;
      import java.security.interfaces.RSAPublicKey;
      import java.util.Base64;
      /**
       * @Description 生成公钥私钥对
       * @createDate 2022/05/05
       * @createTime 14:25
       */
      public class KeyGenerator {
          /**
           * 私钥
           */
          private static byte[] privateKey;
          /**
           * 公钥
           */
          private static byte[] publicKey;
          /**
           * 加密算法
           */
          private static final String KEY_ALGORITHM = "RSA";
          public void generater() {
              try {
                  KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
                  keyPairGenerator.initialize(1024);
                  KeyPair keyPair = keyPairGenerator.genKeyPair();
                  RSAPublicKey pubKey = (RSAPublicKey) keyPair.getPublic();
                  RSAPrivateKey priKey = (RSAPrivateKey) keyPair.getPrivate();
                  privateKey = Base64.getEncoder().encode(priKey.getEncoded());
                  publicKey = Base64.getEncoder().encode(pubKey.getEncoded());
                  System.out.println("公钥:" + new String(publicKey));
                  System.out.println("私钥:" + new String(privateKey));
              } catch (NoSuchAlgorithmException e) {
                  e.printStackTrace();
                  System.out.println("生成密钥对失败!");
              }
          }
          public static void main(String[] args) {
              KeyGenerator keyGenerator = new KeyGenerator();
              keyGenerator.generater();
          }
      }
      

      运行main方法,生成的秘钥对填入RSAUtils的puk和prk,公钥用于加密,私钥用于解密

      package com.license.tools.licensecreate.utils;
      import javax.crypto.BadPaddingException;
      import javax.crypto.Cipher;
      import javax.crypto.IllegalBlockSizeException;
      import javax.crypto.NoSuchPaddingException;
      import java.security.*;
      import java.security.spec.InvalidKeySpecException;
      import java.security.spec.PKCS8EncodedKeySpec;
      import java.security.spec.X509EncodedKeySpec;
      import java.util.Base64;
      /**
       * @Description 对AES密码加密
       * @createDate 2022/05/05
       * @createTime 14:28
       */
      public class RSAUtils {
          /**
           * 公钥base64
           */
          private static String puk = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdfujgTmG4aOa4oK2VysmKvAI+hurN/wuKQjzgJTo3ct6TH5NHFHncb9KXijC1xk2Po+pJ8UjU4XGjU4gq5yhTdeSYPYR6hj5jqLy8fkWpFzeC6RvM4bLDe1lDNKphpcUoo5ZO7T77w9fX2lgJSyy/8LxdBThc4Megga3KW1/W4wIDAQAB";
          /**
           * 加密
           *
           * @return
           * @throws Exception
           */
          protected static String encrypt(String content) throws Exception {
              byte[] publicKeyBytes = puk.getBytes();
              X509EncodedKeySpec x = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyBytes));
              KeyFactory keyFactory = KeyFactory.getInstance("RSA");
              PublicKey pubKey = keyFactory.generatePublic(x);
              Cipher cipher = Cipher.getInstance("RSA");
              cipher.init(Cipher.ENCRYPT_MODE, pubKey);
              byte[] result = cipher.doFinal(content.getBytes("UTF-8"));
              return Base64.getEncoder().encodeToString(result);
          }
          /**
           * 私钥base64
           */
          private static String prk = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ1+6OBOYbho5rigrZXKyYq8Aj6G6s3/C4pCPOAlOjdy3pMfk0cUedxv0peKMLXGTY+j6knxSNThcaNTiCrnKFN15Jg9hHqGPmOovLx+RakXN4LpG8zhssN7WUM0qmGlxSijlk7tPvvD19faWAlLLL/wvF0FOFzgx6CBrcpbX9bjAgMBAAECgYA8uRWohg//PdLXFHxY6JrUNrDW0sXtLoyQfgFimnfbsRpHt0DdgvOJHkQf0VP+gbqdyyEl6TWfflyGEErL39wX1rrosy+LpiN0HeISERJuwJtuiGeR+0qw+Xz2M7VE+e5oD94dRtlzERft2mcDbQAQYUCFNgUBtd1dCJgMJPZJYQJBANHxKKHqMbsH91JsGP8eCu+yeMah0X8cT79nwD71SJRc03W5P1MPKhRyGWJj0M+Wax32pAPCMTfbj19scLplJpUCQQDADD5OuSLYRVqx68/CYbFVK3ye/YD4Cgc+0kT9SoI9bLB10JumHT0seDGeXQqwUPAF3bBZGI8pW2bdtzDj8YGXAkABQXgEv+ncPIf2Lj9YB035cQ/X4E/oerrfYjd8KOtuN7/sDFecn5KY3LXaKM6u7y9k1nzUqOyycNXCtFtYQhKhAkBvgyxyvaFz/uFoyko6zksP705Pa1eFrx0B50pT4P26+O+FmXmnfPbWaXw2PkREmNqmLVGGinImS4JxXzuuP79FAkAFQejjE+5Twi8oSCcNwse7FFP86U6jgcc+S+XCUUkLXQ5SPlkyb037hwoV1lEEJpcyI2tSFRxBKT89KZN0Nfat";
          protected static String decrypt(String signEncrypt) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
              byte[] privateKeyBytes = prk.getBytes();
              PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBytes));
              KeyFactory keyFactory = KeyFactory.getInstance("RSA");
              PrivateKey priKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
              Cipher cipher = Cipher.getInstance("RSA");
              cipher.init(Cipher.DECRYPT_MODE, priKey);
              byte[] result = cipher.doFinal(Base64.getDecoder().decode(signEncrypt));
              return new String(result);
          }
          public static void main(String[] args) throws Exception {
              String password = "123456";
              String a = encrypt(password);
              System.out.println("AES加密秘钥:" + a);
          }
      }
      

      main方法输入AES需要的明文(示例:123456),用RSA加密成密文,做为AESUtils的aesKey

      package com.license.tools.licensecreate.utils;
      import org.springframework.util.Base64Utils;
      import javax.crypto.KeyGenerator;
      import javax.crypto.*;
      import javax.crypto.spec.SecretKeySpec;
      import java.security.InvalidKeyException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.spec.InvalidKeySpecException;
      /**
       * @version V1.0
       * @desc AES 加密工具类
       */
      public class AESUtils {
          /**
           * RSA加密后的AES秘钥
           */
          private static String aesEncyptPwd="ZIkun+KvXFWLZLYUwXqFWazQeRe119AkcGcl+p8Erzi4EEaHBFYcQuGuKthIE+1IWSQxoUpUJkT0T1+xtoRi3txDnBikdrFhccGZdRpqwRv58q5nqxJX4wVrq0Ms02KBKgQRTqqlzfYLzQcYPyhv8KPE8JDVkttic+W+j5pFles=";
          private static final String KEY_ALGORITHM = "AES";
          private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
          /**
           * AES 加密操作
           *
           * @param content  待加密内容
           * @return 返回Base64转码后的加密数据
           */
          public static String encrypt(String content) {
              try {
                  Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
                  byte[] byteContent = content.getBytes("utf-8");
                  cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesEncyptPwd)));
                  byte[] result = cipher.doFinal(byteContent);
                  return Base64Utils.encodeToString(result);
              } catch (Exception ex) {
                  ex.printStackTrace();
                  System.out.println("AES加密失败");
              }
              return null;
          }
          /**
           * AES 解密操作
           *
           * @param content  已加密内容
           * @return
           */
          public static String decrypt(String content) {
              try {
                  //实例化
                  Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
                  //使用密钥初始化,设置为解密模式
                  cipher.init(Cipher.DECRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesEncyptPwd)));
                  //执行操作
                  byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));
                  return new String(result, "utf-8");
              } catch (Exception ex) {
                  ex.printStackTrace();
                  System.out.println("AES解密失败");
              }
              return null;
          }
          /**
           * 生成加密秘钥
           *
           * @return
           */
          private static SecretKeySpec getSecretKey(String aesKey) {
              //返回生成指定算法密钥生成器的 KeyGenerator 对象
              KeyGenerator kg = null;
              try {
                  kg = KeyGenerator.getInstance(KEY_ALGORITHM);
                  //AES 要求密钥长度为 128
                  SecureRandom random=SecureRandom.getInstance("SHA1PRNG","SUN");
                  random.setSeed(aesKey.getBytes());
                  kg.init(128, random);
                  //生成一个密钥
                  SecretKey secretKey = kg.generateKey();
                  return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
              } catch (NoSuchAlgorithmException ex) {
                  ex.printStackTrace();
                  System.out.println("生成加密秘钥失败");
              } catch (NoSuchProviderException e) {
                  e.printStackTrace();
                  System.out.println("生成加密秘钥失败");
              }
              return null;
          }
      }
      

      生成license文件的方法

      package com.license.tools.licensecreate.test;
      import com.license.tools.licensecreate.utils.AESUtils;
      import com.license.tools.licensecreate.utils.DateUtils;
      import org.dom4j.Document;
      import org.dom4j.DocumentHelper;
      import org.dom4j.Element;
      import org.dom4j.io.OutputFormat;
      import org.dom4j.io.XMLWriter;
      import java.io.File;
      import java.io.FileWriter;
      import java.util.Scanner;
      /**
       * @Description 生成签名
       * @createDate 2022/05/05
       * @createTime 17:41
       */
      public class CreateSign {
          public static void main(String[] args) {
              Scanner sc = new Scanner(System.in);
              //系统标识---由mac地址+cpu序列号,在客户工具端获取客户机mac地址和cpu序列号后用AES加密得到
              System.out.println("请输入系统标识串(部署的服务获取):");
              String systemSign = sc.nextLine();
              System.out.println("请输入生效起始时间(格式如:2022-05-05 00:00:00):");
              String generatedTimeStr = sc.nextLine();
              System.out.println("请输入生效截止时间(格式如:2022-05-05 00:00:00):");
              String expiredTimeStr = sc.nextLine();
              System.out.println("请输入上一次校验时间初始值(格式如:2022-05-05 00:00:00):");
              String lastValidateTimeStr = sc.nextLine();
              System.out.println("请输入项目部署唯一版本号(不能带“-”):");
              String version = sc.nextLine();
              System.out.println("请输入license文件生成路径:");
              String path = sc.nextLine();
              createLicense(systemSign, generatedTimeStr, expiredTimeStr, lastValidateTimeStr, version, path);
              System.out.println("license文件生成成功,文件路径:" + path);
          }
          private static void createLicense(String systemSign, String generatedTimeStr, String expiredTimeStr, String lastValidateTimeStr, String version, String path) {
              try {
                  //解密系统标识得到mac地址+cpu序列号
                  String macAndCpu = AESUtils.decrypt(systemSign);
                  System.out.println("客户服务器mac地址和cpu序列号:" + macAndCpu);
                  //MAC地址-CPU序列号-生效起始时间-生效结束结束时间-软件产品序列号(项目版本唯一标识)
      //            String content = "A8:A1:59:41:89:36-BFEBFBFF000906EA-20220506-20220507-dmoiji3xkoa4p33";
                  StringBuilder signBuilder = new StringBuilder(macAndCpu);
                  //生效起始时间
                  long generatedTime = DateUtils.getTimeInMillis(generatedTimeStr);
                  //生效截止时间
                  long expiredTime = DateUtils.getTimeInMillis(expiredTimeStr);
                  //项目唯一标识
                  signBuilder.append("-").append(generatedTime).append("-").append(expiredTime).append("-").append(version);
                  String sign = AESUtils.encrypt(signBuilder.toString());
                  System.out.println("AES加密生成签名:");
                  System.out.println("-----------------------------------------------------------------------------------------------");
                  System.out.println(sign);
                  System.out.println("-----------------------------------------------------------------------------------------------");
                  //生成licence文件
                  Document document = DocumentHelper.createDocument();
                  //根节点
                  Element rootEle = document.addElement("license");
                  //功能数据节点,扩展参数时可在此节点下扩展
                  Element dataEle = rootEle.addElement("features");
                  Element featureEle = dataEle.addElement("feature");
                  featureEle.addAttribute("name", "lastValidateTi");
                  featureEle.addAttribute("ti", AESUtils.encrypt(String.valueOf(DateUtils.getTimeInMillis(lastValidateTimeStr))));
                  //签名节点
                  Element signEle = rootEle.addElement("signature");
                  signEle.setText(sign);
                  System.out.println(document.asXML());
                  OutputFormat format = OutputFormat.createPrettyPrint();
                  // 设置编码格式
                  format.setEncoding("UTF-8");
                  FileWriter fileWriter = new FileWriter(new File(path));
                  XMLWriter xmlWriter = new XMLWriter(fileWriter, format);
                  // 设置是否转义,默认使用转义字符
                  xmlWriter.setEscapeText(false);
                  xmlWriter.write(document);
                  xmlWriter.close();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      

      其中第一步的系统标识由下面的客户工具获取客户机的mac地址和cpu序列号而来,然后将此方法生成的license文件和软件包同路径部署(也可修改客户工具中指定路径)

      客户工具端:

      只有解密方法的RSAUtils

      package com.dtranx.tools.license.utils;
      import javax.crypto.BadPaddingException;
      import javax.crypto.Cipher;
      import javax.crypto.IllegalBlockSizeException;
      import javax.crypto.NoSuchPaddingException;
      import java.security.InvalidKeyException;
      import java.security.KeyFactory;
      import java.security.NoSuchAlgorithmException;
      import java.security.PrivateKey;
      import java.security.spec.InvalidKeySpecException;
      import java.security.spec.PKCS8EncodedKeySpec;
      import java.util.Base64;
      /**
       * @author penghao
       * @Description 对AES密码加密
       * @createDate 2022/05/05
       * @createTime 14:28
       */
      public class RSAUtils {
          /**
           * 私钥base64
           */
          private static String prk = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAJ1+6OBOYbho5rigrZXKyYq8Aj6G6s3/C4pCPOAlOjdy3pMfk0cUedxv0peKMLXGTY+j6knxSNThcaNTiCrnKFN15Jg9hHqGPmOovLx+RakXN4LpG8zhssN7WUM0qmGlxSijlk7tPvvD19faWAlLLL/wvF0FOFzgx6CBrcpbX9bjAgMBAAECgYA8uRWohg//PdLXFHxY6JrUNrDW0sXtLoyQfgFimnfbsRpHt0DdgvOJHkQf0VP+gbqdyyEl6TWfflyGEErL39wX1rrosy+LpiN0HeISERJuwJtuiGeR+0qw+Xz2M7VE+e5oD94dRtlzERft2mcDbQAQYUCFNgUBtd1dCJgMJPZJYQJBANHxKKHqMbsH91JsGP8eCu+yeMah0X8cT79nwD71SJRc03W5P1MPKhRyGWJj0M+Wax32pAPCMTfbj19scLplJpUCQQDADD5OuSLYRVqx68/CYbFVK3ye/YD4Cgc+0kT9SoI9bLB10JumHT0seDGeXQqwUPAF3bBZGI8pW2bdtzDj8YGXAkABQXgEv+ncPIf2Lj9YB035cQ/X4E/oerrfYjd8KOtuN7/sDFecn5KY3LXaKM6u7y9k1nzUqOyycNXCtFtYQhKhAkBvgyxyvaFz/uFoyko6zksP705Pa1eFrx0B50pT4P26+O+FmXmnfPbWaXw2PkREmNqmLVGGinImS4JxXzuuP79FAkAFQejjE+5Twi8oSCcNwse7FFP86U6jgcc+S+XCUUkLXQ5SPlkyb037hwoV1lEEJpcyI2tSFRxBKT89KZN0Nfat";
          protected static String decrypt(String signEncrypt) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
              byte[] privateKeyBytes = prk.getBytes();
              PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyBytes));
              KeyFactory keyFactory = KeyFactory.getInstance("RSA");
              PrivateKey priKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
              Cipher cipher = Cipher.getInstance("RSA");
              cipher.init(Cipher.DECRYPT_MODE, priKey);
              byte[] result = cipher.doFinal(Base64.getDecoder().decode(signEncrypt));
              return new String(result);
          }
      }
      

      AESUtils

      package com.dtranx.tools.license.utils;
      import org.springframework.util.Base64Utils;
      import javax.crypto.Cipher;
      import javax.crypto.KeyGenerator;
      import javax.crypto.SecretKey;
      import javax.crypto.spec.SecretKeySpec;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      /**
       * @version V1.0
       * @desc AES 加密工具类
       */
      public class AESUtils {
          /**
           * RSA加密后的AES秘钥
           */
          private static String aesKey="ZIkun+KvXFWLZLYUwXqFWazQeRe119AkcGcl+p8Erzi4EEaHBFYcQuGuKthIE+1IWSQxoUpUJkT0T1+xtoRi3t" +
                  "xDnBikdrFhccGZdRpqwRv58q5nqxJX4wVrq0Ms02KBKgQRTqqlzfYLzQcYPyhv8KPE8JDVkttic+W+j5pFles=";
          private static final String KEY_ALGORITHM = "AES";
          private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
          /**
           * AES 加密操作
           *
           * @param content  待加密内容
           * @return 返回Base64转码后的加密数据
           */
          protected static String encrypt(String content) {
              try {
                  Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
                  byte[] byteContent = content.getBytes("utf-8");
                  cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesKey)));
                  byte[] result = cipher.doFinal(byteContent);
                  return Base64Utils.encodeToString(result);
              } catch (Exception ex) {
                  ex.printStackTrace();
                  System.out.println("AES加密失败");
              }
              return null;
          }
          /**
           * AES 解密操作
           *
           * @param content  已加密内容
           * @return
           */
          protected static String decrypt(String content) {
              try {
                  //实例化
                  Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
                  //使用密钥初始化,设置为解密模式
                  cipher.init(Cipher.DECRYPT_MODE, getSecretKey(RSAUtils.decrypt(aesKey)));
                  //执行操作
                  byte[] result = cipher.doFinal(Base64Utils.decodeFromString(content));
                  return new String(result, "utf-8");
              } catch (Exception ex) {
                  ex.printStackTrace();
                  System.out.println("AES解密失败");
              }
              return null;
          }
          /**
           * 生成加密秘钥
           *
           * @return
           */
          private static SecretKeySpec getSecretKey(String aesKey) {
              //返回生成指定算法密钥生成器的 KeyGenerator 对象
              KeyGenerator kg = null;
              try {
                  kg = KeyGenerator.getInstance(KEY_ALGORITHM);
                  //AES 要求密钥长度为 128
                  SecureRandom random=SecureRandom.getInstance("SHA1PRNG","SUN");
                  random.setSeed(aesKey.getBytes());
                  kg.init(128, random);
                  //生成一个密钥
                  SecretKey secretKey = kg.generateKey();
                  return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);
              } catch (NoSuchAlgorithmException ex) {
                  ex.printStackTrace();
                  System.out.println("生成加密秘钥失败");
              } catch (NoSuchProviderException e) {
                  e.printStackTrace();
                  System.out.println("生成加密秘钥失败");
              }
              return null;
          }
      }
      

      获取系统mac地址和cpu序列号的SystemUtils

      package com.dtranx.tools.license.utils;
      import lombok.extern.slf4j.Slf4j;
      import java.io.BufferedReader;
      import java.io.IOException;
      import java.io.InputStreamReader;
      import java.net.InetAddress;
      import java.net.InterfaceAddress;
      import java.net.NetworkInterface;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Scanner;
      import java.util.stream.Collectors;
      /**
       * @author penghao
       */
      @Slf4j
      public class Systemutils {
          protected static String getMacAddress() {
              try {
                  java.util.Enumeration en = NetworkInterface.getNetworkInterfaces();
                  StringBuilder sb = new StringBuilder();
                  while (en.hasMoreElements()) {
                      NetworkInterface iface = en.nextElement();
                      List addrs = iface.getInterfaceAddresses();
                      for (InterfaceAddress addr : addrs) {
                          InetAddress ip = addr.getAddress();
                          NetworkInterface network = NetworkInterface.getByInetAddress(ip);
                          if (network == null) {
                              continue;
                          }
                          if (network.getName().toLowerCase().startsWith("ens")) {
                              byte[] mac = network.getHardwareAddress();
                              if (mac == null) {
                                  continue;
                              }
                              for (int i = 0; i < mac.length; i++) {
                                  sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));
                              }
                              String xxy = sb.toString().replaceAll("-", "").toUpperCase();
                              log.info("xxy地址:{}", xxy);
                              return xxy;
                          }
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
                  log.error("读取本机系统信息失败!");
              }
              return null;
          }
          protected static String getCpuNum() {
              BufferedReader reader = null;
              InputStreamReader ir = null;
              try {
                  String[] linux = {"/bin/bash", "-c", "dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
                  String[] windows = {"wmic", "cpu", "get", "ProcessorId"};
                  // 获取系统信息
                  String property = System.getProperty("os.name");
                  Process process = Runtime.getRuntime().exec(property.contains("Window") ? windows : linux);
                  process.getOutputStream().close();
                  ir = new InputStreamReader(process.getInputStream());
                  reader = new BufferedReader(ir);
                  String xxw = reader.readLine();
                  if (xxw != null) {
                      xxw = xxw.replaceAll(" ", "");
                  }
                  log.info("xxw识别码:{}", xxw);
                  return xxw;
              } catch (Exception e) {
                  e.printStackTrace();
                  log.error("获取系统信息失败!");
              } finally {
                  if (reader != null) {
                      try {
                          reader.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
                  if (ir != null) {
                      try {
                          ir.close();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }
              return null;
          }
          public static void main(String[] args) throws Exception {
      //        List macs = getMacAddress();
      //        System.out.println("本机的mac网卡的地址列表" + macs);
              System.out.println(getCpuNum());
          }
      }
      

      校验方法

      package com.dtranx.tools.license.utils;
      import com.dtranx.tools.license.bean.CheckParams;
      import com.dtranx.tools.license.bean.ValidateCodeEnum;
      import com.dtranx.tools.license.bean.ValidateResult;
      import lombok.extern.slf4j.Slf4j;
      import org.dom4j.Document;
      import org.dom4j.Element;
      import org.dom4j.io.OutputFormat;
      import org.dom4j.io.SAXReader;
      import org.dom4j.io.XMLWriter;
      import org.springframework.stereotype.Component;
      import java.io.File;
      import java.io.FileWriter;
      import java.util.HashMap;
      import java.util.List;
      import java.util.Map;
      /**
       * @author PH
       */
      @Slf4j
      @Component
      public class LicenseManager {
          public static Map validate() {
              Map result = new HashMap();
              CheckParams checkParams = null;
              try {
                  checkParams = getCheckParams(result);
                  if (checkParams == null) {
                      return result;
                  }
              } catch (Exception e) {
                  e.printStackTrace();
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.EXCEPTION));
                  return result;
              }
              //校验mac地址
              if (!checkParams.getMacAddress().equals(Systemutils.getMacAddress())) {
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.UNAUTHORIZED));
                  return result;
              }
              //校验cpu序列号
              if (!checkParams.getCpuSerial().equals(Systemutils.getCpuNum())) {
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.UNAUTHORIZED));
                  return result;
              }
              long currentTi = System.currentTimeMillis();
              //校验时间
              if (notAfterLastValidateTime(checkParams.getLastValidateTime(), currentTi) || notAfter(checkParams.getGeneratedTime(), currentTi)
                      || notBefore(checkParams.getExpiredTime(), currentTi)) {
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.EXPIRED));
                  return result;
              }
              result.put("Authorize", ValidateResult.ok());
              return result;
          }
          public static String getSystemSign() {
              String MacAddress = Systemutils.getMacAddress();
              String cpuNum = Systemutils.getCpuNum();
              return AESUtils.encrypt(MacAddress + "-" + cpuNum);
          }
          public static void updateSign(String sign) {
              try {
                  Document document = readLicense();
                  Element rootElement = document.getRootElement();
                  Element signatureEle = rootElement.element("signature");
                  signatureEle.setText(sign);
                  OutputFormat format = OutputFormat.createPrettyPrint();
                  // 设置编码格式
                  format.setEncoding("UTF-8");
                  String path = System.getProperty("user.dir");
                  FileWriter fileWriter = new FileWriter(new File(path + File.separator + "license.xml"));
                  XMLWriter xmlWriter = new XMLWriter(fileWriter, format);
                  // 设置是否转义,默认使用转义字符
                  xmlWriter.setEscapeText(false);
                  xmlWriter.write(document);
                  xmlWriter.close();
                  log.info("更新授权码成功");
              } catch (Exception e) {
                  e.printStackTrace();
                  throw new RuntimeException("更新授权码失败!");
              }
          }
          private static boolean notAfterLastValidateTime(long lastValidateTime, long currentTi) {
              return lastValidateTime >= currentTi;
          }
          private static boolean notBefore(Long expiredTime, long currentTi) {
              return expiredTime <= currentTi;
          }
          private static boolean notAfter(long generatedTime, long currentTi) {
              return generatedTime >= currentTi;
          }
          private static CheckParams getCheckParams(Map result) {
              //读取license文件
              Document document = readLicense();
              if (document == null) {
                  log.error("license 读取失败!");
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.FILE_NOT_EXIST));
                  return null;
              }
              Element rootElement = document.getRootElement();
              Element dataEle = rootElement.element("features");
              List featuresEles = dataEle.elements();
              Element lastValidateTimeEle = featuresEles.get(0);
              //提取上一次验证时间
              String lastValidateTimeStr = lastValidateTimeEle.attributeValue("ti");
              long lastValidateTime = Long.parseLong(AESUtils.decrypt(lastValidateTimeStr));
              log.debug("上一次校验时间:{}", lastValidateTime);
              //提取签名内容
              Element signEle = rootElement.element("signature");
              String signStr = signEle.getText();
              String sign = AESUtils.decrypt(signStr);
              if (sign == null) {
                  log.error("授权码不正确");
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.ILLEGAL));
                  return null;
              }
              log.debug("签名内容:{}", sign);
              String[] signArr = sign.split("-");
              if (signArr.length != 5) {
                  log.error("授权码不正确");
                  result.put("Authorize", ValidateResult.error(ValidateCodeEnum.ILLEGAL));
                  return null;
              }
              CheckParams params = CheckParams.builder().lastValidateTime(lastValidateTime).macAddress(signArr[0])
                      .cpuSerial(signArr[1]).generatedTime(Long.parseLong(signArr[2])).expiredTime(Long.parseLong(signArr[3]))
                      .version(signArr[4]).build();
              return params;
          }
          private static Document readLicense() {
              Document document = null;
              try {
                  SAXReader saxReader = new SAXReader();
                  String path = System.getProperty("user.dir");
                  document = saxReader.read(new File(path + File.separator + "license.xml"));
                  return document;
              } catch (Exception e) {
                  e.printStackTrace();
              }
              return null;
          }
          public static void main(String[] args) {
      //        String sign = AESUtils.decrypt("VorZodH/B6eeNLPA09TNJ8fpjlvrsckBk3VW3Pvr2qzhQVdeL38xS8unNFFxzQrjZ70f4wIoi1Tg1wlZq9DFKuVyp2zD20A//lDswyaD8NsmwMR72R2Ua+Gb0dp+PpM3b9gx2iIFIAtKOyaJlMMV8H4az/EKc/d733lyHfY3wbhsmo4vUvsqPYiriaj+psPu7DgO0DsQqw0xjAblpcrfL1xc42E3STEi9NTNbbBTsLU=");
              String s="HPdW5CR3bRzVGEDMkZtsfQMHbcJ6SabTLJqdNsvJ7aU=";
              System.out.println(AESUtils.decrypt(s));
          }
      }
      

      守护线程,定时获取校验结果,校验时间间隔从配置文件读取

      package com.dtranx.tools.license.utils;
      import com.dtranx.tools.license.bean.ValidateResult;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.stereotype.Component;
      import java.util.Map;
      import java.util.concurrent.TimeUnit;
      /**
       * @author penghao
       * @createDate 2022/05/11
       * @createTime 16:42
       */
      @Component
      @Slf4j
      public class LicenseThread implements Runnable {
          public static Map validateResult = null;
          @Value("${xxy.checkTime}")
          private Long checkTime;
          @Bean
          public void startThread() {
              Thread thread = new Thread(this);
              thread.setDaemon(true);
              thread.start();
          }
          public void run() {
              while (true) {
                  validateResult = LicenseManager.validate();
                  if (validateResult != null) {
                      ValidateResult result = validateResult.get("Authorize");
                      log.debug("license校验结果:" + result.getMessage());
                  }
                  try {
                      //正式改为12个小时校验一次,保持与登录同步即可
      //                TimeUnit.HOURS.sleep(12);
                      //测试1分钟校验一次
                      TimeUnit.SECONDS.sleep(checkTime);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
          public static boolean validateAfterUpdateSign() {
              validateResult = LicenseManager.validate();
              ValidateResult result = validateResult.get("Authorize");
              return result != null && result.getIsValidate();
          }
      }
      

      注意事项

      1、获取cpu序列号时,实际是通过执行命令“dmidecode -t processor | grep ‘ID’ | awk -F ‘:’ ‘{print $2}’ | head -n 1”获取,在docker中运行服务,如果找不到dmidecode 命令,需要绑定硬件信息配置到容器内

      docker 挂载目录增加

      • /dev/mem:/dev/mem
      • /sbin/dmidecode:/sbin/dmidecode
      • /usr/sbin/dmidecode:/usr/sbin/dmidecode

        2、docker网络使用非宿主机网络时,docker内的MAC地址会随着docker的重启改变,导致之前生成的授权码校验不通过。处理措施有以下几种:

        (1)docker容器内使用宿主机的网络“–net=host --privileged=true ”,则mac地址一直跟随宿主机

        (2)docker启动命令添加指定mac地址“ --mac-address=xx:xx:xx:xx:xx:xx”

        源码

        附完整源码地址(包含使用说明):github