我们在使用一些需要购买版权的软件产品时,或者我们做的商业软件需要进行售卖,为了收取费用,一般需要一个软件使用许可证,然后输入这个许可到软件里就能够使用软件。简单的是一串序列码或者一个许可证文件,复杂的是一个定制化插件包。于是有的小伙伴就开始好奇这个许可是怎么实现的,特别是在离线情况下它是怎么给软件授权,同时又能避免被破解的。
本文主要介绍的是许可证形式的授权。
1. 如何控制只在指定服务器上使用
如果不控制指定设备,那么下发了许可证,只要把软件复制多份安装则可到处使用,不利于版权维护,每个设备都有唯一标识:mac地址,ip地址,主板序列号等,在许可证中指定唯一标识则只能指定设备使用。
2. 如何控制软件使用期限
为了版权可持续性收益,对软件使用设置期限,到期续费等,则需要在许可证中配置使用起止日期。
3. 如何控制按收费等级分模块使用功能
售卖的软件产品可能进行分级收费,不同的收费标准可使用的软件功能范围不同。许可证中配置可使用的功能模块节点,前提是软件产品在设计开发时,需要有对应的模块结构代码。
一、流程设计
私用工具:包含生成秘钥对和通过秘钥生成加密内容
客户工具:包含获取客户机唯一标识内容、校验license内容、守护线程获取及存储结果
二、文件防破解
实现流程,直接上图!!!
私用工具端:
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.Enumerationen = 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 Mapvalidate() { 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 MapvalidateResult = 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 挂载目录增加
2、docker网络使用非宿主机网络时,docker内的MAC地址会随着docker的重启改变,导致之前生成的授权码校验不通过。处理措施有以下几种:
(1)docker容器内使用宿主机的网络“–net=host --privileged=true ”,则mac地址一直跟随宿主机
(2)docker启动命令添加指定mac地址“ --mac-address=xx:xx:xx:xx:xx:xx”
附完整源码地址(包含使用说明):github