SpringBoot项目License证书生成与验证(TrueLicense) 【记录】

SpringBoot项目License证书生成与验证(TrueLicense) 【记录】

在非开源产品、商业软件、收费软件等系统的使用上,需要考虑系统的使用版权问题,不能随便一个人拿去在任何环境都能用。应用部署一般分为两种情况:

  • 应用部署在开发者自己的云服务器上,这种情况下用户通过账号扽了给的形式远程访问。只需要在账号登录时,校验目标账号的有效期、访问权限等信息。
  • 应用部署在客户的内网环境中,开发者没有办法控制客户的网络环境以及不能保证应用所在服务器能够访问外网(不能走网上认证)。这种情况下通常的做法就是使用服务器许可文件。在应用启动的时候加载证书,然后在登录或者其它关键操作的地方校验证书的有效性。

一、License介绍

TrueLicense 是一个开源的证书管理引擎。采用非对称加密方式对 License源数据 进行预处理,防止伪造License。
软件许可(License)证书可以在软件产品交付的时候,对其使用时间以及使用范围进行授权。当用户申请(购买)改变使用时间使用范围的时候,授权方可以为用户生成一个新的license替换原来的license即可,从而避免了修改源码、改动部署等繁琐操作。

  • 授权机制原理
  1. 生成密钥对,使用Keytool生成公私钥证书库
  2. 授权者保留私钥,使用私钥对包含授权的信息(使用截止日期、MAC地址/机器码、模块数据 等)的license证书进行数字签名。
  3. 公钥给使用者(放在验证的代码中使用),用于验证license是否符合使用条件。
  • License 运行流程:

二、KeyTool生成密钥对

使用 JDK 中的 KeyTool 工具生成密钥对。
在cmd.exe中运行生成密钥对的命令:

  • 生成私钥库:

keytool -genkeypair -keysize 1024 -validity 3650 -alias “privateKey” -keystore “privateKeys.keystore” -storepass “public_password1234” -keypass “private_password1234” -dname “CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN”

参数释义
alias私钥别名
validity私钥的有效时间天数
keystore指定密钥库文件的名称
storepass指定密钥库密码(获取keystore信息锁需要的密码)
keypass指定别名条目对应的密码(私钥密码)

在当前目录下,生成一个名为privateKeys.keystore的密钥库,同时指定密钥库密码为public_password1234,第一个条目(秘钥)为privateKey,指定条目密码为private_password1234。

  • 将私钥库内的公钥导出到文件中

keytool -exportcert -alias “privateKey” -keystore “privateKeys.keystore” -storepass “public_password1234” -file “certfile.cer”

参数释义
alias私钥别名
validity私钥的有效时间天数
keystore指定密钥库文件的名称
storepass指定密钥库密码(获取keystore信息锁需要的密码)
keypass指定别名条目对应的密码(私钥密码)
file证书名称

将“privateKey”秘钥的公钥(即主体信息,包括公钥,不包括私钥,可公开)导出到名称为certfile.cer文件中!

  • 将该证书文件导入到公钥库中

keytool -import -alias “publicCert” -file “certfile.cer” -keystore “publicCerts.keystore” -storepass “public_password1234”

将上一步导出的certfile.cer文件中的公钥导入到公钥库!

image.png

在执行完以上命令后就会在当前文件夹下生成3个文件:privateKeys.keystore、publicCerts.keystore、certfile.cer
image.png

  • certfile.cer是暂存文件,删除即可。
  • privateKeys.keystore :服务端用来为用户生成License文件
  • publicCerts.keystore :随客户端项目部署到客户服务端,用其解密License文件并校验其许可信息。

三、springboot整合TrueLicense

改步骤的目的是,在服务端生成授权文件 License!
image.png

3.1 构建基础模型数据

  1. 引入TrueLisence
<!-- Licence证书生成依赖 -->
<dependency><groupId>de.schlichtherle.truelicense</groupId><artifactId>truelicense-core</artifactId><version>1.33</version>
</dependency>
  1. 创建License证书自定义校验数据模型类
package com.zdsf.u8cloudmanagementproject.core.license;import lombok.Data;import java.io.Serializable;
import java.util.List;/*** @ClassName : LicenseCheckModel* @Description : 自定义需要校验的参数* @Author : AD*/@Data
public class LicenseCheckModel implements Serializable {private static final long serialVersionUID = -2314678441082223148L;/*** 可被允许IP地址白名单* */private List<String>  ipAddress;/*** 可被允许的MAC地址白名单(网络设备接口的物理地址,通常固化在网卡(Network Interface Card,NIC)的EEPROM(电可擦可编程只读存储器)中,具有全球唯一性。)* */private  List<String> macAddress;/*** 可允许的CPU序列号* */private String cpuSerial;/*** 可允许的主板序列号(硬件序列化?)* */private String mainBoardSerial;}
  1. 创建License证书标准校验参数模型类
package com.zdsf.u8cloudmanagementproject.core.license;import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;import java.io.Serializable;
import java.util.Date;/*** @ClassName : LicenseCreatorParam* @Description : Lisence证书生成类需要的参数* @Author : AD*/@Data
public class LicenseCreatorParam implements Serializable {private static final long serialVersionUID = 2832129012982731724L;/*** 证书subject* */private String subject;/*** 密钥级别* */private String privateAlias;/*** 密钥密码(需要妥善保存,密钥不能让使用者知道)*/private String keyPass;/*** 访问密钥库的密码* */private String storePass;/*** 证书生成路径* */private String licensePath;/*** 密钥库存储路径* */private String privateKeysStorePath;/*** 证书生效时间* */@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 LicenseCheckModel licenseCheckModel;
}

3.2 客户服务器数据获取

TrueLicense的 de.schlichtherle.license.LicenseManager 类自带的verify方法只校验了我们后面颁发的许可文件的生效和过期时间,然而在实际项目中我们可能需要额外校验应用部署的服务器的IP地址、MAC地址、CPU序列号、主板序列号等 机器码 信息,因此我们需要复写框架的部分方法以实现校验自定义参数的目的。
获取客户服务器基本信息,比如:IP、Mac地址、CPU序列号、主板序列号等!

  1. 创建获取客户端服务器相关机器码的抽象类

这里采用抽象类的原因是,客户端在Linux 和 windows 不同类型服务器上部署项目时,在获取相关机器码的方式上会有所差别,所以这里才有抽象类来封装,具体实现就交给下游的各系统类型的实现来分别实现各个方法!
注:这里使用了模板方法模式,将不变部分的算法封装到抽象类,而基本方法的具体实现则由子类来实现。

package com.zdsf.u8cloudmanagementproject.core.license;import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;/*** @ClassName : AbstractServerInfos* @Description : 获取用户 服务器硬件信息(机器码):为LicenseCheckModel服务提供硬件信息 (用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等)* @Author : AD*/
public abstract class AbstractServerInfos {private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);/*** Description: 组装需要额外校验的License参数** @param* @return com.zdsf.u8cloudmanagementproject.core.license.LicenseCheckModel*/public LicenseCheckModel getServerInfos(){LicenseCheckModel licenseCheckModel = new LicenseCheckModel();try {licenseCheckModel.setIpAddress(this.getIpAddress());licenseCheckModel.setMacAddress(this.getMacAddress());licenseCheckModel.setCpuSerial(this.getCpuSerial());licenseCheckModel.setMainBoardSerial(this.MainBoardSerial());}catch (Exception e){logger.error("获取服务器硬件信息失败", e);}return licenseCheckModel;}/*** Description:  获取IP地址信息** @param* @return java.util.List<java.lang.String>*/protected abstract List<String> getIpAddress() throws Exception;/*** Description: 获取Mac地址(网络设备接口的物理地址,通常固化在网卡(Network Interface Card,NIC)的EEPROM(电可擦可编程只读存储器)中,具有全球唯一性。)** @param* @return java.util.List<java.lang.String>*/protected abstract List<String> getMacAddress() throws Exception;/*** Description: 获取CPU序列号** @param* @return java.util.List<java.lang.String>*/protected abstract String getCpuSerial() throws Exception;/*** Description: 获取主板序列号** @param* @return java.lang.String*/protected abstract String MainBoardSerial() throws Exception;/*** Description: 获取当亲啊服务器上所用符合条件的InetAddress** @param* @return java.util.List<java.net.InetAddress>*/protected List<InetAddress> getLocalAllInetAddress() throws Exception{List<InetAddress> result  = new ArrayList<>();//遍历所用网络接口for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ){NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();// 在所用接口下再遍历IP地址for(Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements();){InetAddress inetAddr= (InetAddress) inetAddresses.nextElement();//排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/&& !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){result.add(inetAddr);}}}return result;}/*** Description: 获取某个网络接口的Mac地址** @param inetAddress* @return java.lang.String*/protected String getMacByInetAddress(InetAddress inetAddress) throws Exception{try {byte[] mac = NetworkInterface.getByInetAddress(inetAddress).getHardwareAddress();StringBuilder stringBuilder = new StringBuilder();for (int i = 0; i < mac.length; i++) {if (i != 0){stringBuilder.append("-");}//将十六进制byte转化为字符串String hexString = Integer.toHexString(mac[i] & 0xFF);if(hexString.length() == 1){stringBuilder.append("0" + hexString);}else {stringBuilder.append(hexString);}}return stringBuilder.toString().toUpperCase();}catch (Exception e){e.printStackTrace();}return null;}
}
  1. 客户端Linux类型服务器的相关机器码获取实现类
package com.zdsf.u8cloudmanagementproject.core.license;import com.baomidou.mybatisplus.core.toolkit.StringUtils;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;/*** @ClassName : LinuxServerInfos* @Description : Linux服务器相关机器码获取实现类* @Author : AD*/public class LinuxServerInfos extends AbstractServerInfos{@Overrideprotected List<String> getIpAddress() throws Exception {List<String> result = null;//获取所用网络接口List<InetAddress>  inetAddresses = getLocalAllInetAddress();if (inetAddresses != null && inetAddresses.size()>0){result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String :: toLowerCase).collect(Collectors.toList());}return result;}@Overrideprotected List<String> getMacAddress() throws Exception {List<String> result = null;//1.获取所用网络接口List<InetAddress> inetAddresses  = getLocalAllInetAddress();if (inetAddresses  != null && inetAddresses .size()>0){//2.获取所用网络接口的Mac地址// result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());List<String> list = new ArrayList<>();Set<String> uniqueValues = new HashSet<>();for (InetAddress inetAddress : inetAddresses) {String macByInetAddress = getMacByInetAddress(inetAddress);if (uniqueValues.add(macByInetAddress)) {list.add(macByInetAddress);}}result = list;return result;}return result;}@Overrideprotected String getCpuSerial() throws Exception {//序列号String serialNumber = null;//使用dmidecode命令获取CPU序列号String[] shell =  {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};Process process = Runtime.getRuntime().exec(shell);process.getOutputStream().close();BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line = reader.readLine().trim();if (StringUtils.isNotEmpty(line)){serialNumber = line;}reader.close();return serialNumber;}@Overrideprotected String MainBoardSerial() throws Exception {//序列号String serialNumber = null;//使用dmidecode命令获取主板序列号String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};Process process = Runtime.getRuntime().exec(shell);process.getOutputStream().close();BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line = reader.readLine().trim();if(StringUtils.isNotBlank(line)){serialNumber = line;}reader.close();return serialNumber;}
}
  1. 客户端Windows服务器相关机器码获取实现类
package com.zdsf.u8cloudmanagementproject.core.license;import java.net.InetAddress;
import java.util.*;
import java.util.stream.Collectors;/*** @ClassName : WindowsServerInfos* @Description : Windows客户端相关机器码获取实现类* @Author : AD*/
public class WindowsServerInfos extends AbstractServerInfos{@Overrideprotected List<String> getIpAddress() throws Exception {List<String> result = null;//获取所用网络接口List<InetAddress> inetAddresses = getLocalAllInetAddress();if (inetAddresses!= null && inetAddresses.size() > 0){result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());}return result;}@Overrideprotected List<String> getMacAddress() throws Exception {List<String> result = null;//1. 获取所有网络接口List<InetAddress> inetAddresses = getLocalAllInetAddress();if (inetAddresses  != null && inetAddresses .size()>0){//2.获取所用网络接口的Mac地址// result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());List<String> list = new ArrayList<>();Set<String> uniqueValues = new HashSet<>();for (InetAddress inetAddress : inetAddresses) {String macByInetAddress = getMacByInetAddress(inetAddress);if (uniqueValues.add(macByInetAddress)) {list.add(macByInetAddress);}}result = list;return result;}return result;}@Overrideprotected String getCpuSerial() throws Exception {//序列号String serialNumber  = "";//使用WMIC获取CPU序列号Process process = Runtime.getRuntime().exec("wmic cpu get processorid");process.getOutputStream().close();Scanner scanner = new Scanner(process.getInputStream());if(scanner.hasNext()){scanner.next();}if(scanner.hasNext()){serialNumber = scanner.next().trim();}scanner.close();return serialNumber;}@Overrideprotected String MainBoardSerial() throws Exception {//序列号String serialNumber = "";//使用WMIC获取主板序列号Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");process.getOutputStream().close();Scanner scanner = new Scanner(process.getInputStream());if(scanner.hasNext()){scanner.next();}if(scanner.hasNext()){serialNumber = scanner.next().trim();}scanner.close();return serialNumber;}
}

3.3 其它Custom定制类的创建

3.3.1 公私钥存储相关类定义

通过继承AbstractKeyStoreParam抽象类,重新其中的 getStream方法,达到将公钥、私钥存放到其它磁盘位置,而不是项目中!

package com.zdsf.u8cloudmanagementproject.core.license;import de.schlichtherle.license.AbstractKeyStoreParam;import java.io.*;/*** @ClassName : CustomKeyStoreParam* @Description : 自定义KeyStoreParam,用于将公私钥存储文件存放到其它磁盘位置,而不是存放在项目中* @Author : AD*/
public class CustomKeyStoreParam extends AbstractKeyStoreParam {/*** 公钥 / 私钥 在磁盘上的存储路径* */private String storePath;private String alias;private String storePwd;private String keyPwd;public CustomKeyStoreParam(Class aClass, String resource,String alias,String storePwd,String keyPwd) {super(aClass, resource);this.storePath = resource;this.alias = alias;this.storePwd = storePwd;this.keyPwd = keyPwd;}@Overridepublic String getAlias() {return alias;}@Overridepublic String getStorePwd() {return storePwd;}@Overridepublic String getKeyPwd() {return keyPwd;}@Overridepublic InputStream getStream() throws IOException {final InputStream in = new FileInputStream(new File(storePath));if (null == in){throw new FileNotFoundException(storePath);}return in;}
}

3.3.2 自定义License管理类

创建CustomLicenseManager 管理类,该类继承LicenseManager 类,用来增加我门额外信息的验证工作( TrueLicense默认只给我们验证了时间 )。所以这里需要根据自己的需求在validate()里面增加额外的验证项!
在父类 LicenseManager类中主要的几个方法如下:

  • create 创建证书

重写该方法,因为 LicenseManager类中默认的创建证书方法中,只含有的参数为:有效期、用户类型、用户数量等数据,但是不包含自己拓展的相关的数据(机器码)等。

  • install 安装证书
  • verify 验证证书

TrueLicense的 de.schlichtherle.license.LicenseManager 类自带的verify方法只校验了我们后面颁发的许可文件的生效和过期时间,然而在实际项目中我们可能需要额外校验应用部署的服务器的IP地址、MAC地址、CPU序列号、主板序列号等信息,因此我们需要复写框架的部分方法以实现校验自定义参数的目的。

  • uninstall 卸载证书
package com.zdsf.u8cloudmanagementproject.core.license;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import de.schlichtherle.license.*;
import de.schlichtherle.xml.GenericCertificate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;import static de.schlichtherle.xml.XMLConstants.DEFAULT_BUFSIZE;/*** @ClassName : CustomLicenseManager* @Description : 自定义LicenseManager类,用于增加额外的服务器机器码相关数据的校验* @Author : AD*/
public class CustomLicenseManager extends LicenseManager {private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);//XML编码private static final String XML_CHARSET = "UTF-8";//默认BUFSIZEprivate static final int DEFUAULT_BUFSIZE = 1024 * 4;public CustomLicenseManager(){}public CustomLicenseManager(LicenseParam param){super(param);}/*** 复写create方法* @param* @return byte[]*/@Overrideprotected synchronized byte[] create(LicenseContent content,LicenseNotary notary)throws Exception {initialize(content);this.validateCreate(content);final GenericCertificate certificate = notary.sign(content);return getPrivacyGuard().cert2key(certificate);}/*** 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息* @param* @return de.schlichtherle.license.LicenseContent*/@Overrideprotected synchronized LicenseContent install(final byte[] key,final LicenseNotary notary)throws Exception {final GenericCertificate certificate = getPrivacyGuard().key2cert(key);notary.verify(certificate);final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());this.validate(content);setLicenseKey(key);setCertificate(certificate);return content;}/*** 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息* @param* @return de.schlichtherle.license.LicenseContent*/@Overrideprotected synchronized LicenseContent verify(final LicenseNotary notary)throws Exception {GenericCertificate certificate = getCertificate();// Load license key from preferences,final byte[] key = getLicenseKey();if (null == key){throw new NoLicenseInstalledException(getLicenseParam().getSubject());}certificate = getPrivacyGuard().key2cert(key);notary.verify(certificate);final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());this.validate(content);setCertificate(certificate);return content;}/*** 校验生成证书的参数信息* @param content 证书正文*/protected synchronized void validateCreate(final LicenseContent content)throws LicenseContentException {final LicenseParam param = getLicenseParam();final Date now = new Date();final Date notBefore = content.getNotBefore();final Date notAfter = content.getNotAfter();if (null != notAfter && now.after(notAfter)){throw new LicenseContentException("证书失效时间不能早于当前时间");}if (null != notBefore && null != notAfter && notAfter.before(notBefore)){throw new LicenseContentException("证书生效时间不能晚于证书失效时间");}final String consumerType = content.getConsumerType();if (null == consumerType){throw new LicenseContentException("用户类型不能为空");}}/*** 复写validate方法,增加IP地址、Mac地址等其他信息校验* @param content LicenseContent*/@Overrideprotected synchronized void validate(final LicenseContent content)throws LicenseContentException {//1. 首先调用父类的validate方法super.validate(content);//2. 然后校验自定义的License参数//License中可被允许的参数信息LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();//当前服务器真实的参数信息LicenseCheckModel serverCheckModel = getServerInfos();if(expectedCheckModel != null && serverCheckModel != null){//校验IP地址if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){throw new LicenseContentException("当前服务器的IP没在授权范围内");}//校验Mac地址if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");}//校验主板序列号if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");}//校验CPU序列号if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");}}else{throw new LicenseContentException("不能获取服务器硬件信息");}}/*** 重写XMLDecoder解析XML* @param encoded XML类型字符串* @return java.lang.Object*/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_BUFSIZE),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) {logger.error("XMLDecoder解析XML失败",e);}}return null;}/*** 获取当前服务器需要额外校验的License参数* @return demo.LicenseCheckModel*/private LicenseCheckModel getServerInfos(){//操作系统类型String osName = System.getProperty("os.name").toLowerCase();AbstractServerInfos abstractServerInfos = null;//根据不同操作系统类型选择不同的数据获取方法if (osName.startsWith("windows")) {abstractServerInfos = new WindowsServerInfos();} else if (osName.startsWith("linux")) {abstractServerInfos = new LinuxServerInfos();}else{//其他服务器类型abstractServerInfos = new LinuxServerInfos();}return abstractServerInfos.getServerInfos();}/*** 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/>* 如果存在IP在可被允许的IP/Mac地址范围内,则返回true* @return boolean*/private boolean checkIpAddress(List<String> expectedList, List<String> serverList){if(expectedList != null && expectedList.size() > 0){if(serverList != null && serverList.size() > 0){for(String expected : expectedList){if(serverList.contains(expected.trim())){return true;}}}return false;}else {return true;}}/*** 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内* @return boolean*/private boolean checkSerial(String expectedSerial,String serverSerial){if(StringUtils.isNotBlank(expectedSerial)){if(StringUtils.isNotBlank(serverSerial)){if(expectedSerial.equals(serverSerial)){return true;}}return false;}else{return true;}}}

3.3.3 License证书生成类

该类的创建主要用于生成客户端持有的项目证书!
*安装证书的类要与服务端生成证书的类要在同一个包路径下,尤其是LicenseCheckModel类需要在同一个包路径下,防止XML反序列化失败。

package com.zdsf.u8cloudmanagementproject.core.license;import de.schlichtherle.license.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.text.MessageFormat;
import java.util.prefs.Preferences;/*** @ClassName : LicenseCreator* @Description : 生成License证书* @Author : AD*/
public class LicenseCreator {private static Logger logger = LogManager.getLogger(LicenseCreator.class);private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");private LicenseCreatorParam param;public LicenseCreator(LicenseCreatorParam param) {this.param = param;}/*** 生成License证书* @return boolean*/public boolean generateLicense(){try {LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());LicenseContent licenseContent = initLicenseContent();licenseManager.store(licenseContent,new File(param.getLicensePath()));return true;}catch (Exception e){logger.error(MessageFormat.format("证书生成失败:{0}",param),e);return false;}}/*** 初始化证书生成参数* @return de.schlichtherle.license.LicenseParam*/private LicenseParam initLicenseParam(){Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);//设置对证书内容加密的秘钥CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class,param.getPrivateKeysStorePath(),param.getPrivateAlias(),param.getStorePass(),param.getKeyPass());LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject(),preferences,privateStoreParam,cipherParam);return licenseParam;}/*** 设置证书生成正文信息* @return de.schlichtherle.license.LicenseContent*/private LicenseContent initLicenseContent(){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());//扩展校验服务器硬件信息licenseContent.setExtra(param.getLicenseCheckModel());return licenseContent;}
}

3.3.4 服务端控制层

这里需要在项目中的application.properties配置文件中确定一个证书生成路径@Value("${license.licensePath}")
image.png
同时还需要将之前通过 KeyTool生成的私钥复制到该目录下。
image.png

package com.zdsf.u8cloudmanagementproject.core.license.controller;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.zdsf.u8cloudmanagementproject.core.license.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;/*** @ClassName : LicenseCreatorController* @Description : License证书相关接口控制层* @Author : AD*/@RestController
@RequestMapping("/license")
public class LicenseCreatorController {/*** 证书生成路径*/@Value("${license.licensePath}")private String licensePath;/*** Description: 获取服务器硬件信息** @param osName 操作系统类型,为null则自动判断* @return com.zdsf.u8cloudmanagementproject.core.license.LicenseCheckModel*/@RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})public LicenseCheckModel getServerInfos(@RequestParam(value = "osName",required = false) String osName) {//操作系统类型if(StringUtils.isBlank(osName)){osName = System.getProperty("os.name");}osName = osName.toLowerCase();AbstractServerInfos abstractServerInfos = null;//根据不同操作系统类型选择不同的数据获取方法if (osName.startsWith("windows")) {abstractServerInfos = new WindowsServerInfos();} else if (osName.startsWith("linux")) {abstractServerInfos = new LinuxServerInfos();}else{//其他服务器类型abstractServerInfos = new LinuxServerInfos();}return abstractServerInfos.getServerInfos();}/*** Description: License证书生成接口** @param param 生成证书需要的参数* @return java.util.Map<java.lang.String,java.lang.Object>*/@RequestMapping(value = "/generateLicense",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})public Map<String,Object> generateLicense(@RequestBody(required = true) LicenseCreatorParam param) {Map<String,Object> resultMap = new HashMap<>(2);if(StringUtils.isBlank(param.getLicensePath())){param.setLicensePath(licensePath);}LicenseCreator licenseCreator = new LicenseCreator(param);boolean result = licenseCreator.generateLicense();if(result){resultMap.put("result","ok");resultMap.put("msg",param);}else{resultMap.put("result","error");resultMap.put("msg","证书文件生成失败!");}return resultMap;}
}
  • 获取部署服务器机器码数据接口测试

image.png

  • generateLicense生成证书方法示例参数:

{
“subject”: “license_demo”,
“privateAlias”: “privateKey”,
“keyPass”: “private_password1234”,
“storePass”: “public_password1234”,
“licensePath”: “E:/LicenseDemo/license.lic”,
“privateKeysStorePath”: “E:/LicenseDemo/privateKeys.keystore”,
“issuedTime”: “2022-04-26 14:48:12”,
“expiryTime”: “2022-08-22 00:00:00”,
“consumerType”: “User”,
“consumerAmount”: 1,
“description”: “这是证书描述信息”,
“licenseCheckModel”: {
“ipAddress”: [
“192.168.3.57”
],
“macAddress”: [
“D8-F2-CA-06-1A-F3”
],
“cpuSerial”: “BFEBFBFF000806EA”,
“mainBoardSerial”: “PM01I01911000743”
}
}

image.png
生成了License证书:
image.png

将 客户端 项目部署到客户服务器,通过以下接口获取服务器的硬件信息(等license文件生成后需要删除这个项目。当然也可以通过命令手动获取客户服务器的硬件信息,然后在开发者自己的电脑上生成license文件)。

  • License 申请流程

  • 找回License

四、客户端部署应用添加License校验

image.png
其中获取客户服务器的基本信息【AbstractServerInfos.class】,获取客户Linux服务器的基本信息【LinuxServerInfos.class】,获取客户Windows服务器的基本信息【WindowsServerInfos.class】,自定义的可被允许的服务器硬件信息的实体类【LicenseCheckModel.class】,自定义LicenseManager【CustomLicenseManager.class】,自定义KeyStoreParam【CustomKeyStoreParam.class】均与服务端代码一致;
新增的内容如下:

4.1 证书校验相关类

  • 证书校验参数类
package com.zdsf.u8cloudmanagementproject.core.license;import lombok.Data;/*** @ClassName : LicenseVerifyParam* @Description : license证书校验参数类* @Author : AD*/
@Data
public class LicenseVerifyParam {/*** 证书subject*/private String subject;/*** 公钥别称*/private String publicAlias;/*** 访问公钥库的密码*/private String storePass;/*** 证书生成路径*/private String licensePath;/*** 密钥库存储路径*/private String publicKeysStorePath;}
  • 证书校验单例模式设置
package com.zdsf.u8cloudmanagementproject.core.license;import de.schlichtherle.license.LicenseManager;
import de.schlichtherle.license.LicenseParam;/*** @ClassName : LicenseManageHolder* @Description : 监听器管理处理类 单例创建LicenseManager实例* @Author : AD*/
public class LicenseManagerHolder {private static volatile LicenseManager LICENSE_MANAGER;public static LicenseManager getInstance(LicenseParam param){if(LICENSE_MANAGER == null){synchronized (LicenseManagerHolder.class){if(LICENSE_MANAGER == null){LICENSE_MANAGER = new CustomLicenseManager(param);}}}return LICENSE_MANAGER;}
}
  • 证书校验类
package com.zdsf.u8cloudmanagementproject.core.license;import de.schlichtherle.license.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.io.File;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.prefs.Preferences;/*** @ClassName : LicenseVerify* @Description : license证书校验类* @Author : AD*/
public class LicenseVerify {private static final Logger logger = LoggerFactory.getLogger(LicenseVerify.class);/*** 安装License证书*/public synchronized LicenseContent install(LicenseVerifyParam param){LicenseContent result = null;DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//1. 安装证书try{LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));licenseManager.uninstall();result = licenseManager.install(new File(param.getLicensePath()));logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));}catch (Exception e){logger.error("证书安装失败!",e);}return result;}/*** 校验License证书* @return boolean*/public boolean verify(){LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//2. 校验证书try {LicenseContent licenseContent = licenseManager.verify();logger.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));return true;}catch (Exception e){logger.error("证书校验失败!",e);return false;}}/*** 初始化证书生成参数* @param param License校验类需要的参数* @return de.schlichtherle.license.LicenseParam*/private LicenseParam initLicenseParam(LicenseVerifyParam param){Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class,param.getPublicKeysStorePath(),param.getPublicAlias(),param.getStorePass(),null);return new DefaultLicenseParam(param.getSubject(),preferences,publicStoreParam,cipherParam);}
}

4.2 拦截器相关配置

  • 证书验证拦截器
package com.zdsf.u8cloudmanagementproject.core.license;import com.alibaba.fastjson2.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @ClassName : LoginInterceptor* @Description : 拦截器,请求拦截器 拦截相关请求验证证书* @Author : AD*/
@Component
public class LicenseCheckInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {LicenseVerify licenseVerify = new LicenseVerify();//校验证书是否有效boolean verifyResult = licenseVerify.verify();if(verifyResult){return true;}else{response.setCharacterEncoding("utf-8");JSONObject obj = new JSONObject();obj.put("errcode", "0319");obj.put("errmsg", "您的证书无效,请核查服务器是否取得授权或重新申请证书!");response.getWriter().print(obj);response.getWriter().flush();return false;}}
}
  • 拦截器配置类
package com.zdsf.u8cloudmanagementproject.config;import com.zdsf.u8cloudmanagementproject.core.license.LicenseCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @ClassName : WebMvcConfig* @Description : 注册 拦截器配置* @Author : AD*/@Configuration
public class WebMvcConfig implements WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registration){registration.addInterceptor(new LicenseCheckInterceptor()).addPathPatterns("/check/**");}}

4.3 其它配置

  • application.properties 配置文件内容示例
# license.licensePath = E:/LicenseDemo#License相关配置
license.subject=license_demo
license.publicAlias=publicCert
license.storePass=public_password1234
license.licensePath=E:/LicenseDemo/license.lic
license.publicKeysStorePath=E:/LicenseDemo/publicCerts.keystore
license.uploadPath=E:/LicenseDemo/
  • 证书安装监听器 (项目启动时进行证书的安装操作)
package com.zdsf.u8cloudmanagementproject.core.license;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;/*** @ClassName : LicenseCheckListener* @Description : 用于在项目启动的时候安装License证书* @Author : AD*/
@Component
public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {private static Logger logger = LoggerFactory.getLogger(LicenseCheckListener.class);/*** 证书subject*/@Value("${license.subject}")private String subject;/*** 公钥别称*/@Value("${license.publicAlias}")private String publicAlias;/*** 访问公钥库的密码*/@Value("${license.storePass}")private String storePass;/*** 证书生成路径*/@Value("${license.licensePath}")private String licensePath;/*** 密钥库存储路径*/@Value("${license.publicKeysStorePath}")private String publicKeysStorePath;@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {//root application context 没有parentApplicationContext context = event.getApplicationContext().getParent();if(context == null){if(StringUtils.isNotBlank(licensePath)){logger.info("++++++++ 开始安装证书 ++++++++");LicenseVerifyParam param = new LicenseVerifyParam();param.setSubject(subject);param.setPublicAlias(publicAlias);param.setStorePass(storePass);param.setLicensePath(licensePath);param.setPublicKeysStorePath(publicKeysStorePath);LicenseVerify licenseVerify = new LicenseVerify();//安装证书licenseVerify.install(param);logger.info("++++++++ 证书安装结束 ++++++++");}}}
}
  • 测试访问地址拦截controller
package com.licenseDemo.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;/*** @ClassName : LoginController* @Description : 模拟登录check检测证书* @Author : AD*/@RestController
@RequestMapping("/check")
public class LoginController {@PostMapping("/login")public Map<String, Object> check(String username, String password) {Map<String, Object> result = new HashMap<>();//模拟登录checkresult.put("success", true);result.put("message", "登录成功");result.put("data", "证书校验通过");return result;}@GetMapping("/getLogin")public Map<String, Object> check2(String username) {System.out.println("username = " + username);Map<String, Object> result = new HashMap<>();//模拟登录checkresult.put("success", true);result.put("message", "登录成功");result.put("data", "证书校验通过");return result;}
}

4.4 测试客户端

  • 启动客户端,生成证书:

image.png

  • 调用接口验证了证书

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/429599.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

变电站缺陷数据集8307张,带xml标注和txt标注,可以直接用于yolo训练

变电站缺陷数据集8307张&#xff0c; 带xml标注和txt标注&#xff0c;可以直接用于yolo训练&#xff0c;赠附五个脚本 变电站缺陷数据集 数据集概述 变电站缺陷数据集是一个专门针对变电站设备和环境缺陷检测的图像数据集。该数据集包含了8307张经过标注的图像&#xff0c;旨…

Java 入门指南:JVM(Java虚拟机)垃圾回收机制 —— 垃圾收集器

文章目录 垃圾回收机制Stop-the-World垃圾收集器垃圾收集器分类Serial 收集器Serial Old 收集器ParNew 收集器Parallel Scavenge 收集器Parallel Old 收集器CMS 收集器CMS 收集器缺点 G1 收集器G1 收集器特点G1 收集器的分代理念G1 收集器运作过程 垃圾回收机制 垃圾回收&…

【Linux笔记】如何将内容从一个文件复制到另一个文件

比如&#xff1a;将文件tmp_file.txt中的部分数据&#xff0c;复制到file01.txt中去 tmp_file.txt文中内容&#xff1a; file01.txt为空文档 一、使用vi编辑器 I、文件中直接使用:e 目标文件进行切换文件复制 1、打开被复制文件 vi tmp_file.txt 2、进入一般命令模式 默认情况为…

排序-----归并排序(递归版)

核心思想&#xff1a;假设数组前后两部分各自有序&#xff0c;然后各定义两个指针&#xff0c;谁小谁放到新开辟的数组里面&#xff0c;最后把新开辟的数组赋值给原数组就完成了。要使前后两部分有序就采用递归的方式&#xff0c;不断往下划分块&#xff0c;最后一层划分为两个…

SVM原理

SVM 这里由于过了很长时间 博主当时因为兴趣了解了下 博主现在把以前的知识放到博客上 作为以前的学习的一个结束 这些东西来自其他资料上 小伙伴看不懂英文的自行去翻译下吧 博主就偷个懒了 多维空间和低维空间 不一样的分法&#xff0c;将数据映射到高维 &…

鸿蒙OpenHarmony【轻量系统内核扩展组件(动态加载)】子系统开发

基本概念 在硬件资源有限的小设备中&#xff0c;需要通过算法的动态部署能力来解决无法同时部署多种算法的问题。以开发者易用为主要考虑因素&#xff0c;同时考虑到多平台的通用性&#xff0c;LiteOS-M选择业界标准的ELF加载方案&#xff0c;方便拓展算法生态。LiteOS-M提供类…

ZYNQ学习--AXI总线协议

一、AXI 总线简介 AXI&#xff08;Advanced Extensible Interface&#xff09;高级拓展总线是AMBA&#xff08;Advanced Microcontroller Bus Architecture&#xff09;高级微控制总线架构中的一个高性能总线协议&#xff0c;由ARM公司开发。AXI总线协议被广泛应用于高带宽、低…

PyQt5 导入ui文件报错 AttributeError: type object ‘Qt‘ has no attribute

问题描述&#xff1a; 利用 PyQt5 编写可视化界面是较为普遍的做法&#xff0c;但是使用全新UI版本的 Pycharm 修改之前正常的UI文件时&#xff0c;在没有动其他代码的情况下发现出现以下报错 AttributeError: type object Qt has no attribute Qt::ContextMenuPolicy::Defaul…

JavaEE: 深入探索TCP网络编程的奇妙世界(四)

文章目录 TCP核心机制TCP核心机制四: 滑动窗口为啥要使用滑动窗口?滑动窗口介绍滑动窗口出现丢包咋办? TCP核心机制五: 流量控制 TCP核心机制 上一篇文章 JavaEE: 深入探索TCP网络编程的奇妙世界(三) 书接上文~ TCP核心机制四: 滑动窗口 为啥要使用滑动窗口? 之前我们讨…

BERT的代码实现

目录 1.BERT的理论 2.代码实现 2.1构建输入数据格式 2.2定义BERT编码器的类 2.3BERT的两个任务 2.3.1任务一&#xff1a;Masked Language Modeling MLM掩蔽语言模型任务 2.3.2 任务二&#xff1a;next sentence prediction 3.整合代码 4.知识点个人理解 1.BERT的理论 B…

深度学习02-pytorch-08-自动微分模块

​​​​​​​ 其实自动微分模块&#xff0c;就是求相当于机器学习中的线性回归损失函数的导数。就是求梯度。 反向传播的目的&#xff1a; 更新参数&#xff0c; 所以会使用到自动微分模块。 神经网络传输的数据都是 float32 类型。 案例1: 代码功能概述&#xff1a; 该…

鸿蒙Harmony应用开发,数据驾驶舱 项目结构搭建

对于一个项目而言&#xff0c;在拿到我们的开发任务后&#xff0c;我们最重要的就是技术的选型。选型定下来了之后我们便开始脚手架的搭建&#xff0c;然后开始撸代码&#xff0c;开搞. 首先我们需要对一些常见依赖库的引入 我们需要再oh-package.json5的dependencies节点下面…

8--SpringBoot原理分析、注解-详解(面试高频提问点)

目录 SpringBootApplication 1.元注解 --->元注解 Target Retention Documented Inherited 2.SpringBootConfiguration Configuration Component Indexed 3.EnableAutoConfiguration&#xff08;自动配置核心注解&#xff09; 4.ComponentScan Conditional Co…

基于PHP的新闻管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于phpMySQL的新闻管理系统。…

JavaWeb--纯小白笔记03:servlet入门---动态网页的创建

笔记&#xff1a;index.html在tomcat中为默认的名字&#xff0c;html里面的语法不严谨。改配置文件要小心&#xff0c;不然容易删掉其他 Servlet&#xff1a;服务器端小程序&#xff0c;写动态网页需要用Servlet&#xff0c;普通的java类通过继承HttpServlet&#xff0c;可以响…

【GUI设计】基于Matlab的图像处理GUI系统(1),用matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于Matlab的图像处理GUI系统&#xff0c;用matlab实现。 本次内容主要分为两部分&a…

Why is OpenAI image generation Api returning 400 bad request in Unity?

题意&#xff1a;为什么 OpenAI 图像生成 API 在 Unity 中返回 400 Bad Request 错误&#xff1f; 问题背景&#xff1a; Im testing out dynamically generating images using OpenAI API in Unity. Amusingly, I actually generated most of this code from chatGPT. 我正在…

【笔记】第二节 轧制、热处理和焊接工艺

2.2 钢轨的轧制工艺 坯料进厂按标准验收, 然后装加热炉加热, 加热好的钢坯经高压水除鳞后进行轧制。轧出的钢轨经锯切、打印到中央冷床冷却, 然后装缓冷坑进行缓冷。缓冷后的钢轨进行矫直、轨端加工和端头淬火。钢轨入库前逐根进行探伤和外观检查。 钢轨的轧制 #mermaid-svg-…

foreach,for in和for of的区别

forEach 不能使用break return 结束并退出循环 for in 和 for of 可以使用break return&#xff1b; for in 遍历的是数组的索引&#xff08;即键名&#xff09;&#xff0c;而for of遍历的是数组元素值。 for of 遍历的只是数组内的元素&#xff0c;而不包括数组的原型属性…

后端-navicat查找语句(单表与多表)

表格字段设置如图 语句&#xff1a; 1.输出 1.输出name和age列 SELECT name,age from student 1.2.全部输出 select * from student 2.where子语句 1.运算符&#xff1a; 等于 >大于 >大于等于 <小于 <小于等于 ! <>不等于 select * from stude…