License授权是一种常见的商业模式,一般用于在客户端部署项目后进行使用人员或功能限制,也常用于软件的试用场景。
主要实现思路就是在服务端生成密钥对及证书,在客户端启动或访问过程中进行验证。
本文实现的是通过IP地址、MAC地址、CPU序列号、主板序列号(或BIOS序列号)等信息生成license证书,同时可以设置生效及失效时间,控制项目到期后不可用。
作为记录的同时也希望能帮助到需要的朋友。
新建文件夹,并在文件夹下打开cmd命令行,执行如下命令:
# 生成密钥对 keytool -genkey -keysize 1024 -keyalg DSA -validity 3650 -alias "privateKey" -keystore "privateKeys.keystore" -storepass "xxxxxx" -keypass "xxxxxxx" -dname "CN=cnhqd, OU=cnhqd, O=cnhqd, L=XA, ST=SX, C=CN" keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "xxxxxx" -file "certfile.cer" keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "xxxxxx"
参数说明:
keysize 密钥长度 keyalg 加密方式 validity 私钥的有效期(单位:天) alias 私钥别称 keystore 指定私钥库文件的名称 (生成在当前目录) storepass 指定私钥库的密码 (keystore 文件存储密码) keypass 指定别名条目的密码 (私钥加解密密码) dname 证书个人信息 CN 为你的姓名 OU 为你的组织单位名称 O 为你的组织名称 L 为你所在的城市名称 ST 为你所在的省份名称 C 为你的国家名称 或 区号
上述命令执行完成后会在当前目录生成三个文件:
这里我们新建了一个springboot + maven的项目,通过swagger生成证书。
整体结构如下:
使用的软件版本:
软件名称 | 版本 |
---|---|
springboot | 2.7.13 |
jdk | 1.8 |
maven | 3.5 |
swagger | 2.9.2 |
knife4j | 2.0.2 |
truelicense | 1.33 |
关键类及功能说明:
1、AbstractServerInfos: 用于获取服务端信息,包含IP地址、mac地址、cpu序列号、主板或BIOS序列号等信息,具体实现分windows和linux
2、LicenseCreator: 生成证书的主类
3、AdditionInfo: 需要额外校验的信息实体类,属性可删减可扩展
pom.xml
4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.13 com.zjtx.tech.license license-server 1.0.0 license-server license-server 1.8 org.springframework.boot spring-boot-starter-web org.apache.commons commons-lang3 3.7 io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 com.github.xiaoymin knife4j-spring-boot-starter 2.0.2 de.schlichtherle.truelicense truelicense-core 1.33 org.projectlombok lombok license-service org.apache.maven.plugins maven-surefire-plugin true org.apache.tomcat.maven tomcat7-maven-plugin 2.2 8090 / UTF-8
package com.zjtx.tech.license.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LicenseServerApplication { public static void main(String[] args) { SpringApplication.run(LicenseServerApplication.class, args); } }
SwaggerConfig.java
package com.zjtx.tech.license.server.config; import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; @Configuration @EnableSwagger2 @EnableKnife4j public class SwaggerConfig implements InitializingBean { @Value("${swagger.host:{null}}") private String host; @Value("${swagger.title:{null}}") private String title; @Value("${swagger.version:{null}}") private String version; @Value("${swagger.description:{null}}") private String description; @Value("${swagger.license:{null}}") private String license; @Value("${swagger.licenseUrl:{null}}") private String licenseUrl; @Value("${swagger.contact.name:{null}}") private String contactName; @Value("${swagger.contact.email:{null}}") private String contactEmail; @Value("${swagger.contact.url:{null}}") private String contactUrl; @Value("${swagger.package:{null}}") private String packagePath; //配置swagger的Docket的bean实例 @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(packagePath)) .build().host(host); } private ApiInfo apiInfo() { //作者信息 Contact contact = new Contact(contactName, contactUrl, contactEmail); return new ApiInfo(title,description,version, contactUrl,contact,license,licenseUrl,new ArrayList<>()); } @Override public void afterPropertiesSet() { System.out.println("SwaggerConfig==================afterPropertiesSet"); } }
LicenseCreatorParam.java
package com.zjtx.tech.license.server.entity; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @ApiModel("生成证书实体类") public class LicenseCreatorParam implements Serializable { private static final long serialVersionUID = 2832129012982731724L; @ApiModelProperty("证书subject") private String subject; @ApiModelProperty("密钥别称") private String privateAlias; @ApiModelProperty("密钥密码") private String keyPass; @ApiModelProperty("访问秘钥库的密码") private String storePass; @ApiModelProperty("证书生成路径") private String licensePath; @ApiModelProperty("密钥库存储路径") private String privateKeysStorePath; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ApiModelProperty("证书生效时间") private Date issuedTime = new Date(); @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @ApiModelProperty("证书失效时间") private Date expireTime; @ApiModelProperty("用户类型") private String consumerType = "user"; @ApiModelProperty("用户数量") private Integer consumerAmount = 1; @ApiModelProperty("描述信息") private String description = ""; @ApiModelProperty("额外的服务器硬件校验信息") private AdditionInfo additionInfo; }
AdditionInfo.java
package com.zjtx.tech.license.server.entity; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.List; @Data @ApiModel("服务器硬件信息实体类") public class AdditionInfo implements Serializable { private static final long serialVersionUID = -2314678441082223148L; @ApiModelProperty("可被允许的IP地址") private ListipAddress; @ApiModelProperty("可被允许的MAC地址") private List macAddress; @ApiModelProperty("可被允许的CPU序列号") private String cpuSerial; @ApiModelProperty("可被允许的主板序列号") private String mainBoardSerial; }
AbstractServerInfos.java
package com.zjtx.tech.license.server.serverinfo; import com.zjtx.tech.license.server.entity.AdditionInfo; 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; import java.util.stream.Collectors; @Slf4j public abstract class AbstractServerInfos { public AdditionInfo getServerInfos() { AdditionInfo result = new AdditionInfo(); 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; } protected ListgetIpAddress() throws Exception{ List result = null; //获取所有网络接口 List inetAddresses = getLocalAllInetAddress(); if(inetAddresses != null && inetAddresses.size() > 0){ result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList()); } return result; } protected List getMacAddress() throws Exception{ List result = null; //1. 获取所有网络接口 List inetAddresses = getLocalAllInetAddress(); if(inetAddresses != null && inetAddresses.size() > 0){ //2. 获取所有网络接口的Mac地址 result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList()); } return result; } protected abstract String getCPUSerial() throws Exception; protected abstract String getMainBoardSerial() throws Exception; protected List getLocalAllInetAddress() throws Exception { List result = new ArrayList<>(4); // 遍历所有的网络接口 for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) { NetworkInterface iface = networkInterfaces.nextElement(); // 在所有的接口下再遍历IP for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) { InetAddress inetAddr = inetAddresses.nextElement(); //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址 if (!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/ && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()) { result.add(inetAddr); } } } return result; } protected String getMacByInetAddress(InetAddress inetAddr) { try { byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < mac.length; i++) { if (i != 0) { sb.append("-"); } //将十六进制byte转化为字符串 String temp = Integer.toHexString(mac[i] & 0xff); if (temp.length() == 1) { sb.append("0").append(temp); } else { sb.append(temp); } } return sb.toString().toUpperCase(); } catch (SocketException e) { e.printStackTrace(); } return null; } }
WindowsServerInfos.java
package com.zjtx.tech.license.server.serverinfo; import org.apache.commons.lang3.StringUtils; import java.util.Scanner; 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 = getSysInfo("wmic baseboard get serialnumber"); if(StringUtils.isEmpty(serialNumber)) { //如果获取不到主板序列号就获取bios序列号 serialNumber = getSysInfo("wmic bios get serialnumber"); } return serialNumber; } /** * 通过命令行获取系统信息 * @param command 查询信息的命令 * @return 系统对应数据 * @throws Exception 异常信息 */ private String getSysInfo(String command) throws Exception { String serialNumber = ""; Process process = Runtime.getRuntime().exec(command); 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; } }
LinuxServerInfos.java
package com.zjtx.tech.license.server.serverinfo; import org.apache.commons.lang3.StringUtils; import java.io.BufferedReader; import java.io.InputStreamReader; public class LinuxServerInfos extends AbstractServerInfos { @Override protected 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; } @Override protected 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; } }
CustomKeyStoreParam.java
package com.zjtx.tech.license.server.serverinfo; import de.schlichtherle.license.AbstractKeyStoreParam; import java.io.*; import java.nio.file.Files; public class CustomKeyStoreParam extends AbstractKeyStoreParam { /** * 公钥/私钥在磁盘上的存储路径 */ private final String storePath; private final String alias; private final String storePwd; private final String keyPwd; public CustomKeyStoreParam(Class clazz, String resource, String alias, String storePwd, String keyPwd) { super(clazz, resource); this.storePath = resource; this.alias = alias; this.storePwd = storePwd; this.keyPwd = keyPwd; } @Override public String getAlias() { return alias; } @Override public String getStorePwd() { return storePwd; } @Override public String getKeyPwd() { return keyPwd; } @Override public InputStream getStream() throws IOException { return Files.newInputStream(new File(storePath).toPath()); } }
LicenseCreator.java
package com.zjtx.tech.license.server.util; import com.zjtx.tech.license.server.entity.LicenseCreatorParam; import com.zjtx.tech.license.server.serverinfo.CustomKeyStoreParam; import de.schlichtherle.license.*; import lombok.Data; import lombok.extern.slf4j.Slf4j; import javax.security.auth.x500.X500Principal; import java.io.File; import java.text.MessageFormat; import java.util.prefs.Preferences; @Data @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 LicenseManager(initLicenseParam()); LicenseContent licenseContent = initLicenseContent(); licenseManager.store(licenseContent, new File(param.getLicensePath())); return true; } catch (Exception e) { log.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()); return new DefaultLicenseParam(param.getSubject(), preferences, privateStoreParam, cipherParam); } /** * 设置证书生成正文信息 * * @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.getExpireTime()); licenseContent.setConsumerType(param.getConsumerType()); licenseContent.setConsumerAmount(param.getConsumerAmount()); licenseContent.setInfo(param.getDescription()); //扩展校验服务器硬件信息 licenseContent.setExtra(param.getAdditionInfo()); return licenseContent; } }
LicenseController.java
package com.zjtx.tech.license.server.controller; import com.zjtx.tech.license.server.entity.AdditionInfo; import com.zjtx.tech.license.server.entity.LicenseCreatorParam; import com.zjtx.tech.license.server.util.LicenseCreator; import com.zjtx.tech.license.server.serverinfo.AbstractServerInfos; import com.zjtx.tech.license.server.serverinfo.LinuxServerInfos; import com.zjtx.tech.license.server.serverinfo.WindowsServerInfos; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; @RestController @Api(tags = "license证书") @RequestMapping("/license") public class LicenseController { /** * 证书生成路径 */ @Value("${license.licensePath}") private String licensePath; @ApiOperation("获取服务器硬件信息") @GetMapping("/getServerInfos/{osName}") public AdditionInfo getServerInfos(@PathVariable String osName) { //操作系统类型 if (StringUtils.isBlank(osName)) { osName = System.getProperty("os.name"); } osName = osName.toLowerCase(); AbstractServerInfos abstractServerInfos; //根据不同操作系统类型选择不同的数据获取方法 if (osName.startsWith("windows")) { abstractServerInfos = new WindowsServerInfos(); } else if (osName.startsWith("linux")) { abstractServerInfos = new LinuxServerInfos(); } else {//其他服务器类型 abstractServerInfos = new LinuxServerInfos(); } return abstractServerInfos.getServerInfos(); } @ApiOperation("生成证书") @PostMapping("/generateLicense") public MapgenerateLicense(@RequestBody LicenseCreatorParam param) { Map 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; } }
application.yml
server: port: 8090 servlet: context-path: /api license: # 生成证书的目录 根据实际情况调整 licensePath: D:/study/license/licensefile/license.lic ##swagger swagger: package: com.zjtx.tech.license.server.controller host: 127.0.0.1:8090 title: License-Server服务端 version: v1.0 description: API接口文档 license: License licenseUrl: https://blog.zjtx.vip contact: name: 泽济天下 url: https://blog.zjtx.vip email: xxxx@qq.com logging: level: com.zjtx.tech.license: debug #解决高版本SpringBoot整合Swagger 启动报错Failed to start bean ‘documentationPluginsBootstrapper‘ 问题 #参考https://blog.csdn.net/weixin_39792935/article/details/122215625 spring: mvc: path match: matching-strategy: ant_path_matcher
浏览器访问http://localhost:8090/api/doc.html
截图中的参数配置:
{ "consumerAmount": 1, "consumerType": "user", "description": "授权给xxx的证书,时长1年", "expireTime": "2024-11-15 23:59:59", "issuedTime": "2023-11-15 00:00:00", "keyPass": "cnhqd@20231114", "additionInfo": { //需要把这部分内容替换成上一步获取到的服务器信息 "ipAddress": [ "172.16.60.104" ], "macAddress": [ "6C-4B-90-C6-01-AB" ], "cpuSerial": "BFEBFBFF000906EA", "mainBoardSerial": "P509PEZ0" }, "licensePath": "D:/study/license/licensefile/license.lic", "privateAlias": "privateKey", "privateKeysStorePath": "D:/study/license/licensefile/privateKeys.keystore", "storePass": "xxxxxx", "subject": "license_xxx" }
参数配置完成后点击发送会在licensePath对应目录下生成lic后缀的文件,就是需要放在客户端项目目录中的文件。
参数配置说明:
至此,公钥和客户端证书就生成完成了。
说明: 客户端中使用的框架及版本与服务端一致,代码中部分类与服务端一致(见下图),就不再列出代码。
整体项目结构如下:
pom.xml
4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.13 com.zjtx.tech.license license-client 1.0.0 license-client license-client 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 com.github.xiaoymin knife4j-spring-boot-starter 2.0.2 de.schlichtherle.truelicense truelicense-core 1.33 org.apache.commons commons-lang3 3.7 org.projectlombok lombok com.github.ben-manes.caffeine caffeine 2.9.3
LicenseClientApplication.java
package com.zjtx.tech.license.client; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class LicenseClientApplication { public static void main(String[] args) { SpringApplication.run(LicenseClientApplication.class, args); } }
CustomLicenseManager.java
package com.zjtx.tech.license.client.util; import com.zjtx.tech.license.client.serverinfo.AbstractServerInfos; import com.zjtx.tech.license.client.serverinfo.LinuxServerInfos; import com.zjtx.tech.license.client.serverinfo.WindowsServerInfos; import com.zjtx.tech.license.server.entity.AdditionInfo; import de.schlichtherle.license.*; import de.schlichtherle.xml.GenericCertificate; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.util.CollectionUtils; import java.beans.XMLDecoder; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @Slf4j public class CustomLicenseManager extends LicenseManager { //XML编码 private static final String XML_CHARSET = "UTF-8"; //默认buffer size private static final int DEFAULT_BUFFER_SIZE = 8 * 1024; public CustomLicenseManager(LicenseParam param) { super(param); } @Override protected synchronized byte[] create(LicenseContent content,LicenseNotary notary) throws Exception { initialize(content); this.validateCreate(content); final GenericCertificate certificate = notary.sign(content); return getPrivacyGuard().cert2key(certificate); } @Override protected 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; } @Override protected synchronized LicenseContent verify(final LicenseNotary notary) throws Exception { // Load license key from preferences, final byte[] key = getLicenseKey(); if (null == key) { throw new NoLicenseInstalledException(getLicenseParam().getSubject()); } GenericCertificate certificate = getPrivacyGuard().key2cert(key); notary.verify(certificate); final LicenseContent content = (LicenseContent) this.load(certificate.getEncoded()); this.validate(content); setCertificate(certificate); return content; } protected synchronized void validateCreate(final LicenseContent content) throws LicenseContentException { 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("用户类型不能为空"); } } @Override protected synchronized void validate(final LicenseContent content) throws LicenseContentException { //1. 首先调用父类的validate方法 super.validate(content); //2. 然后校验自定义的License参数 //License中可被允许的参数信息 AdditionInfo expectedCheckModel = (AdditionInfo) content.getExtra(); //当前服务器真实的参数信息 AdditionInfo serverCheckModel = getServerInfos(); //有一个对象为空 直接抛异常 if (expectedCheckModel == null || serverCheckModel == null) { throw new LicenseContentException("不能获取服务器硬件信息"); } //校验IP地址 if (ipAddressInvalid(expectedCheckModel.getIpAddress(), serverCheckModel.getIpAddress())) { throw new LicenseContentException("当前服务器的IP没在授权范围内"); } //校验Mac地址 if (ipAddressInvalid(expectedCheckModel.getMacAddress(), serverCheckModel.getMacAddress())) { throw new LicenseContentException("当前服务器的Mac地址没在授权范围内"); } //校验主板序列号 if (serialInvalid(expectedCheckModel.getMainBoardSerial(), serverCheckModel.getMainBoardSerial())) { throw new LicenseContentException("当前服务器的主板序列号没在授权范围内"); } //校验CPU序列号 if (serialInvalid(expectedCheckModel.getCpuSerial(), serverCheckModel.getCpuSerial())) { throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内"); } } 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_BUFFER_SIZE), 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); } } return null; } private AdditionInfo getServerInfos() { //操作系统类型 String 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.getServerInfos(); } private boolean ipAddressInvalid(ListexpectedList, List serverList) { if(CollectionUtils.isEmpty(expectedList) || CollectionUtils.isEmpty(serverList)) { return true; } for (String expected : expectedList) { if (serverList.contains(expected.trim())) { return false; } } return true; } private boolean serialInvalid(String expectedSerial, String serverSerial) { //为空或者不相等 就认为无效 return StringUtils.isBlank(expectedSerial) || StringUtils.isBlank(serverSerial) || !expectedSerial.equals(serverSerial); } }
LicenseVerify.java 证书安装与验证
package com.zjtx.tech.license.client.util; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.zjtx.tech.license.server.entity.LicenseVerifyParam; import com.zjtx.tech.license.client.serverinfo.CustomKeyStoreParam; import de.schlichtherle.license.*; import lombok.extern.slf4j.Slf4j; import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.concurrent.TimeUnit; import java.util.prefs.Preferences; @Slf4j public class LicenseVerify { private static final String CACHE_KEY_LICENSE_CHECK_RESULT = "license_check_result"; private static final Cache
LicenseManagerHolder.java 一个单例实现类
package com.zjtx.tech.license.client.util; import de.schlichtherle.license.LicenseManager; import de.schlichtherle.license.LicenseParam; 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; } }
FileUtils.java 文件及文件流获取工具类
package com.zjtx.tech.license.client.util; import org.apache.commons.lang3.StringUtils; import org.springframework.core.io.ClassPathResource; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; public class FileUtils { private static final String CLASSPATH_RESOURCE_PREFIX = "classpath:"; public static File getFile(String path) throws IOException { if(StringUtils.isEmpty(path)) { throw new FileNotFoundException("未找到文件路径"); } if(path.startsWith(CLASSPATH_RESOURCE_PREFIX)) { return new ClassPathResource(path.replaceAll(CLASSPATH_RESOURCE_PREFIX, "")).getFile(); } return new File(path); } public static InputStream getInputStream(String path) throws IOException { if(StringUtils.isEmpty(path)) { throw new FileNotFoundException("未找到文件路径"); } if(path.startsWith(CLASSPATH_RESOURCE_PREFIX)) { return new ClassPathResource(path.replaceAll(CLASSPATH_RESOURCE_PREFIX, "")).getInputStream(); } return Files.newInputStream(Paths.get(path)); } }
LicenseCheckListener.java 用于应用启动后安装证书
package com.zjtx.tech.license.client.config; import com.zjtx.tech.license.client.util.LicenseVerify; import com.zjtx.tech.license.server.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{ /** * 证书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; @Override public void onApplicationEvent(ContextRefreshedEvent event) { //root application context 没有parent ApplicationContext 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("++++++++ 证书安装结束 ++++++++"); } } } }
LicenseCheckInterceptor.java 证书校验拦截器 在controller之前执行
package com.zjtx.tech.license.client.config; import com.alibaba.fastjson.JSON; import com.zjtx.tech.license.client.util.LicenseVerify; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; @Slf4j @Component public class LicenseCheckInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("进入拦截器,验证证书可使用性"); LicenseVerify licenseVerify = new LicenseVerify(); //校验证书是否有效 boolean verifyResult = licenseVerify.verify(); if (verifyResult) { log.info("验证成功,证书可用"); return true; } else { log.info("验证失败,证书无效"); response.setContentType("application/json;charset=utf-8"); Mapresult = new HashMap<>(1); result.put("result", "您的证书无效,请核查服务器是否取得授权或重新申请证书!"); response.getWriter().write(JSON.toJSONString(result)); return false; } } }
InterceptorConfig.java 安全配置类, 不同版本可能写法不同,自行修改即可
package com.zjtx.tech.license.client.config; 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; @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Resource private LicenseCheckInterceptor licenseCheckInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //添加要拦截的url registry.addInterceptor(licenseCheckInterceptor) // 拦截的路径 .addPathPatterns("/**"); } }
LoginController.java
package com.zjtx.tech.license.client.controller; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @Api(tags = "测试") @RequestMapping("/text") public class LoginController { @ApiOperation("登录") @GetMapping("/login") public Maptest() { Map result = new HashMap<>(1); result.put("code", 200); result.put("mes", "登录成功"); return result; } }
server: port: 8099 servlet: context-path: /api license: basePath: D:/study/license/licensefile subject: license_cnhqd publicAlias: publicCert storePass: cnhqd@20231114 licensePath: classpath:license/license.lic publicKeysStorePath: classpath:license/publicCerts.keystore uploadPath: classpath:license/licensefile/ ##swagger swagger: package: com.zjtx.tech.license.client.controller host: 127.0.0.1:8090 title: License-Client客户端 version: v1.0 description: API接口文档 license: License licenseUrl: https://blog.zjtx.vip contact: name: 泽济天下 url: https://blog.zjtx.vip email: xxxx@qq.com logging: level: com.zjtx.tech.license: debug #解决高版本SpringBoot整合Swagger 启动报错Failed to start bean ‘documentationPluginsBootstrapper‘ 问题 #参考https://blog.csdn.net/weixin_39792935/article/details/122215625 spring: mvc: path match: matching-strategy: ant_path_matcher
注意: 不要忘了把lic文件和publicKeys.keystore文件放到resources/license目录下,这个非常重要。
启动项目,可以看到如下日志输出即为安装完成并正常启动:
浏览器访问http://localhost:8099/doc.html,可以看到如下日志输出:
可以看到可以正常验证通过,同时在上述代码里加入了缓存用于保存证书认证结果,有效期设置的是5分钟。
如果证书过期,访问接口会提示证书无效或过期的异常, 可自行测试。
异常信息:
2023-11-17 08:42:31.487 ERROR 12164 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.context.support.DefaultLifecycleProcessor.access0(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.28.jar:5.3.28] at java.lang.Iterable.forEach(Iterable.java:75) ~[na:1.8.0_181] at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:937) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.28.jar:5.3.28] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.13.jar:2.7.13] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) [spring-boot-2.7.13.jar:2.7.13] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) [spring-boot-2.7.13.jar:2.7.13] at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) [spring-boot-2.7.13.jar:2.7.13] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) [spring-boot-2.7.13.jar:2.7.13] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) [spring-boot-2.7.13.jar:2.7.13] at com.zjtx.tech.license.client.LicenseClientApplication.main(LicenseClientApplication.java:10) [classes/:na] Caused by: java.lang.NullPointerException: null at springfox.documentation.spi.service.contexts.Orderings.compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null] at springfox.documentation.spi.service.contexts.Orderings.compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null] at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na] at java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:1.8.0_181] at java.util.TimSort.sort(TimSort.java:220) ~[na:1.8.0_181] at java.util.Arrays.sort(Arrays.java:1438) ~[na:1.8.0_181] at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null] at com.google.common.collect.Iterators.transform(Iterators.java:750) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na] at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null] at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.28.jar:5.3.28] ... 14 common frames omitted
解决方案: application.yml文件中添加如下配置即可:
#解决高版本SpringBoot整合Swagger 启动报错Failed to start bean ‘documentationPluginsBootstrapper‘ 问题 #参考https://blog.csdn.net/weixin_39792935/article/details/122215625 spring: mvc: path match: matching-strategy: ant_path_matcher
证书安装失败问题
异常信息:
2023-11-17 08:44:25.551 INFO 13640 --- [ main] s.d.s.w.s.ApiListingReferenceScanner : Scanning for api listing references 2023-11-17 08:44:25.691 INFO 13640 --- [ main] c.z.t.l.c.config.LicenseCheckListener : ++++++++ 开始安装证书 ++++++++ java.lang.ClassNotFoundException: com/zjtx/tech/license/server/entity/AdditionInfo Continuing ... java.lang.NullPointerException: target should not be null Continuing ... java.lang.IllegalStateException: The outer element does not return value Continuing ... java.lang.IllegalStateException: The outer element does not return value Continuing ... java.lang.IllegalStateException: The outer element does not return value Continuing ... java.lang.IllegalStateException: The outer element does not return value Continuing ... 2023-11-17 08:44:28.578 ERROR 13640 --- [ main] c.z.t.license.client.util.LicenseVerify : 证书安装失败!
解决方案: 把AdditionInfo.java 和 LicenseVerifyParam.java 放到和服务端一样名字的包下即可。
原因分析: 证书安装的时候会做序列化和反序列化,并且是通过xml的格式进行的,xml中包含了要解析到的目标类全名,如果不一致,就解析不到数据导致的错误。目前知道的就这一种方案,如有其他更好的方案欢迎评论区留言交流。
package com.zjtx.tech.license.server.entity; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data public class LicenseVerifyParam { @ApiModelProperty("证书subject") private String subject; @ApiModelProperty("公钥别称") private String publicAlias; @ApiModelProperty("访问公钥库的密码") private String storePass; @ApiModelProperty("证书生成路径") private String licensePath; @ApiModelProperty("密钥库存储路径") private String publicKeysStorePath; }
本文主要记录了springboot应用实现license授权的过程,主要包含服务端证书生成、客户端证书安装及校验的相关配置过程。
目前暂时没在项目中使用,不过作为技术储备还是有一定价值的。
希望能帮助到需要的朋友。
针对以上内容有任何疑问或者建议欢迎留言评论交流~~~
创作不易,欢迎一键三连~~~