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.xdnf.cn/news/148034.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Qt笔记(十七)cmake编译Qt项目

Qt笔记&#xff08;十七&#xff09;cmake编译Qt项目 1. 文件内容与文件结构1.1.文件目录1.2. CMakeLists.txt内容1.3. main.cpp文件1.4. mouseevent.h1.5. mouseevent.cpp1.6. 生成Visual Studio项目后编译报错1.7. 界面显示中文乱码问题 1. 文件内容与文件结构 1.1.文件目录…

jdk11特性介绍

JDK 11&#xff08;也称为Java 11&#xff09;是Java平台的一个重要版本&#xff0c;它引入了许多新特性和改进&#xff0c;旨在提高开发者的生产力和Java平台的性能。以下是一些JDK 11的主要特性&#xff1a; 局部变量类型推断&#xff08;Local-Variable Syntax for Lambda P…

2009考研数学真题解析-数二:

第一题&#xff1a; 解析&#xff1a;先找间断点&#xff1a;分母不能等于0&#xff0c;分母是sinΠx&#xff0c; 因此不难看出间断点是x0&#xff0c;-1&#xff0c;-2&#xff0c;-3。。。。。 接着一个一个来算这些点是什么间断点。 &#xff0c;从x趋于2开始&#xff0c;分…

JavaScript是如何来的~~

文章目录 前言一、网络的诞生 ( The birth of the Web )二、Mosaic 浏览器三、Netscape 浏览器四、JavaScript的诞生 ~ 千呼万唤始出来总结 前言 例如&#xff1a;想要了解一门语言的发展历程&#xff0c;首先你得知道它是怎么来的&#xff0c;所以本文开篇介绍了网络的基本发…

智能BI平台项目

1.项目介绍 BI商业智能&#xff1a;数据可视化、报表可视化系统 4&#xff09;发布订阅 Resource 是基于名称进行查找的&#xff0c;而Spring框架中更常用的 Autowired 则是基于类型进行查找的。如果找不到匹配的bean&#xff0c;Autowired 会抛出异常&#xff0c;而 Resource…

EAGLE——探索混合编码器的多模态大型语言模型的设计空间

概述 准确解释复杂视觉信息的能力是多模态大型语言模型 (MLLM) 的关键重点。最近的研究表明&#xff0c;增强的视觉感知可显著减少幻觉并提高分辨率敏感任务&#xff08;例如光学字符识别和文档分析&#xff09;的性能。最近的几种 MLLM 通过利用视觉编码器的混合来实现这一点…

网络层协议 —— IP协议

目录 0.前言 1.IP协议的格式 2.IP地址 2.1IP地址的划分 国际间IP地址的划分 公有IP 私有IP 特殊的IP地址 国内IP地址的划分 2.2IP地址不足问题 2.3IP地址的功能 2.4如何使用IP地址 2.5IP地址的构成 3.网段划分 以前的方案 现在的方案 4.认识宏观网络 5.路由 …

SpringCloud config native 配置

SpringCloud config native 配置 1.概述 最近项目使用springCloud 框架&#xff0c;使用config搭建git作为配置中心。 在私有化部署中&#xff0c;出现很多比较麻烦的和鸡肋的设计。 每次部署都需要安装gitlab 有些环境安装完gitlab&#xff0c;外面不能访问&#xff0c;不给开…

QT实现升级进度条页面

一.功能说明 在Qt中实现固件升级的进度条显示窗口&#xff0c;你可以通过创建一个自定义的对话框&#xff08;Dialog&#xff09;来完成。这个对话框可以包含一个进度条&#xff08;QProgressBar&#xff09;、一些文本标签&#xff08;QLabel&#xff09;用于显示状态信息&am…

SSL 最长签发时间是多久?

在当今数字化的时代&#xff0c;网络安全变得至关重要。为了确保数据在网络传输中的安全性&#xff0c;SSL&#xff08;Secure Sockets Layer&#xff0c;安全套接层&#xff09;证书被广泛应用。那么&#xff0c;SSL最长签发时间是多久呢&#xff1f; SSL证书是一种数字证书&…

差分数组介绍

差分数组 差分数组介绍定义性质性质1: 计算数列第i项的值性质2: 计算数列第i项的前缀和应用场景差分数组具体示例【leetcode】370.区间加法题目描述题解【leetcode】1109. 航班预订统计题目描述题解【leetcode】2848.与车相交的点题目描述题解差分数组介绍 定义 对于已知有n个…

C#如何把写好的类编译成dll文件

1 新建一个类库项目 2 直接改写这个Class1.cs文件 3 记得要添加Windows.Forms引用 4 我直接把在别的项目中做好的cs文件搞到这里来&#xff0c;连文件名也改了&#xff08;FilesDirectory.cs&#xff09;&#xff0c;这里using System.Windows.Forms不会报错&#xff0c;因为前…

制造解法 Manufactured Solutions 相关的论文的阅读笔记

Verification of Euler/Navier–Stokes codes using the method of manufactured solutions https://doi.org/10.1002/fld.660 粘性项与扩散项之间的平衡 For the Navier–Stokes simulations presented herein, the absolute viscosity is chosen to be a large constant va…

【Java】掌握Java:基础概念与核心技能

文章目录 前言&#xff1a;1. 注释2. 字面量3. 变量详解3.1 变量的定义3.2 变量里的数据存储原理3.3 数据类型3.4 关键字、标识符 4. 方法4.1 方法是啥&#xff1f;4.2 方法的完整定义格式4.3 方法如何使用&#xff1a;4.4 方法的其他形式4.5 方法的其他注意事项4.5.1 方法是可…

如何充分使用芝士AI呢?一文讲清楚助力论文完成无忧

为了解决各位学弟学妹们的论文烦恼&#xff0c;助力大家毕业无忧&#xff0c;芝士AI由985硕博团队的学长学姐们潜心研发出来的一款集齐论文选题、开题报告、论文初稿、论文查重、论文降重、论文降AIGC率、论文答辩稿、论文答辩PPT&#xff0c;一站式解决困扰大家已久的论文问题…

如何创建标准操作规程(SOP)[+模板]

创建、分发和管理流程文档和逐步说明的能力是确定企业成功的关键因素。许多组织依赖标准操作规程&#xff08;SOP&#xff09;作为基本形式的文档&#xff0c;指导他们的工作流程操作。 然而&#xff0c;SOP不仅仅是操作路线图&#xff1b;它们就像高性能车辆中的先进GPS系统一…

机器视觉-7 检测原理之预处理(图像增强)

在图像处理领域&#xff0c;图像增强是一个非常重要的技术&#xff0c;目的是通过调整图像的某些特征来改善图像的视觉效果&#xff0c;或为后续的图像分析和处理做准备。在 OpenCV 中&#xff0c;C 提供了多种图像增强方法&#xff0c;包括直方图均衡化、对比度拉伸、锐化、边…

双向链表-

链表特性&#xff1a;带头/不带头 循环/非循环 --->排列组合后&#xff0c;共有8种链表结构 一.双向链表的定义 前一个节点存了后一个节点的地址&#xff0c;后一个节点也存了前一个节点的地址&#xff0c;即循环链表 二.代码解析 //双向链表 //与非循环链表区别&#…

面试官:Spring是如何解决循依赖问题?

Spring 的循环依赖一直都是 Spring 中一个很重要的话题&#xff0c;一方面是 Spring 为了解决循环依赖做了很多工作&#xff0c;另一个方面是因为它是面试 Spring 的常客&#xff0c;因为他要求你看过 Spring 的源码&#xff0c;如果没有看过 Spring 源码你基本上是回答不了这个…

【Java】线程暂停比拼:wait() 和 sleep()的较量

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持&#xff01; 在Java多线程编程中&#xff0c;合理地控制线程的执行是至关重要的。wait()和sleep()是两个常用的方法&#xff0c;它们都可以用来暂停线程的执行&#xff0c;但它们之间存在着显著的差异。本文将详…