基于 TrueLicense 生成的授权文件证书存在很多局限性。所用这里通过自定义的方式来实现一个License授权文件的生成!
这里通过非对称加密RSA 的方式来创建 项目授权文件内容! 需要注意项目打包后最好将class文件进行防反编译的操作! 否则通过暴力方式还能能获取到授权文件的解密方式,然后破解着就能通过源码的方式重新生成对应的授权文件!
一、授权文件 服务端
1.1 服务端配置相关内容:
- application.properties 配置文件内容
# 证书下载目录地址,为null则默认下载到项目根目录 (这里不用添加 /license.lic 后缀,后续代码中会自动添加进入!!)
#license.licensePath = E:\\LicenseDemo
license.licensePath =
#License证书文件所在的服务器请求下载地址的前缀
springboot.license.server.prefix = http://127.0.0.1:8999/license/
- pom.xml 中需要注入的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 相关工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
1.2 RSA工具类代码
该类主要用于生成RSA 密钥对,同时对我们的 授权文件证书的内容进行加密操作:
package com.selfLicense.licenseselfservice.utils;
import com.alibaba.fastjson2.JSONObject;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;
/**
* @ClassName : RSAUtil
* @Description : RSA非对称加密工具类
* @Author : AD
*/
public class RSAUtil {
/** 密钥规范(KeySpec) */
public static final String KEY_ALGORITHM ="RSA";
public static void main(String[] args) throws Exception {
/* ===生成RSA公钥私钥测试=== */
Map map = generateRandomToBase64Key();
System.out.println("JSONObject.toJSONString(map) = " + JSONObject.toJSONString(map));
Map<String,String> signTestMap = new HashMap<String,String>();
signTestMap.put("name","张三");
signTestMap.put("age","25");
signTestMap.put("sex","男");
signTestMap.put("nullValueTest","");
signTestMap.put("null",null);
/* ===生成参数签名sort测试=== */
String excludeNullSign = generateSortSign(signTestMap, true);
String containNullSign = generateSortSign(signTestMap, false);
System.out.println("包含null的签名顺序数据 excludeNullSign = " + excludeNullSign);
System.out.println("排除null的签名顺序数据 containNullSign = " + containNullSign);
/* ===生成签名测试=== */
String signature = generateSign(containNullSign.getBytes("UTF-8"), (String) map.get("privateKey"), SIGN_ALGORITHM);
System.out.println("数据签名信息 signature= " + signature);
/* ===验证签名测试=== */
boolean verifyValue = signVerify(containNullSign.getBytes("UTF-8"), (String) map.get("publicKey"), signature, SIGN_ALGORITHM);
System.out.println("签名认证结果 verifyValue = " + verifyValue);
String jsonString = JSONObject.toJSONString(signTestMap);
System.out.println("代加密的数据内容 jsonString = " + jsonString);
/* ===数据加密测试=== */
String encryptToBase64 = encryptToBase64(jsonString.getBytes("UTF-8"), (String) map.get("publicKey"));
System.out.println("加密之后的数据 encryptToBase64 = " + encryptToBase64);
/* ===数据解密测试=== */
String decrypt = decryptByBase64ToJsonString(encryptToBase64, (String) map.get("privateKey"));
System.out.println("解密之后的数据 decrypt = " + decrypt);
}
/** ====================【Rsa加密相关操作】===================== */
//RSA最大加密明文大小
public static final int MAX_ENCRYPT_BLOCK = 117;
//RSA最大解密密文大小
public static final int MAX_DECRYPT_BLOCK = 128;
/**
* RSA 公钥私钥生成器
* */
public static Map generateRandomToBase64Key(){
String KEY_ALGORITHM ="RSA";
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
//密钥位数
keyPairGenerator.initialize(1024);
//创建公钥/私钥
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
byte[] publicKeyEncoded = publicKey.getEncoded();
byte[] privateKeyEncoded = privateKey.getEncoded();
HashMap<String,String> map = new HashMap<>();
map.put("publicKey", ByteArrayToBase64(publicKeyEncoded));
map.put("privateKey",ByteArrayToBase64(privateKeyEncoded));
return map;
}
/**
* Description: 数据加密成为Base64编码格式
*
* @param data 待加密数据 (Byte[])
* @param publicKey 加密公钥 (Base64编码)
* @return java.lang.String
*/
public static String encryptToBase64(byte[] data,String publicKey) throws Exception
{
byte[] bytes = encryptByPublicKeyToByte(data, publicKey, MAX_ENCRYPT_BLOCK);
return ByteArrayToBase64(bytes);
}
/**
* Description:
*
* @param data 待解密数据 (Base64编码)
* @param privateKey 私钥 (Base64编码)
* @return java.lang.String
*/
public static String decryptByBase64ToJsonString(String data,String privateKey) throws Exception
{
byte[] bytes = Base64ToByteArray(data);
byte[] decryptData = decryptByPrivateKeyToByte(bytes, privateKey, MAX_DECRYPT_BLOCK);
return new String(decryptData,"UTF-8");
}
/**
* Description: 公钥分段加密数据
*
* @param data 待加密源数据 (Byte[])
* @param publicKey 公钥(BASE64编码)
* @param length 段长 1024长度的公钥最大取117
* @return byte[] 加密后byte数据
*/
public static byte[] encryptByPublicKeyToByte(byte[] data,String publicKey,int length) throws Exception
{
/*
* 将BASE64编码格式 publicKey进行解码
* */
byte[] publicKeyByte = Base64ToByteArray(publicKey);
/*
* 使用X509EncodedKeySpec类创建了一个X.509编码的KeySpec对象,并将publicKeyByte作为参数传入。
* 将公钥 [字符串] 解码成 [公钥对象] ,以便用于加密数据。
* */
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyByte);
/*
* 通过KeyFactory获取了RSA的实例
* */
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
/*
* 调用generatePublic方法使用之前创建的X509EncodedKeySpec对象来生成公钥。
* */
Key generatePublicKey = keyFactory.generatePublic(x509EncodedKeySpec);
/*
* 创建一个Cipher实例,它是用于加密或解密数据的对象。Cipher类提供了加密和解密功能,并支持许多不同的加密算法。
* 在这里,getInstance 方法中传入了keyFactory.getAlgorithm()[获取与指定密钥工厂相关联的算法名称。],它用于获取与指定算法关联的 Cipher 实例。
* */
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
/*
* 初始化 Cipher 对象。
* 在初始化过程中,指定加密模式为 ENCRYPT_MODE,并传入了之前生成的公钥 generatePublicKey。
* */
cipher.init(Cipher.ENCRYPT_MODE,generatePublicKey);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
//段落起始位置
int offSet = 0;
byte[] cache;
int i = 0;
//对数据进行分段加密
while (inputLen - offSet > 0)
{
if (inputLen - offSet > length) {
cache = cipher.doFinal(data,offSet,length);
} else {
cache = cipher.doFinal(data,offSet,inputLen-offSet);
}
out.write(cache,0,cache.length);
i++;
offSet = i * length;
}
byte[] encryptDate = out.toByteArray();
out.close();
return encryptDate;
}
/**
* Description: 私钥分段解密数据
*
* @param data 待解密数据
* @param privateKey 私密(BUSE64编码)
* @param length 分段解密长度 128
* @return byte[]
*/
public static byte[] decryptByPrivateKeyToByte(byte[] data,String privateKey,int length) throws Exception
{
byte[] privateKeyByte = Base64ToByteArray(privateKey);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyByte);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key generatePrivateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE,generatePrivateKey);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
//对数据进行分段解密
while (inputLen - offSet > 0)
{
if (inputLen - offSet > length)
{
cache = cipher.doFinal(data,offSet,length);
} else {
cache = cipher.doFinal(data,offSet,inputLen - offSet);
}
out.write(cache,0,cache.length);
i++;
offSet = i * length;
}
byte[] decryptData = out.toByteArray();
out.close();
return decryptData;
}
/** ====================【数据格式转换相关操作】===================== */
private static char[] HEXCHAR = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
/**
* Description: 将byte数组转换为Base64字符串
*
* @param bytes
* @return java.lang.String
*/
public static String ByteArrayToBase64(byte[] bytes){
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(bytes);
}
/**
* Description: Base64字符串转换为byte数组
*
* @param base64
* @return byte[]
*/
public static byte[] Base64ToByteArray(String base64){
BASE64Decoder decoder = new BASE64Decoder();
try{
return decoder.decodeBuffer(base64);
}catch (Exception ex){
return null;
}
}
/**
* Description: 将字节数组转换为十六进制字符串
*
* @param b
* @return java.lang.String
*/
public static String ByteArrayToHexString(byte[] b){
/*
* 每个字节都可以用两个十六进制字符来表示,因此初始化的容量是字节数组长度的两倍。
* */
StringBuilder sb = new StringBuilder(b.length * 2);
for (int i = 0; i<b.length ;i++)
{
/*
* 首先取字节的高四位,然后查找对应的十六进制字符,并将其追加到StringBuilder中
* */
sb.append(HEXCHAR[(b[i] & 0xf0) >>> 4]);
/*
* 取字节的低四位,找到对应的十六进制字符,并追加到StringBuilder中。
* */
sb.append(HEXCHAR[b[i] & 0x0f]);
}
return sb.toString();
}
/**
* Description: 将十六进制字符串转换为字节数组
*
* @param s
* @return byte[]
*/
public static byte[] HexStringToByteArray(String s) {
byte[] bytes;
bytes = new byte[s.length() / 2];
for (int i = 0; i < bytes.length ; i++)
{
bytes[i] = (byte) Integer.parseInt(s.substring(2*i,2*i+2),16);
}
return bytes;
}
/** ====================【sign签名相关操操作】===================== */
/** 签名算法 */
public static final String SIGN_ALGORITHM = "SHA256withRSA";
/**
* Description: 根据请求Map集合数据,自然排序sort后,组装生成对应请求中的签名参数sign
*
* @param map
* @param allowValueNull 空值value是否参加验签 (最好设置为false,排除空值value参与验签)
* @return java.lang.String
*/
public static String generateSortSign(Map<String,String> map ,boolean allowValueNull){
List<String> keys = new ArrayList<>(map.size());
for(String key : map.keySet())
{
/*
* 排除下列参数数据
* 1.不允许出现空值value 且 map 中 为null 的键值对
* 2.参数签名内容键值对
* */
if ( (!allowValueNull && isEntity(map.get(key))) || "sign".equals(key) || "signValue".equals(key) || "signature".equals(key))
{
continue;
}
keys.add(key);
}
/* sort静态方法,用于自然顺序或者自定义顺序对List 进行排序处理 */
Collections.sort(keys);
StringBuffer stringBuffer = new StringBuffer();
boolean isFirst = true;
for (String key : keys)
{
if (isFirst)
{
stringBuffer.append(key).append("=").append(map.get(key));
isFirst =false;
continue;
}
stringBuffer.append("&").append(key).append("=").append(map.get(key));
}
return stringBuffer.toString();
}
/**
* Description: 用于生成数据的数字签名,并将签名数据转换为十六进制字符串格式返回
*
* @param rawDate 请求数据(签名裸数据) (byte[])
* @param privateKey 私钥 (Base64编码)
* @param algorithm 签名验算算法
* @return java.lang.String (HexString)
*/
public static String generateSign(byte[] rawDate,String privateKey,String algorithm) throws Exception
{
byte[] privateKeyBytes = Base64ToByteArray(privateKey);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey generatePrivate = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
/* 使用指定的签名算法algorithm,通过Signature实例获取签名对象signature。 */
Signature signature = Signature.getInstance(algorithm);
// Signature signature = Signature.getInstance(SIGN_ALGORITHM);
/* 初始化签名对象,传入生成的私钥generatePrivate。 */
signature.initSign(generatePrivate);
/* 将要签名的裸数据rawData传入签名对象。 */
signature.update(rawDate);
/* 生成签名数据sign */
byte[] sign = signature.sign();
return ByteArrayToHexString(sign);
}
/**
* Description: 验证签名
*
* @param data 请求数据(签名裸数据) (byte[])
* @param publicKey 公钥 (Base64编码)
* @param sign 签名数据 (HexString)
* @param algorithm 签名验算算法
* @return boolean
*/
public static boolean signVerify(byte[] data,String publicKey,String sign,String algorithm) throws Exception
{
byte[] publicKeyBytes = Base64ToByteArray(publicKey);
/* X509EncodedKeySpec是一个用于表示X.509标准编码的公钥规范的类。 */
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
/*
* KEY_ALGORITHM是一个常量,通常为"RSA"。
* KeyFactory用于将KeySpec对象转换为密钥对象。
* */
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
/* 使用KeyFactory的generatePublic方法将X509EncodedKeySpec转换为PublicKey对象 */
PublicKey generatePublicKey = keyFactory.generatePublic(x509EncodedKeySpec);
/*
* Signature类用于生成和验证数字签名。
* getInstance方法根据指定的算法获取Signature实例。
* */
Signature signature = Signature.getInstance(algorithm);
// Signature signature = Signature.getInstance(SIGN_ALGORITHM);
signature.initVerify(generatePublicKey);
signature.update(data);
return signature.verify(HexStringToByteArray(sign));
}
/**
* Description: 判断对象是否为空
*
* @param str
* @return boolean
*/
public static boolean isEntity(Object str){
if (str instanceof String){
str =((String) str).trim();
}
return (str == null || "".equals(str));
}
}
1.3 证书内容实体类
证书模型实体类数据,可以根据实际情况,自定义添加需要填充的证书内容。
package com.selfLicense.licenseselfservice.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @ClassName : LicenseEntity
* @Description : 证书模型实体类数据
* @Author : AD
*/
@Data
public class LicenseEntity {
/** 证书主题 */
private String subject;
/** 证书发行者 */
private String issuer = "xxxx科技有限公司";
/** 证书生效时间 */
@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;
/**
* 可允许的CPU序列号
* */
private String cpuSerial;
/**
* 可允许的主板序列号(硬件序列化?)
* */
private String mainBoardSerial;
private List<String> IpAddress;
private List<String> MacAddress;
/**
* 限制系统中可注册的人数 [自定义]
* */
private Long registerAmount = 2000L;
/** 注册模块数据 */
private List<String> modules;
}
1.3 授权文件/证书 工具类
该类是创建证书的主要类,主要包含功能有 证书的创建、证书的数据校验功能、证书的读取等功能。
ps:由于这里服务端和客户端使用的方法都全部写在了该类中 并未独立出来,所用会存在某些对象不存在的现象。不存在的对象都放在了客户端模块中,只有客户端模块才会使用到。 这里之间将对应的方法注释掉即可!
package com.selfLicense.licenseselfservice.utils;
import com.alibaba.fastjson2.JSONObject;
import com.selfLicense.licenseselfservice.model.LicenseEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @ClassName : CustomLicenseManager
* @Description : 自定义证书管理操作工具
* @Author : AD
*/
public class CustomLicenseManager {
private static final String XML_CHARSET = "UTF-8";
private static final String LICENSE_BEGIN = "--------------------------------------BEGIN U8CLOUD.LICENSE.1.0--------------------------------------\n";
private static final String LICENSE_PRIVATE_KEY_BEGIN = "\n--------------------------------------U8CLOUD.LICENSE.1.0 PRIVATE KEY--------------------------------------\n";
private static final String LICENSE_SINGN_BEGIN = "\n--------------------------------------U8CLOUD.LICENSE.1.0 SIGN--------------------------------------\n";
private static final String LICENSE_END = "\n--------------------------------------END U8CLOUD.LICENSE.1.0--------------------------------------\n";
private static final String LICENSE_DATA_KEY = "LICENSE_DATA";
private static final String LICENSE_PRIVATE_KEY_KEY = "LICENSE_PRIVATE_KEY";
private static final String LICENSE_SIGN_KEY = "LICENSE_SIGN";
private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);
/**
* Description: 证书创建类
*
* @param licenseEntity 证书生成参数对象
* @return byte[]
*/
public synchronized String serviceCreateLicense(LicenseEntity licenseEntity) throws Exception
{
// 证书创建参数的校验
serviceValidateCreate(licenseEntity);
// 获取公私钥
Map map = RSAUtil.generateRandomToBase64Key();
String publicKey = (String) map.get("publicKey");
String privateKey = (String) map.get("privateKey");
// 证书加密数据
String licenseData = JSONObject.toJSONString(licenseEntity);
String encryptToBase64 = RSAUtil.encryptToBase64(licenseData.getBytes(XML_CHARSET), publicKey);
// 证书加密后加签数据
String sign = RSAUtil.generateSign(encryptToBase64.getBytes(XML_CHARSET), privateKey, RSAUtil.SIGN_ALGORITHM);
// log.info("待加密数据:{} \n加密后数据:{} \n加密数据sign:{}",licenseData,encryptToBase64,sign);
StringBuffer license = new StringBuffer();
license.append(LICENSE_BEGIN).append(encryptToBase64).append(LICENSE_PRIVATE_KEY_BEGIN).append(privateKey).append(LICENSE_SINGN_BEGIN).append(sign).append(LICENSE_END)
.append("产品名称:U8Cloud License V:LICENSE.1.0\n")
.append("证书发行者:"+licenseEntity.getIssuer()+"\n")
.append("证书生成日期:"+licenseEntity.getIssuedTime()+"\n")
.append("证书失效日期:"+licenseEntity.getExpiryTime()+"\n");
return license.toString();
}
/**
* Description: 证书创建时的数据校验
*
* @param licenseEntity
* @return void
*/
public synchronized void serviceValidateCreate(LicenseEntity licenseEntity) throws Exception
{
final Date now = new Date();
final Date notBefore = licenseEntity.getIssuedTime();
final Date notAfter = licenseEntity.getExpiryTime();
if (null != notAfter && now.after(notAfter)){
throw new RuntimeException("证书失效时间不能早于当前时间!");
}
if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
throw new RuntimeException("证书生效时间不能晚于证书失效时间!");
}
final String consumerType = licenseEntity.getCpuSerial();
if (null == consumerType){
throw new RuntimeException("主机序列号不能为空!");
}
}
/**
* Description: 证书生成输出流
*
* @param licenseStr 证书加密后的内容
* @param ioPath 证书生成路径
* @return void
*/
public void serviceOutputLicenseFile(String licenseStr,String ioPath) throws Exception
{
// 输出证书文件
File file = new File(ioPath);
OutputStream fileOutputStream = new FileOutputStream(file);
OutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
bufferedOutputStream.write(licenseStr.getBytes(XML_CHARSET));
bufferedOutputStream.flush();
bufferedOutputStream.close();
fileOutputStream.close();
}
/**
* Description: [client 证书安装操作]读取证书文件内容,并分解各部分内容
* @param licensePath
* @return void
*/
public synchronized LicenseEntity clientLicenseLoad(String licensePath) throws Exception
{
File file = new File(licensePath);
if (!file.exists()){
throw new RuntimeException("证书文件不存在!");
}
InputStream fileIn = new FileInputStream(file);
InputStream bufferFileIn = new BufferedInputStream(fileIn);
String licenseStr = "";
byte[] bytes = new byte[1024];
int len = 0;
while ( (len = bufferFileIn.read(bytes)) != -1)
{
licenseStr += new String(bytes,0,len);
}
bufferFileIn.close();
fileIn.close();
String licenseData = stringSubMethod(licenseStr, LICENSE_BEGIN, LICENSE_PRIVATE_KEY_BEGIN);
String licensePrivateKey = stringSubMethod(licenseStr, LICENSE_PRIVATE_KEY_BEGIN, LICENSE_SINGN_BEGIN);
String licenseSign = stringSubMethod(licenseStr, LICENSE_SINGN_BEGIN, LICENSE_END);
/*
System.out.println("licenseData = " + licenseData);
System.out.println("licensePrivateKey = " + licensePrivateKey);
System.out.println("licenseSign = " + licenseSign);
*/
// 分解获取数据内容
Map<String ,String> map = new HashMap<>();
map.put(LICENSE_DATA_KEY,licenseData);
map.put(LICENSE_PRIVATE_KEY_KEY,licensePrivateKey);
map.put(LICENSE_SIGN_KEY,licenseSign);
// 读取证书文件内容
String jsonString = RSAUtil.decryptByBase64ToJsonString(licenseData,licensePrivateKey);
LicenseEntity licenseEntity = JSONObject.parseObject(jsonString, LicenseEntity.class);
// 验签操作
return licenseEntity;
}
/**
* Description: 证书内容分解方法
*
* @param targetStr 目标
* @param startMarker 开始字符串
* @param endMarker 结束字符串
* @return java.lang.String
*/
public String stringSubMethod(String targetStr,String startMarker,String endMarker)
{
// 加上startMarker的长度以跳过它
// int startIndex = targetStr.indexOf(startMarker)+startMarker.length();
int startIndex = targetStr.lastIndexOf(startMarker)+startMarker.length();
// 从startIndex开始搜索endMarker
int endIndex = targetStr.indexOf(endMarker, startIndex);
if (startIndex != -1 && endIndex != -1) {
return targetStr.substring(startIndex, endIndex);
} else {
throw new RuntimeException("证书内容有误!");
}
}
/**
* Description: 证书相关数据校验方法
* @param expectedCheck 解析出的证书内容
* @return void
*/
public synchronized void clientLicenseValidate(final LicenseEntity expectedCheck) throws Exception
{
// 先判断待安装证书是否已经过期!
final Date now = new Date();
final Date notAfter = expectedCheck.getExpiryTime();
if(now.after(notAfter)){
throw new RuntimeException("【License】 系统证书过期,当前时间已超过证书有效期 -- "+
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(notAfter) +" ");
}
//当前服务器真实的参数信息
LicenseEntity serverCheckModel = AbstractServerInfos.getServer(null).getServerInfos();
if(expectedCheck != null && serverCheckModel != null){
//校验IP地址
if(!checkIpAddress(expectedCheck.getIpAddress(),serverCheckModel.getIpAddress())){
String message = "【License】 系统证书无效,当前服务器的IP没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验Mac地址
if(!checkIpAddress(expectedCheck.getMacAddress(),serverCheckModel.getMacAddress())){
String message = "【License】 系统证书无效,当前服务器的Mac地址没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验主板序列号
if(!checkSerial(expectedCheck.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
String message = "【License】 系统证书无效,当前服务器的主板序列号没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验CPU序列号
if(!checkSerial(expectedCheck.getCpuSerial(),serverCheckModel.getCpuSerial())){
String message = "【License】 系统证书无效,当前服务器的CPU序列号没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
}else{
logger.error("【License】 不能获取服务器硬件信息");
throw new RuntimeException("不能获取服务器硬件信息");
}
}
/**
* Description: 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
*
* @param expectedSerial 证书中包含的序列号
* @param serverSerial 当前服务器真实的序列号
* @return boolean
*/
private boolean checkSerial(String expectedSerial,String serverSerial){
if(expectedSerial != null && expectedSerial.trim().length() > 0){
if(serverSerial != null && serverSerial.trim().length() > 0){
if(expectedSerial.equals(serverSerial)){
return true;
}
}
return false;
}else{
return true;
}
}
/**
* Description: 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
*
* @param expectedList 证书中包含的list
* @param serverList 当前服务器真实的list
* @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;
}
}
public static void main(String[] args) throws Exception {
CustomLicenseManager customLicenseManager = new CustomLicenseManager();
String licensePath = "E:/LicenseDemo/license/20240802173559/license";
LicenseEntity licenseMap1 = null;
try {
licenseMap1 = customLicenseManager.clientLicenseLoad(licensePath);
System.out.println("licenseMap1 = " + licenseMap1);
} catch (Exception e) {
throw new RuntimeException(e);
}
customLicenseManager.clientLicenseValidate(licenseMap1);
}
}
1.4 授权文件创建Service
证书创建Service服务类代码:
package com.selfLicense.licenseselfservice.service;
import com.selfLicense.licenseselfservice.model.LicenseEntity;
import com.selfLicense.licenseselfservice.utils.CustomLicenseManager;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName : LicenseCreateService
* @Description : 证书创建Service服务类
* @Author : AD
*/
public class LicenseCreateService {
private LicenseEntity licenseEntity;
public LicenseCreateService(LicenseEntity licenseEntity) {
this.licenseEntity = licenseEntity;
}
/**
* Description: 证书生成方法
*
* @param
* @return com.selfLicense.licenseselfservice.utils.ResponseResult
*/
public Map<String,String> generateLicense(String ioPath)
{
Map<String, String> resultMap = new HashMap<>();
CustomLicenseManager customLicenseManager = new CustomLicenseManager();
try {
String licenseStr = customLicenseManager.serviceCreateLicense(licenseEntity);
customLicenseManager.serviceOutputLicenseFile(licenseStr, ioPath);
} catch (Exception e) {
resultMap.put("code", "500");
resultMap.put("message","证书生成失败:"+ e.getMessage());
return resultMap;
}
resultMap.put("code", "200");
resultMap.put("message","证书生成成功!");
return resultMap;
}
}
1.5 授权文件Controller层代码
服务端授权文件Controller层代码,主要包含授权文件的创建接口和下载接口!
这里Controller层中的统一返回值数据 ResponseResult 就
不copy出来了,根据执行项目中的统一返回数据类型进行修改即可!
package com.selfLicense.licenseselfservice.controller;
import com.selfLicense.licenseselfservice.model.LicenseEntity;
import com.selfLicense.licenseselfservice.service.LicenseCreateService;
import com.selfLicense.licenseselfservice.model.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Map;
/**
* @ClassName : LicenseCreateController
* @Description : 服务端证书生成类控制层
* @Author : AD
*/
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("/license")
public class LicenseCreateController {
/** 配置文件,默认证书生成路径 */
@Value("${license.licensePath}")
private String propertiesLicensePath;
/** 配置文件,默认证书下载地址 属性值:默认值 */
@Value("${springboot.license.server.prefix:http://localhost:8080/license/}")
private String propertiesLicUrl;
@PostMapping("/generate")
public ResponseResult createLicense(@RequestBody LicenseEntity licenseEntity)
{
/* Description: 获取证书内容传输路径(通过会创建证书存储路径)*/
String tempLicensePath = propertiesLicensePath;
if (tempLicensePath == null || "".equals(tempLicensePath.trim()))
{
tempLicensePath = System.getProperty("user.dir");
}
String licDir = tempLicensePath + "/license/"+new SimpleDateFormat("yyyyMMddHHmmss").format(System.currentTimeMillis());
File file = new File(licDir);
if(!file.exists()){
if(!file.mkdirs()){
throw new RuntimeException("创建目录"+licDir+",失败! \n[ 请检查是是否有创建目录的权限或者手动进行创建!]");
}
}
String licenseIoPath = licDir.replace("\\","/") + "/license";
log.info("证书生成路径:"+licenseIoPath);
/* 证书生成操作 */
LicenseCreateService licenseCreateService = new LicenseCreateService(licenseEntity);
Map<String, String> resultMap = licenseCreateService.generateLicense(licenseIoPath);
if ("200".equals(resultMap.get("code"))){
resultMap.put("donwloadUrl",propertiesLicUrl+"download?path="+licenseIoPath);
return ResponseResult.ok(resultMap);
}
return ResponseResult.fail(resultMap.get("message"));
}
@GetMapping("/download")
public void downLoad(@RequestParam(value = "path") String path, HttpServletRequest request, HttpServletResponse response) throws Exception{
File file = new File(path);
if(!file.exists()){
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
InputStream is = new FileInputStream(file);
String fileName = file.getName();
// 设置文件ContentType类型,这样设置,会自动判断下载文件类型
response.setContentType("multipart/form-data");
// 设置编码格式
response.setCharacterEncoding("UTF-8");
// 设置可以识别Html文件
response.setContentType("text/html");
// 设置头中附件文件名的编码
setAttachmentCoding(request, response, fileName);
// 设置文件头:最后一个参数是设置下载文件名
// response.setHeader("Content-Disposition", "attachment;fileName="+file.getName()+".lic");
BufferedInputStream bis = new BufferedInputStream(is);
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024 * 10];
int length ;
while ((length = bis.read(buffer, 0, buffer.length)) != -1) {
os.write(buffer, 0, length);
}
os.close();
bis.close();
is.close();
}
private void setAttachmentCoding(HttpServletRequest request, HttpServletResponse response, String fileName) {
String browser;
try {
browser = request.getHeader("User-Agent");
if (-1 < browser.indexOf("MSIE 6.0") || -1 < browser.indexOf("MSIE 7.0")) {
// IE6, IE7 浏览器
response.addHeader("content-disposition", "attachment;filename="
+ new String(fileName.getBytes(), "ISO8859-1"));
} else if (-1 < browser.indexOf("MSIE 8.0")) {
// IE8
response.addHeader("content-disposition", "attachment;filename="
+ URLEncoder.encode(fileName, "UTF-8"));
} else if (-1 < browser.indexOf("MSIE 9.0")) {
// IE9
response.addHeader("content-disposition", "attachment;filename="
+ URLEncoder.encode(fileName, "UTF-8"));
} else if (-1 < browser.indexOf("Chrome")) {
// 谷歌
response.addHeader("content-disposition",
"attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));
} else if (-1 < browser.indexOf("Safari")) {
// 苹果
response.addHeader("content-disposition", "attachment;filename="
+ new String(fileName.getBytes(), "ISO8859-1"));
} else {
// 火狐或者其他的浏览器
response.addHeader("content-disposition",
"attachment;filename*=UTF-8''" + URLEncoder.encode(fileName, "UTF-8"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
证书生成效果图:
二、授权文件 客户端
2.1 客户端配置相关内容
- application.properties 配置文件内容
#================License Verify 证书验证配置E===============#
#证书存放路径
springboot.license.verify.licensePath= E:/program_myself/spring_parent_project/license/20240805103634/license
#================License Verify 证书验证配置E===============#
- pom 文件中所需要的依赖
所需要的依赖与,服务端所需要的依赖相同!
<!-- 相关工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.43</version>
</dependency>
2.2 客户端工具类
2.2.1 RSAUtil 工具类
RSA 加解密工具类,代码与服务端RSA工具类相同 ( 1.2 节中的代码 )
2.2.2 授权文件工具类
该工具类中的内容与 服务端授权文件工具类( 1.3节 )内容相同,应为关于授权文件的所有操作(服务端、客户端) 都全部统一写在了一个类中的,可以根据实际需求进行拆分处理!
package com.selfLicense.licenseselfclient.utils;
import com.alibaba.fastjson2.JSONObject;
import com.selfLicense.licenseselfclient.model.LicenseEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName : CustomLicenseManager
* @Description : 自定义证书管理操作工具
* @Author : AD
*/
public class CustomLicenseManager {
/** 证书读取参数 */
private LicenseEntity param;
public LicenseEntity getParam() {
return param;
}
public void setParam(LicenseEntity param) {
this.param = param;
}
private static final String XML_CHARSET = "UTF-8";
private static final String LICENSE_BEGIN = "--------------------------------------BEGIN U8CLOUD.LICENSE.1.0--------------------------------------\n";
private static final String LICENSE_PRIVATE_KEY_BEGIN = "\n--------------------------------------U8CLOUD.LICENSE.1.0 PRIVATE KEY--------------------------------------\n";
private static final String LICENSE_SINGN_BEGIN = "\n--------------------------------------U8CLOUD.LICENSE.1.0 SIGN--------------------------------------\n";
private static final String LICENSE_END = "\n--------------------------------------END U8CLOUD.LICENSE.1.0--------------------------------------\n";
private static final String LICENSE_DATA_KEY = "LICENSE_DATA";
private static final String LICENSE_PRIVATE_KEY_KEY = "LICENSE_PRIVATE_KEY";
private static final String LICENSE_SIGN_KEY = "LICENSE_SIGN";
private static Logger logger = LogManager.getLogger(CustomLicenseManager.class);
/**
* Description: 证书创建类
*
* @param licenseEntity 证书生成参数对象
* @return byte[]
*/
public synchronized String serviceCreateLicense(LicenseEntity licenseEntity) throws Exception
{
// 证书创建参数的校验
serviceValidateCreate(licenseEntity);
// 获取公私钥
Map map = RSAUtil.generateRandomToBase64Key();
String publicKey = (String) map.get("publicKey");
String privateKey = (String) map.get("privateKey");
// 证书加密数据
String licenseData = JSONObject.toJSONString(licenseEntity);
String encryptToBase64 = RSAUtil.encryptToBase64(licenseData.getBytes(XML_CHARSET), publicKey);
// 证书加密后加签数据
String sign = RSAUtil.generateSign(encryptToBase64.getBytes(XML_CHARSET), privateKey, RSAUtil.SIGN_ALGORITHM);
// log.info("待加密数据:{} \n加密后数据:{} \n加密数据sign:{}",licenseData,encryptToBase64,sign);
StringBuffer license = new StringBuffer();
license.append(LICENSE_BEGIN).append(encryptToBase64).append(LICENSE_PRIVATE_KEY_BEGIN).append(privateKey).append(LICENSE_SINGN_BEGIN).append(sign).append(LICENSE_END)
.append("产品名称:U8Cloud License V:LICENSE.1.0\n")
.append("证书发行者:"+licenseEntity.getIssuer()+"\n")
.append("证书生成日期:"+licenseEntity.getIssuedTime()+"\n")
.append("证书失效日期:"+licenseEntity.getExpiryTime()+"\n");
return license.toString();
}
/**
* Description: 证书创建时的数据校验
*
* @param licenseEntity
* @return void
*/
public synchronized void serviceValidateCreate(LicenseEntity licenseEntity) throws Exception
{
final Date now = new Date();
final Date notBefore = licenseEntity.getIssuedTime();
final Date notAfter = licenseEntity.getExpiryTime();
if (null != notAfter && now.after(notAfter)){
throw new RuntimeException("证书失效时间不能早于当前时间!");
}
if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
throw new RuntimeException("证书生效时间不能晚于证书失效时间!");
}
final String consumerType = licenseEntity.getCpuSerial();
if (null == consumerType){
throw new RuntimeException("主机序列号不能为空!");
}
}
/**
* Description: 证书生成输出流
*
* @param licenseStr 证书加密后的内容
* @param ioPath 证书生成路径
* @return void
*/
public void serviceOutputLicenseFile(String licenseStr,String ioPath) throws Exception
{
// 输出证书文件
File file = new File(ioPath);
OutputStream fileOutputStream = new FileOutputStream(file);
OutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
bufferedOutputStream.write(licenseStr.getBytes(XML_CHARSET));
bufferedOutputStream.flush();
bufferedOutputStream.close();
fileOutputStream.close();
}
/**
* Description: [client 证书安装操作]读取证书文件内容,并分解各部分内容
* @param licensePath
* @return void
*/
public synchronized LicenseEntity clientLicenseLoad(String licensePath) throws Exception
{
File file = new File(licensePath);
if (!file.exists()){
throw new RuntimeException("证书文件不存在!");
}
InputStream fileIn = new FileInputStream(file);
InputStream bufferFileIn = new BufferedInputStream(fileIn);
String licenseStr = "";
byte[] bytes = new byte[1024];
int len = 0;
while ( (len = bufferFileIn.read(bytes)) != -1)
{
licenseStr += new String(bytes,0,len);
}
bufferFileIn.close();
fileIn.close();
String licenseData = stringSubMethod(licenseStr, LICENSE_BEGIN, LICENSE_PRIVATE_KEY_BEGIN);
String licensePrivateKey = stringSubMethod(licenseStr, LICENSE_PRIVATE_KEY_BEGIN, LICENSE_SINGN_BEGIN);
String licenseSign = stringSubMethod(licenseStr, LICENSE_SINGN_BEGIN, LICENSE_END);
/*
System.out.println("licenseData = " + licenseData);
System.out.println("licensePrivateKey = " + licensePrivateKey);
System.out.println("licenseSign = " + licenseSign);
*/
// 分解获取数据内容
Map<String ,String> map = new HashMap<>();
map.put(LICENSE_DATA_KEY,licenseData);
map.put(LICENSE_PRIVATE_KEY_KEY,licensePrivateKey);
map.put(LICENSE_SIGN_KEY,licenseSign);
// 读取证书文件内容
String jsonString = RSAUtil.decryptByBase64ToJsonString(licenseData,licensePrivateKey);
LicenseEntity licenseEntity = JSONObject.parseObject(jsonString, LicenseEntity.class);
// 验签操作
return licenseEntity;
}
/**
* Description: 证书内容分解方法
*
* @param targetStr 目标
* @param startMarker 开始字符串
* @param endMarker 结束字符串
* @return java.lang.String
*/
public String stringSubMethod(String targetStr,String startMarker,String endMarker)
{
// 加上startMarker的长度以跳过它
// int startIndex = targetStr.indexOf(startMarker)+startMarker.length();
int startIndex = targetStr.lastIndexOf(startMarker)+startMarker.length();
// 从startIndex开始搜索endMarker
int endIndex = targetStr.indexOf(endMarker, startIndex);
if (startIndex != -1 && endIndex != -1) {
return targetStr.substring(startIndex, endIndex);
} else {
throw new RuntimeException("证书内容有误!");
}
}
/**
* Description: 证书相关数据校验方法
* @param expectedCheck 解析出的证书内容
* @return void
*/
public synchronized void clientLicenseValidate(final LicenseEntity expectedCheck) throws Exception
{
// 先判断待安装证书是否已经过期!
final Date now = new Date();
final Date notAfter = expectedCheck.getExpiryTime();
if(now.after(notAfter)){
throw new RuntimeException("【License】 系统证书过期,当前时间已超过证书有效期 -- "+
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(notAfter) +" ");
}
//当前服务器真实的参数信息
LicenseEntity serverCheckModel = AbstractServerInfos.getServer(null).getServerInfos();
if(expectedCheck != null && serverCheckModel != null){
//校验IP地址
if(!checkIpAddress(expectedCheck.getIpAddress(),serverCheckModel.getIpAddress())){
String message = "【License】 系统证书无效,当前服务器的IP没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验Mac地址
if(!checkIpAddress(expectedCheck.getMacAddress(),serverCheckModel.getMacAddress())){
String message = "【License】 系统证书无效,当前服务器的Mac地址没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验主板序列号
if(!checkSerial(expectedCheck.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
String message = "【License】 系统证书无效,当前服务器的主板序列号没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
//校验CPU序列号
if(!checkSerial(expectedCheck.getCpuSerial(),serverCheckModel.getCpuSerial())){
String message = "【License】 系统证书无效,当前服务器的CPU序列号没在授权范围内";
logger.error(message);
throw new RuntimeException(message);
}
}else{
logger.error("【License】 不能获取服务器硬件信息");
throw new RuntimeException("不能获取服务器硬件信息");
}
}
/**
* Description: 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
*
* @param expectedSerial 证书中包含的序列号
* @param serverSerial 当前服务器真实的序列号
* @return boolean
*/
private boolean checkSerial(String expectedSerial,String serverSerial){
if(expectedSerial != null && expectedSerial.trim().length() > 0){
if(serverSerial != null && serverSerial.trim().length() > 0){
if(expectedSerial.equals(serverSerial)){
return true;
}
}
return false;
}else{
return true;
}
}
/**
* Description: 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
*
* @param expectedList 证书中包含的list
* @param serverList 当前服务器真实的list
* @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;
}
}
public static void main(String[] args) throws Exception {
// CustomLicenseManager customLicenseManager = new CustomLicenseManager();
// String licensePath = "E:/LicenseDemo/license/20240802173559/license";
// LicenseEntity licenseMap1 = null;
// try {
// licenseMap1 = customLicenseManager.clientLicenseLoad(licensePath);
// System.out.println("licenseMap1 = " + licenseMap1);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
//
// customLicenseManager.clientLicenseValidate(licenseMap1);
}
}
2.2.3 授权工具类单例模式
该类的作用主要是将 授权文件工具类以单例的方式在客户端进行创建,这样,使用每次需要进行校验证书操作时,都会使用到启动时所初始化的证书实例,将证书的解析数据存入缓存中!
package com.selfLicense.licenseselfclient.utils;
import com.selfLicense.licenseselfclient.model.LicenseEntity;
/**
* @ClassName : LicenseManageHolder
* @Description : 监听器管理处理类 单例创建LicenseManager实例
* @Author : AD
*/
public class LicenseManagerSingleHolder {
private static volatile CustomLicenseManager LICENSE_MANAGER;
/**
* 这里采用单例模式,是应为,在项目启动时创建了该对象同时赋予了 param参数数据,
* 在后续其它功能中需要 调用该对象进行 License校验 verify时,直接从已赋值的单例中进行获取 LicenseManager对象即可,
* 就不需要在通过 公钥别称、访问公钥库的密码、证书生成路径、密钥库存储路径 等参数来解析证书了!
* */
public static CustomLicenseManager getInstance(String licensePath){
if(LICENSE_MANAGER == null){
synchronized (LicenseManagerSingleHolder.class){
if(LICENSE_MANAGER == null){
CustomLicenseManager customLicenseManager = new CustomLicenseManager();
LicenseEntity licenseEntity = null;
try {
licenseEntity = customLicenseManager.clientLicenseLoad(licensePath);
if(licenseEntity == null){
throw new RuntimeException("INIT License FAIL: License file is null!");
}
customLicenseManager.setParam(licenseEntity);
LICENSE_MANAGER = customLicenseManager;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
return LICENSE_MANAGER;
}
}
2.3 授权文件/证书实体类
该实体类与服务端实体类( 1.3 节 )相同,主要用于将授权文件/证书中的加密内容解密之后,解析为该实体类对象,方便后续的验证相关功能!
package com.selfLicense.licenseselfclient.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* @ClassName : LicenseEntity
* @Description : 证书模型实体类数据
* @Author : AD
*/
@Data
public class LicenseEntity {
/** 证书主题 */
private String subject;
/** 证书发行者 */
private String issuer;
/** 证书生效时间 */
@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;
/**
* 可允许的CPU序列号
* */
private String cpuSerial;
/**
* 可允许的主板序列号(硬件序列化?)
* */
private String mainBoardSerial;
private List<String> IpAddress;
private List<String> MacAddress;
/**
* 限制系统中可注册的人数 [自定义]
* */
private Long registerAmount;
/** 注册模块数据 */
private List<String> modules;
}
2.4 客户端硬件信息获取类
该类的主要作用是,获取项目部署服务器的相关机器码数据,将这些机器码数据与证书中提取到的机器码数据,进行对比,从而达到授权的作用。
2.4.1 硬件信息获取抽象类
package com.selfLicense.licenseselfclient.utils;
import com.selfLicense.licenseselfclient.model.LicenseEntity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.stream.Collectors;
/**
* @ClassName : AServerInfos
* @Description : 服务器硬件信息抽象类 -- 模板方法,将通用的方法抽离到父类中
* 获取用户 服务器硬件信息(机器码):为LicenseCheckModel服务提供硬件信息
* @Author : AD
*/
public abstract class AbstractServerInfos {
private static Logger logger = LogManager.getLogger(AbstractServerInfos.class);
/**
* Description: 静态方法,获取系统服务信息实例对象!
*/
public static AbstractServerInfos getServer(String osName) {
if ("".equals(osName) || osName == null) {
osName = System.getProperty("os.name").toLowerCase();
}
AbstractServerInfos abstractServerInfos;
//根据不同操作系统类型选择不同的数据获取方法
if (osName.startsWith("windows")) {
abstractServerInfos = new WindowsServerInfos();
} else if (osName.startsWith("linux")) {
abstractServerInfos = new LinuxServerInfos();
} else {
//其他服务器类型
abstractServerInfos = new LinuxServerInfos();
}
return abstractServerInfos;
}
/**
* Description: 组装需要额外校验的License参数(相关机器码)
*/
public LicenseEntity getServerInfos(){
LicenseEntity licenseCheckModel = new LicenseEntity();
try {
licenseCheckModel.setIpAddress(this.getIpAddress());
licenseCheckModel.setMacAddress(this.getMacAddress());
licenseCheckModel.setCpuSerial(this.getCPUSerial());
licenseCheckModel.setMainBoardSerial(this.getMainBoardSerial());
}catch (Exception e){
logger.error("获取服务器硬件信息失败!", e);
}
return licenseCheckModel;
}
/**
* 获取IP地址信息
* */
protected List<String> getIpAddress() throws Exception {
/** 获取所有网络接口 */
List<InetAddress> inetAddresses = getLocalAllInetAddress();
if (inetAddresses!= null && inetAddresses.size() > 0) {
return inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
}
return null;
}
/**
* 获取Mac地址
* 网络设备接口的物理地址,通常固化在网卡(Network Interface Card,NIC)的EEPROM(电可擦可编程只读存储器)中,具有全球唯一性。)
* */
protected List<String> getMacAddress() throws Exception {
/** 获取所有网络接口 */
List<InetAddress> inetAddresses = getLocalAllInetAddress();
if (inetAddresses != null && inetAddresses .size()>0) {
return inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
}
return null;
}
/**
* 获取CPU序列号
*/
protected abstract String getCPUSerial() throws Exception;
/**
* 获取主板序列号
*/
protected abstract String getMainBoardSerial() throws Exception;
/**
* Description: 获取当前服务器所有符合条件的网络地址InetAddress
*/
protected List<InetAddress> getLocalAllInetAddress() throws Exception {
List<InetAddress> result = new ArrayList<>(4);
// 遍历所有的网络接口
for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
NetworkInterface ni = (NetworkInterface) networkInterfaces.nextElement();
// 在所有的接口下再遍历IP
for (Enumeration addresses = ni.getInetAddresses(); addresses.hasMoreElements(); ) {
InetAddress address = (InetAddress) addresses.nextElement();
//排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
if (!address.isLoopbackAddress()
/*&& !inetAddr.isSiteLocalAddress()*/
&& !address.isLinkLocalAddress() && !address.isMulticastAddress()) {
result.add(address);
}
}
}
return result;
}
/**
* Description: 获取某个网络地址对应的Mac地址
*/
protected String getMacByInetAddress(InetAddress inetAddr) {
try {
byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
if (i != 0) {
stringBuilder.append("-");
}
/** 将十六进制byte转化为字符串 */
String temp = Integer.toHexString(mac[i] & 0xff);
if (temp.length() == 1) {
stringBuilder.append("0").append(temp);
} else {
stringBuilder.append(temp);
}
}
return stringBuilder.toString().toUpperCase();
} catch (SocketException e) {
e.printStackTrace();
}
return null;
}
}
2.4.2 Windows系统的实现类
package com.selfLicense.licenseselfclient.utils;
import java.util.Scanner;
/**
* @ClassName : WindowsServerInfos
* @Description : 用于获取客户Windows服务器的基本信息
* @Author : AD
*/
public class WindowsServerInfos extends AbstractServerInfos {
@Override
protected 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;
}
@Override
protected 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.4.3 Linux系统的实现类
package com.selfLicense.licenseselfclient.utils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* @ClassName : LinuxServerInfos
* @Description : 用于获取客户Linux服务器的基本信息
* @Author : AD
*/
public class LinuxServerInfos extends AbstractServerInfos {
@Override
protected 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 (line != null || line.length() != 0){
serialNumber = line;
}
reader.close();
return serialNumber;
}
@Override
protected String getMainBoardSerial() 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 (line != null || line.length() != 0){
serialNumber = line;
}
reader.close();
return serialNumber;
}
}
2.5 客户端授权Service层
客户端Service层中主要包含接口有: 证书安装接口、证书校验接口
package com.selfLicense.licenseselfclient.service;
import com.selfLicense.licenseselfclient.model.LicenseEntity;
import com.selfLicense.licenseselfclient.utils.CustomLicenseManager;
import com.selfLicense.licenseselfclient.utils.LicenseManagerSingleHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
/**
* @ClassName : licenseVerifyService
* @Description : 证书校验Service服务层
* @Author : AD
*/
public class LicenseVerifyService {
private static final Logger logger = LoggerFactory.getLogger(LicenseVerifyService.class);
/**
* Description: 【Client客户端】 安装证书操作
*
* @return de.schlichtherle.license.LicenseContent
*/
public synchronized boolean install(String licensePath){
boolean result = false;
DateFormat formatType = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
/** 创建License证书管理器对象 */
// 采用单例模式进行操作
CustomLicenseManager licenseManager = LicenseManagerSingleHolder.getInstance(licensePath);
/** 5、开始安装 */
LicenseEntity licenseEntity = licenseManager.getParam();
licenseManager.clientLicenseValidate(licenseEntity);
logger.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",formatType.format(licenseEntity.getIssuedTime()),formatType.format(licenseEntity.getExpiryTime())));
result = true;
} catch (RuntimeException contentExc){
String message = contentExc.getMessage();
logger.error(message);
} catch (Exception e){
logger.error(e.getMessage(),e);
}
return result;
}
/**
* 【Client客户端】校验License证书
* @return boolean
*/
public boolean verify(){
CustomLicenseManager licenseManager = LicenseManagerSingleHolder.getInstance(null);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//2. 校验证书
try {
LicenseEntity param = licenseManager.getParam();
licenseManager.clientLicenseValidate(param);
logger.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(param.getIssuedTime()),format.format(param.getExpiryTime())));
return true;
}catch (Exception e){
logger.error("证书校验失败!",e);
return false;
}
}
}
2.6 客户端校验时机
2.6.1 证书校验监听器示例
package com.selfLicense.licenseselfclient.listener;
import com.selfLicense.licenseselfclient.service.LicenseVerifyService;
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.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.ResourceUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* @ClassName : LicenseVerifyInstallListener
* @Description : 项目启动时安装证书&定时检测lic变化,自动更替lic [client端 项目证书安装监听器(不做检测)]
* @Author : AD
*/
@Component
public class LicenseVerifyInstallListener implements ApplicationListener<ContextRefreshedEvent> {
private static Logger logger = LoggerFactory.getLogger(LicenseVerifyInstallListener.class);
/**
* 证书生成路径
*/
@Value("${springboot.license.verify.licensePath}")
private String licensePath;
/**文件唯一身份标识 == 相当于人类的指纹一样*/
private static String md5 = "";
private static boolean isLoad = false;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
/* 是从一个事件对象中获取其应用程序上下文的父上下文 */
ApplicationContext context = event.getApplicationContext().getParent();
if(context == null){
if (licensePath!= null && licensePath.trim().length() > 0){
install();
try{
String readMd5 = getMd5(licensePath);
isLoad = true;
if(LicenseVerifyInstallListener.md5 == null || "".equals(LicenseVerifyInstallListener.md5)){
LicenseVerifyInstallListener.md5 =readMd5;
logger.info("【LicenseLicense】 license.lic 授权文件 MD5值:"+readMd5);
}
}catch (Exception e){
}
}
//证书路径为null的情况处理
}
//上下文为null的情况处理:
}
private void install() {
logger.info("++++++++ 开始安装证书 ++++++++");
LicenseVerifyService licenseVerifyManager = new LicenseVerifyService();
/** 走定义校验证书并安装 */
boolean install = licenseVerifyManager.install(licensePath);
if (install) {
logger.info("++++++++ 证书安装成功 ++++++++");
}else {
logger.info("++++++++ 证书安装失败 ++++++++");
}
}
/** 5秒检测一次,不能太快也不能太慢 */
@Scheduled(cron = "0 0/1 * * * ?")
protected void timer() throws Exception {
logger.info("【LicenseLicense】定时检测lic是否变化 isLoad:{} \t md5:{}",LicenseVerifyInstallListener.isLoad,LicenseVerifyInstallListener.md5);
if(!isLoad){
return;
}
String readMd5 = getMd5(licensePath);
// 不相等,说明lic变化了进行重新安装!
logger.info("【LicenseLicense】license.lic文件是否发生变化,new md5:{} \t old md5:{} ",readMd5,LicenseVerifyInstallListener.md5);
if(!readMd5.equals(LicenseVerifyInstallListener.md5)){
install();
LicenseVerifyInstallListener.md5 = readMd5;
}
}
/**
* 获取文件的MD5值
*/
public String getMd5(String filePath) throws Exception {
File file;
String md5 = "";
try {
/* 获取文件对象 */
file = ResourceUtils.getFile(filePath);
if (file.exists()) {
FileInputStream is = new FileInputStream(file);
byte[] data = new byte[is.available()];
is.read(data);
/** 计算字节数组的MD5值 */
md5 = DigestUtils.md5DigestAsHex(data);
is.close();
}
} catch (FileNotFoundException e) {
}
return md5;
}
}
2.6.2 证书校验拦截器
package com.selfLicense.licenseselfclient.interceptor;
import com.alibaba.fastjson2.JSONObject;
import com.selfLicense.licenseselfclient.service.LicenseVerifyService;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName : LicenseCheckInterceptor
* @Description : 证书校验拦截器:请求拦截器 拦截相关请求验证证书!
* @Author : AD
*/
@Component
public class LicenseCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
LicenseVerifyService licenseVerify = new LicenseVerifyService();
//校验证书是否有效
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.selfLicense.licenseselfclient.config;
import com.selfLicense.licenseselfclient.interceptor.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 {
@Override
public void addInterceptors(InterceptorRegistry registration){
registration.addInterceptor(new LicenseCheckInterceptor()).addPathPatterns("/check/login/**");
}
}
2.6 客户端Controller层代码
2.6.1 获取机器码Controller层
package com.selfLicense.licenseselfclient.controller;
/**
* @ClassName : HardWareInfoController
* @Description :
* @Author : AD
*/
import com.selfLicense.licenseselfclient.model.LicenseEntity;
import com.selfLicense.licenseselfclient.model.ResponseResult;
import com.selfLicense.licenseselfclient.utils.AbstractServerInfos;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
/**
* @ClassName : HardWareInfoController
* @Description : 硬件信息获取API控制层
* @Author : AD
*/
@CrossOrigin
@RestController
@Slf4j
@RequestMapping("/license")
public class HardWareInfoController {
@GetMapping(value = "/getClientInfo",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseResult getClientHardWareInfo(@RequestParam(value = "osName",required = false) String osName){
AbstractServerInfos server = AbstractServerInfos.getServer(osName);
LicenseEntity serverInfos = server.getServerInfos();
return ResponseResult.ok("获取机器码成功!", serverInfos);
}
}
2.6.2 登录校验证书Controller
package com.selfLicense.licenseselfclient.controller;
import com.selfLicense.licenseselfclient.model.ResponseResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName : LoginTestLicenseController
* @Description :模拟登录check检测证书
* @Author : AD
*/
@RestController
@RequestMapping("/check")
public class LoginTestLicenseController {
@GetMapping("/login/licenseVerify")
public ResponseResult check(String username, String password) {
//模拟访问路径,触发证书校验功能
return ResponseResult.ok("success", "登录成功!");
}
}