Spring Boot2 使用 License 实现系统软件版权许可认证

本文主要是介绍Spring Boot2 使用 License 实现系统软件版权许可认证,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一 License 简介

License即版权许可证,一般用于收费软件给付费用户提供的访问许可证明。根据应用部署位置的不同,一般可以分为以下两种情况讨论:

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

二 License文件生成服务端

Spring Boot2 使用 TrueLicense 为客户生成 License 许可文件,创建微服务项目名称: cloud-license-serve,版本:2.3.0

2.1 pom.xml

        <!-- License --><dependency><groupId>de.schlichtherle.truelicense</groupId><artifactId>truelicense-core</artifactId><version>1.33</version></dependency><dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId><version>1.9.18</version></dependency>

2.2 application.properties

server.port=8081#启用优雅关机
server.shutdown=graceful
#缓冲10秒
spring.lifecycle.timeout-per-shutdown-phase=10s#License相关配置
license.licensePath=D:/workspace/gitee/spring-boot2-license/license/license.lic

2.3 License参数类

package com.modules.entity;import com.fasterxml.jackson.annotation.JsonFormat;
import com.modules.entity.LicenseCheckModel;import java.io.Serializable;
import java.util.Date;/*** License生成类需要的参数**/
public class LicenseCreatorParam implements Serializable {private static final long serialVersionUID = -7793154252684580872L;/*** 证书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;public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getPrivateAlias() {return privateAlias;}public void setPrivateAlias(String privateAlias) {this.privateAlias = privateAlias;}public String getKeyPass() {return keyPass;}public void setKeyPass(String keyPass) {this.keyPass = keyPass;}public String getStorePass() {return storePass;}public void setStorePass(String storePass) {this.storePass = storePass;}public String getLicensePath() {return licensePath;}public void setLicensePath(String licensePath) {this.licensePath = licensePath;}public String getPrivateKeysStorePath() {return privateKeysStorePath;}public void setPrivateKeysStorePath(String privateKeysStorePath) {this.privateKeysStorePath = privateKeysStorePath;}public Date getIssuedTime() {return issuedTime;}public void setIssuedTime(Date issuedTime) {this.issuedTime = issuedTime;}public Date getExpiryTime() {return expiryTime;}public void setExpiryTime(Date expiryTime) {this.expiryTime = expiryTime;}public String getConsumerType() {return consumerType;}public void setConsumerType(String consumerType) {this.consumerType = consumerType;}public Integer getConsumerAmount() {return consumerAmount;}public void setConsumerAmount(Integer consumerAmount) {this.consumerAmount = consumerAmount;}public String getDescription() {return description;}public void setDescription(String description) {this.description = description;}public LicenseCheckModel getLicenseCheckModel() {return licenseCheckModel;}public void setLicenseCheckModel(LicenseCheckModel licenseCheckModel) {this.licenseCheckModel = licenseCheckModel;}@Overridepublic String toString() {return "LicenseCreatorParam{" +"subject='" + subject + '\'' +", privateAlias='" + privateAlias + '\'' +", keyPass='" + keyPass + '\'' +", storePass='" + storePass + '\'' +", licensePath='" + licensePath + '\'' +", privateKeysStorePath='" + privateKeysStorePath + '\'' +", issuedTime=" + issuedTime +", expiryTime=" + expiryTime +", consumerType='" + consumerType + '\'' +", consumerAmount=" + consumerAmount +", description='" + description + '\'' +", licenseCheckModel=" + licenseCheckModel +'}';}
}
package com.modules.entity;import java.io.Serializable;
import java.util.List;/*** 自定义需要校验的License参数**/
public class LicenseCheckModel implements Serializable{private static final long serialVersionUID = 8600137500316662317L;/*** 可被允许的IP地址*/private List<String> ipAddress;/*** 可被允许的MAC地址*/private List<String> macAddress;/*** 可被允许的CPU序列号*/private String cpuSerial;/*** 可被允许的主板序列号*/private String mainBoardSerial;public List<String> getIpAddress() {return ipAddress;}public void setIpAddress(List<String> ipAddress) {this.ipAddress = ipAddress;}public List<String> getMacAddress() {return macAddress;}public void setMacAddress(List<String> macAddress) {this.macAddress = macAddress;}public String getCpuSerial() {return cpuSerial;}public void setCpuSerial(String cpuSerial) {this.cpuSerial = cpuSerial;}public String getMainBoardSerial() {return mainBoardSerial;}public void setMainBoardSerial(String mainBoardSerial) {this.mainBoardSerial = mainBoardSerial;}@Overridepublic String toString() {return "LicenseCheckModel{" +"ipAddress=" + ipAddress +", macAddress=" + macAddress +", cpuSerial='" + cpuSerial + '\'' +", mainBoardSerial='" + mainBoardSerial + '\'' +'}';}
}

2.4 添加抽象类AbstractServerInfos,用户获取服务器的硬件信息

package com.modules.license;import com.modules.entity.LicenseCheckModel;
import lombok.extern.slf4j.Slf4j;import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;/*** 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等**/
@Slf4j
public abstract class AbstractServerInfos {/*** 组装需要额外校验的License参数* @since 1.0.0* @return demo.LicenseCheckModel*/public LicenseCheckModel getServerInfos(){LicenseCheckModel result = new LicenseCheckModel();try {result.setIpAddress(this.getIpAddress());result.setMacAddress(this.getMacAddress());result.setCpuSerial(this.getCPUSerial());result.setMainBoardSerial(this.getMainBoardSerial());}catch (Exception e){log.error("获取服务器硬件信息失败",e);}return result;}/*** 获取IP地址* @return java.util.List<java.lang.String>*/protected abstract List<String> getIpAddress() throws Exception;/*** 获取Mac地址* @return java.util.List<java.lang.String>*/protected abstract List<String> getMacAddress() throws Exception;/*** 获取CPU序列号* @return java.lang.String*/protected abstract String getCPUSerial() throws Exception;/*** 获取主板序列号* @return java.lang.String*/protected abstract String getMainBoardSerial() throws Exception;/*** 获取当前服务器所有符合条件的InetAddress* @return java.util.List<java.net.InetAddress>*/protected List<InetAddress> getLocalAllInetAddress() throws Exception {List<InetAddress> result = new ArrayList<>(4);// 遍历所有的网络接口for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();// 在所有的接口下再遍历IPfor (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;}/*** 获取某个网络接口的Mac地址* @param* @return void*/protected String getMacByInetAddress(InetAddress inetAddr){try {byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();StringBuffer stringBuffer = new StringBuffer();for(int i=0;i<mac.length;i++){if(i != 0) {stringBuffer.append("-");}//将十六进制byte转化为字符串String temp = Integer.toHexString(mac[i] & 0xff);if(temp.length() == 1){stringBuffer.append("0" + temp);}else{stringBuffer.append(temp);}}return stringBuffer.toString().toUpperCase();} catch (SocketException e) {e.printStackTrace();log.error("获取某个网络接口的Mac地址异常", e.getMessage());}return null;}
}

获取客户Linux服务器的基本信息

package com.modules.license;import org.apache.commons.lang3.StringUtils;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.List;
import java.util.stream.Collectors;/*** 用于获取客户Linux服务器的基本信息**/
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());}return result;}@Overrideprotected String getCPUSerial() throws Exception {//序列号String serialNumber = "";//使用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.isNotBlank(line)){serialNumber = line;}reader.close();return serialNumber;}@Overrideprotected String getMainBoardSerial() throws Exception {//序列号String serialNumber = "";//使用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;}
}

获取客户Windows服务器的基本信息

package com.modules.license;import java.net.InetAddress;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;/*** 用于获取客户Windows服务器的基本信息**/
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());}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 getMainBoardSerial() 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;}
}

注:这里使用了模板方法模式,将不变部分的算法封装到抽象类,而基本方法的具体实现则由子类来实现。

2.5 自定义LicenseManager,用于增加额外的服务器硬件信息校验

package com.modules.license;import com.modules.entity.LicenseCheckModel;
import de.schlichtherle.license.*;
import de.schlichtherle.xml.GenericCertificate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.List;/*** 自定义LicenseManager,用于增加额外的服务器硬件信息校验**/
@Slf4j
public class CustomLicenseManager extends LicenseManager{//XML编码private static final String XML_CHARSET = "UTF-8";//默认BUFSIZEprivate static final int DEFAULT_BUFSIZE = 8 * 1024;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) {log.error("XMLDecoder解析XML失败",e.getMessage());}}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;}}}

2.6 最后是License生成类,用于生成License证书

package com.modules.license;import com.modules.entity.LicenseCreatorParam;
import de.schlichtherle.license.*;
import lombok.extern.slf4j.Slf4j;import javax.security.auth.x500.X500Principal;
import java.io.File;
import java.text.MessageFormat;
import java.util.prefs.Preferences;/*** License生成类**/
@Slf4j
public class LicenseCreator {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){log.error(MessageFormat.format("证书生成失败:{0}",param),e.getMessage());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;}}

2.7 添加一个生成证书的Controller

这个Controller对外提供了两个RESTful接口,分别是 [获取服务器硬件信息] 和 [生成证书],这样做不安全,可以自己写测试类。

package com.modules.controller;import com.modules.entity.LicenseCheckModel;
import com.modules.entity.LicenseCreatorParam;
import com.modules.license.*;
import org.apache.commons.lang3.StringUtils;
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;/**** 用于生成证书文件,不能放在给客户部署的代码里* @since 1.0.0*/
@RestController
@RequestMapping("/license")
public class LicenseCreatorController {/*** 证书生成路径*/@Value("${license.licensePath}")private String licensePath;/*** 获取服务器硬件信息* @param osName 操作系统类型,如果为空则自动判断* @return com.ccx.models.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();}/*** 生成证书* @param param 生成证书需要的参数,如:{"subject":"ccx-models","privateAlias":"privateKey","keyPass":"5T7Zz5Y0dJFcqTxvzkH5LDGJJSGMzQ","storePass":"3538cef8e7","licensePath":"C:/Users/zifangsky/Desktop/license.lic","privateKeysStorePath":"C:/Users/zifangsky/Desktop/privateKeys.keystore","issuedTime":"2018-04-26 14:48:12","expiryTime":"2018-12-31 00:00:00","consumerType":"User","consumerAmount":1,"description":"这是证书描述信息","licenseCheckModel":{"ipAddress":["192.168.245.1","10.0.5.22"],"macAddress":["00-50-56-C0-00-01","50-7B-9D-F9-18-41"],"cpuSerial":"BFEBFBFF000406E3","mainBoardSerial":"L1HF65E00X9"}}* @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 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;}
}

2.8 使用JDK自带的 keytool 工具生成公私钥证书库

 假如我们设置公钥库密码为:public_password1234,私钥库密码为:private_password1234,创建目录,在目录地址输入cmd,文件生成命令如下:

#生成命令
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"#导出命令
keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -file "certfile.cer"#导入命令
keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "public_password1234"

上述命令执行完成之后,会在当前路径下生成三个文件,分别是:privateKeys.keystore、publicCerts.keystore、certfile.cer。其中文件certfile.cer不再需要可以删除,文件privateKeys.keystore用于当前的 cloud-license-serve 项目给客户生成license文件,而文件publicCerts.keystore则随应用代码部署到客户服务器,用户解密license文件并校验其许可信息。

2.9 为客户生成license文件

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

获取服务器硬件信息

生成简单证书
{"subject": "license_demo","privateAlias": "privateKey","keyPass": "private_password1234","storePass": "public_password1234","licensePath": "D:/workspace/gitee/spring-boot2-license/license/license.lic","privateKeysStorePath": "D:/workspace/gitee/spring-boot2-license/license/privateKeys.keystore","issuedTime": "2021-05-14 00:00:01","expiryTime": "2021-05-14 11:21:00","consumerType": "User","consumerAmount": 1,"description": "这是证书描述信息"
}

生成复杂证书
{"subject": "license_demo","privateAlias": "privateKey","keyPass": "private_password1234","storePass": "public_password1234","licensePath": "D:/workspace/gitee/spring-boot2-license/license/license.lic","privateKeysStorePath": "D:/workspace/gitee/spring-boot2-license/license/privateKeys.keystore","issuedTime": "2021-05-14 00:00:01","expiryTime": "2021-05-14 23:59:59","consumerType": "User","consumerAmount": 1,"description": "这是证书描述信息","ipAddress": ["172.16.1.126","192.168.56.1"],"macAddress": ["00-E1-4C-68-07-B5","0A-00-27-00-00-10"],"cpuSerial": "178BFBFF00860F01","mainBoardSerial": "BBDW0820AG001635"
}

三 客户部署的应用中添加License校验

创建微服务项目名称: cloud-license-client,版本:2.3.0,模拟给客户部署的应用。

3.1 pom.xml

        <!-- License --><dependency><groupId>de.schlichtherle.truelicense</groupId><artifactId>truelicense-core</artifactId><version>1.33</version></dependency><dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId><version>1.9.18</version></dependency>

3.2 application.properties

server.port=8082#启用优雅关机
server.shutdown=graceful
#缓冲10秒
spring.lifecycle.timeout-per-shutdown-phase=10s#License相关配置
license.subject=license_demo
license.publicAlias=publicCert
license.storePass=public_password1234
license.licensePath=D:/workspace/gitee/spring-boot2-license/license/license.lic
license.publicKeysStorePath=D:/workspace/gitee/spring-boot2-license/license/publicCerts.keystore

3.3 License校验类

package com.modules.license;import com.modules.entity.LicenseVerifyParam;
import de.schlichtherle.license.*;
import lombok.extern.slf4j.Slf4j;import java.io.File;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.prefs.Preferences;/*** License校验类*/
@Slf4j
public class LicenseVerify {/*** 安装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()));log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));}catch (Exception e){log.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();
//            System.out.println(licenseContent.getSubject());log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));return true;}catch (Exception e){log.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);}}

3.4 添加Listener,用于在项目启动的时候安装License证书

package com.modules.license;import com.modules.entity.LicenseVerifyParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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;/*** 在项目启动时安装证书*/
@Slf4j
@Component
public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {/*** 证书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)){log.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);log.info("++++++++ 证书安装结束 ++++++++");}}}
}

3.5 添加拦截器,用于在登录的时候校验License证书

package com.modules.common.config;import com.modules.license.LicenseCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import javax.annotation.Resource;/*** @Description: 拦截器配置*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {@Resourceprivate LicenseCheckInterceptor licenseCheckInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//添加要拦截的urlregistry.addInterceptor(licenseCheckInterceptor)// 拦截的路径.addPathPatterns("/**");// 放行的路径
//                .excludePathPatterns("/admin/login");}
}

3.6 模拟登陆LoginController

package com.modules.controller;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;/*** 登录*/
@Slf4j
@CrossOrigin
@RestController
public class LoginController {/*** 模拟登录验证* @param loginName 用户名* @param password 密码*/@GetMapping(value = "login")public Map<String,Object> test(@RequestParam(required = true) String loginName, @RequestParam(required = true) String password){Map<String,Object> result = new HashMap<>(1);log.info(MessageFormat.format("登录名称:{0},密码:{1}",loginName,password));//模拟登录log.info("模拟登录被拦截检查证书可用性");result.put("code",200);return result;}
}

3.7 测试

3.7.1 验证失败测试

在客户项目启动时,验证许可证可使用性,如果验证报错则验证失败,调用接口会被拦截(只要在拦截范围内),进行许可证不可用提示。

3.7.2 验证成功测试

在客户项目启动时,验证许可证可使用性,如果验证成功,调用接口不会被拦截,系统正常操作。

四 源码

码云源码地址:https://gitee.com/lichao12/spring-boot2-license

这篇关于Spring Boot2 使用 License 实现系统软件版权许可认证的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/696612

相关文章

Spring Security基于数据库的ABAC属性权限模型实战开发教程

《SpringSecurity基于数据库的ABAC属性权限模型实战开发教程》:本文主要介绍SpringSecurity基于数据库的ABAC属性权限模型实战开发教程,本文给大家介绍的非常详细,对大... 目录1. 前言2. 权限决策依据RBACABAC综合对比3. 数据库表结构说明4. 实战开始5. MyBA

AJAX请求上传下载进度监控实现方式

《AJAX请求上传下载进度监控实现方式》在日常Web开发中,AJAX(AsynchronousJavaScriptandXML)被广泛用于异步请求数据,而无需刷新整个页面,:本文主要介绍AJAX请... 目录1. 前言2. 基于XMLHttpRequest的进度监控2.1 基础版文件上传监控2.2 增强版多

使用Python自建轻量级的HTTP调试工具

《使用Python自建轻量级的HTTP调试工具》这篇文章主要为大家详细介绍了如何使用Python自建一个轻量级的HTTP调试工具,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录一、为什么需要自建工具二、核心功能设计三、技术选型四、分步实现五、进阶优化技巧六、使用示例七、性能对比八、扩展方向建

Spring Security方法级安全控制@PreAuthorize注解的灵活运用小结

《SpringSecurity方法级安全控制@PreAuthorize注解的灵活运用小结》本文将带着大家讲解@PreAuthorize注解的核心原理、SpEL表达式机制,并通过的示例代码演示如... 目录1. 前言2. @PreAuthorize 注解简介3. @PreAuthorize 核心原理解析拦截与

一文详解JavaScript中的fetch方法

《一文详解JavaScript中的fetch方法》fetch函数是一个用于在JavaScript中执行HTTP请求的现代API,它提供了一种更简洁、更强大的方式来处理网络请求,:本文主要介绍Jav... 目录前言什么是 fetch 方法基本语法简单的 GET 请求示例代码解释发送 POST 请求示例代码解释

Java图片压缩三种高效压缩方案详细解析

《Java图片压缩三种高效压缩方案详细解析》图片压缩通常涉及减少图片的尺寸缩放、调整图片的质量(针对JPEG、PNG等)、使用特定的算法来减少图片的数据量等,:本文主要介绍Java图片压缩三种高效... 目录一、基于OpenCV的智能尺寸压缩技术亮点:适用场景:二、JPEG质量参数压缩关键技术:压缩效果对比

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

Redis分片集群的实现

《Redis分片集群的实现》Redis分片集群是一种将Redis数据库分散到多个节点上的方式,以提供更高的性能和可伸缩性,本文主要介绍了Redis分片集群的实现,具有一定的参考价值,感兴趣的可以了解一... 目录1. Redis Cluster的核心概念哈希槽(Hash Slots)主从复制与故障转移2.

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

使用Python实现一键隐藏屏幕并锁定输入

《使用Python实现一键隐藏屏幕并锁定输入》本文主要介绍了使用Python编写一个一键隐藏屏幕并锁定输入的黑科技程序,能够在指定热键触发后立即遮挡屏幕,并禁止一切键盘鼠标输入,这样就再也不用担心自己... 目录1. 概述2. 功能亮点3.代码实现4.使用方法5. 展示效果6. 代码优化与拓展7. 总结1.