当前位置:   article > 正文

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

de.schlichtherle.license

一 License 简介

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

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

二 License文件生成服务端

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

2.1 pom.xml

  1. <!-- License -->
  2. <dependency>
  3. <groupId>de.schlichtherle.truelicense</groupId>
  4. <artifactId>truelicense-core</artifactId>
  5. <version>1.33</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>net.sourceforge.nekohtml</groupId>
  9. <artifactId>nekohtml</artifactId>
  10. <version>1.9.18</version>
  11. </dependency>

2.2 application.properties

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

2.3 License参数类

  1. package com.modules.entity;
  2. import com.fasterxml.jackson.annotation.JsonFormat;
  3. import com.modules.entity.LicenseCheckModel;
  4. import java.io.Serializable;
  5. import java.util.Date;
  6. /**
  7. * License生成类需要的参数
  8. *
  9. */
  10. public class LicenseCreatorParam implements Serializable {
  11. private static final long serialVersionUID = -7793154252684580872L;
  12. /**
  13. * 证书subject
  14. */
  15. private String subject;
  16. /**
  17. * 密钥别称
  18. */
  19. private String privateAlias;
  20. /**
  21. * 密钥密码(需要妥善保管,不能让使用者知道)
  22. */
  23. private String keyPass;
  24. /**
  25. * 访问秘钥库的密码
  26. */
  27. private String storePass;
  28. /**
  29. * 证书生成路径
  30. */
  31. private String licensePath;
  32. /**
  33. * 密钥库存储路径
  34. */
  35. private String privateKeysStorePath;
  36. /**
  37. * 证书生效时间
  38. */
  39. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
  40. private Date issuedTime = new Date();
  41. /**
  42. * 证书失效时间
  43. */
  44. @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
  45. private Date expiryTime;
  46. /**
  47. * 用户类型
  48. */
  49. private String consumerType = "user";
  50. /**
  51. * 用户数量
  52. */
  53. private Integer consumerAmount = 1;
  54. /**
  55. * 描述信息
  56. */
  57. private String description = "";
  58. /**
  59. * 额外的服务器硬件校验信息
  60. */
  61. private LicenseCheckModel licenseCheckModel;
  62. public String getSubject() {
  63. return subject;
  64. }
  65. public void setSubject(String subject) {
  66. this.subject = subject;
  67. }
  68. public String getPrivateAlias() {
  69. return privateAlias;
  70. }
  71. public void setPrivateAlias(String privateAlias) {
  72. this.privateAlias = privateAlias;
  73. }
  74. public String getKeyPass() {
  75. return keyPass;
  76. }
  77. public void setKeyPass(String keyPass) {
  78. this.keyPass = keyPass;
  79. }
  80. public String getStorePass() {
  81. return storePass;
  82. }
  83. public void setStorePass(String storePass) {
  84. this.storePass = storePass;
  85. }
  86. public String getLicensePath() {
  87. return licensePath;
  88. }
  89. public void setLicensePath(String licensePath) {
  90. this.licensePath = licensePath;
  91. }
  92. public String getPrivateKeysStorePath() {
  93. return privateKeysStorePath;
  94. }
  95. public void setPrivateKeysStorePath(String privateKeysStorePath) {
  96. this.privateKeysStorePath = privateKeysStorePath;
  97. }
  98. public Date getIssuedTime() {
  99. return issuedTime;
  100. }
  101. public void setIssuedTime(Date issuedTime) {
  102. this.issuedTime = issuedTime;
  103. }
  104. public Date getExpiryTime() {
  105. return expiryTime;
  106. }
  107. public void setExpiryTime(Date expiryTime) {
  108. this.expiryTime = expiryTime;
  109. }
  110. public String getConsumerType() {
  111. return consumerType;
  112. }
  113. public void setConsumerType(String consumerType) {
  114. this.consumerType = consumerType;
  115. }
  116. public Integer getConsumerAmount() {
  117. return consumerAmount;
  118. }
  119. public void setConsumerAmount(Integer consumerAmount) {
  120. this.consumerAmount = consumerAmount;
  121. }
  122. public String getDescription() {
  123. return description;
  124. }
  125. public void setDescription(String description) {
  126. this.description = description;
  127. }
  128. public LicenseCheckModel getLicenseCheckModel() {
  129. return licenseCheckModel;
  130. }
  131. public void setLicenseCheckModel(LicenseCheckModel licenseCheckModel) {
  132. this.licenseCheckModel = licenseCheckModel;
  133. }
  134. @Override
  135. public String toString() {
  136. return "LicenseCreatorParam{" +
  137. "subject='" + subject + '\'' +
  138. ", privateAlias='" + privateAlias + '\'' +
  139. ", keyPass='" + keyPass + '\'' +
  140. ", storePass='" + storePass + '\'' +
  141. ", licensePath='" + licensePath + '\'' +
  142. ", privateKeysStorePath='" + privateKeysStorePath + '\'' +
  143. ", issuedTime=" + issuedTime +
  144. ", expiryTime=" + expiryTime +
  145. ", consumerType='" + consumerType + '\'' +
  146. ", consumerAmount=" + consumerAmount +
  147. ", description='" + description + '\'' +
  148. ", licenseCheckModel=" + licenseCheckModel +
  149. '}';
  150. }
  151. }
  1. package com.modules.entity;
  2. import java.io.Serializable;
  3. import java.util.List;
  4. /**
  5. * 自定义需要校验的License参数
  6. *
  7. */
  8. public class LicenseCheckModel implements Serializable{
  9. private static final long serialVersionUID = 8600137500316662317L;
  10. /**
  11. * 可被允许的IP地址
  12. */
  13. private List<String> ipAddress;
  14. /**
  15. * 可被允许的MAC地址
  16. */
  17. private List<String> macAddress;
  18. /**
  19. * 可被允许的CPU序列号
  20. */
  21. private String cpuSerial;
  22. /**
  23. * 可被允许的主板序列号
  24. */
  25. private String mainBoardSerial;
  26. public List<String> getIpAddress() {
  27. return ipAddress;
  28. }
  29. public void setIpAddress(List<String> ipAddress) {
  30. this.ipAddress = ipAddress;
  31. }
  32. public List<String> getMacAddress() {
  33. return macAddress;
  34. }
  35. public void setMacAddress(List<String> macAddress) {
  36. this.macAddress = macAddress;
  37. }
  38. public String getCpuSerial() {
  39. return cpuSerial;
  40. }
  41. public void setCpuSerial(String cpuSerial) {
  42. this.cpuSerial = cpuSerial;
  43. }
  44. public String getMainBoardSerial() {
  45. return mainBoardSerial;
  46. }
  47. public void setMainBoardSerial(String mainBoardSerial) {
  48. this.mainBoardSerial = mainBoardSerial;
  49. }
  50. @Override
  51. public String toString() {
  52. return "LicenseCheckModel{" +
  53. "ipAddress=" + ipAddress +
  54. ", macAddress=" + macAddress +
  55. ", cpuSerial='" + cpuSerial + '\'' +
  56. ", mainBoardSerial='" + mainBoardSerial + '\'' +
  57. '}';
  58. }
  59. }

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

  1. package com.modules.license;
  2. import com.modules.entity.LicenseCheckModel;
  3. import lombok.extern.slf4j.Slf4j;
  4. import java.net.InetAddress;
  5. import java.net.NetworkInterface;
  6. import java.net.SocketException;
  7. import java.util.ArrayList;
  8. import java.util.Enumeration;
  9. import java.util.List;
  10. /**
  11. * 用于获取客户服务器的基本信息,如:IP、Mac地址、CPU序列号、主板序列号等
  12. *
  13. */
  14. @Slf4j
  15. public abstract class AbstractServerInfos {
  16. /**
  17. * 组装需要额外校验的License参数
  18. * @since 1.0.0
  19. * @return demo.LicenseCheckModel
  20. */
  21. public LicenseCheckModel getServerInfos(){
  22. LicenseCheckModel result = new LicenseCheckModel();
  23. try {
  24. result.setIpAddress(this.getIpAddress());
  25. result.setMacAddress(this.getMacAddress());
  26. result.setCpuSerial(this.getCPUSerial());
  27. result.setMainBoardSerial(this.getMainBoardSerial());
  28. }catch (Exception e){
  29. log.error("获取服务器硬件信息失败",e);
  30. }
  31. return result;
  32. }
  33. /**
  34. * 获取IP地址
  35. * @return java.util.List<java.lang.String>
  36. */
  37. protected abstract List<String> getIpAddress() throws Exception;
  38. /**
  39. * 获取Mac地址
  40. * @return java.util.List<java.lang.String>
  41. */
  42. protected abstract List<String> getMacAddress() throws Exception;
  43. /**
  44. * 获取CPU序列号
  45. * @return java.lang.String
  46. */
  47. protected abstract String getCPUSerial() throws Exception;
  48. /**
  49. * 获取主板序列号
  50. * @return java.lang.String
  51. */
  52. protected abstract String getMainBoardSerial() throws Exception;
  53. /**
  54. * 获取当前服务器所有符合条件的InetAddress
  55. * @return java.util.List<java.net.InetAddress>
  56. */
  57. protected List<InetAddress> getLocalAllInetAddress() throws Exception {
  58. List<InetAddress> result = new ArrayList<>(4);
  59. // 遍历所有的网络接口
  60. for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) {
  61. NetworkInterface iface = (NetworkInterface) networkInterfaces.nextElement();
  62. // 在所有的接口下再遍历IP
  63. for (Enumeration inetAddresses = iface.getInetAddresses(); inetAddresses.hasMoreElements(); ) {
  64. InetAddress inetAddr = (InetAddress) inetAddresses.nextElement();
  65. //排除LoopbackAddress、SiteLocalAddress、LinkLocalAddress、MulticastAddress类型的IP地址
  66. if(!inetAddr.isLoopbackAddress() /*&& !inetAddr.isSiteLocalAddress()*/
  67. && !inetAddr.isLinkLocalAddress() && !inetAddr.isMulticastAddress()){
  68. result.add(inetAddr);
  69. }
  70. }
  71. }
  72. return result;
  73. }
  74. /**
  75. * 获取某个网络接口的Mac地址
  76. * @param
  77. * @return void
  78. */
  79. protected String getMacByInetAddress(InetAddress inetAddr){
  80. try {
  81. byte[] mac = NetworkInterface.getByInetAddress(inetAddr).getHardwareAddress();
  82. StringBuffer stringBuffer = new StringBuffer();
  83. for(int i=0;i<mac.length;i++){
  84. if(i != 0) {
  85. stringBuffer.append("-");
  86. }
  87. //将十六进制byte转化为字符串
  88. String temp = Integer.toHexString(mac[i] & 0xff);
  89. if(temp.length() == 1){
  90. stringBuffer.append("0" + temp);
  91. }else{
  92. stringBuffer.append(temp);
  93. }
  94. }
  95. return stringBuffer.toString().toUpperCase();
  96. } catch (SocketException e) {
  97. e.printStackTrace();
  98. log.error("获取某个网络接口的Mac地址异常", e.getMessage());
  99. }
  100. return null;
  101. }
  102. }

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

  1. package com.modules.license;
  2. import org.apache.commons.lang3.StringUtils;
  3. import java.io.BufferedReader;
  4. import java.io.InputStreamReader;
  5. import java.net.InetAddress;
  6. import java.util.List;
  7. import java.util.stream.Collectors;
  8. /**
  9. * 用于获取客户Linux服务器的基本信息
  10. *
  11. */
  12. public class LinuxServerInfos extends AbstractServerInfos {
  13. @Override
  14. protected List<String> getIpAddress() throws Exception {
  15. List<String> result = null;
  16. //获取所有网络接口
  17. List<InetAddress> inetAddresses = getLocalAllInetAddress();
  18. if(inetAddresses != null && inetAddresses.size() > 0){
  19. result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
  20. }
  21. return result;
  22. }
  23. @Override
  24. protected List<String> getMacAddress() throws Exception {
  25. List<String> result = null;
  26. //1. 获取所有网络接口
  27. List<InetAddress> inetAddresses = getLocalAllInetAddress();
  28. if(inetAddresses != null && inetAddresses.size() > 0){
  29. //2. 获取所有网络接口的Mac地址
  30. result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
  31. }
  32. return result;
  33. }
  34. @Override
  35. protected String getCPUSerial() throws Exception {
  36. //序列号
  37. String serialNumber = "";
  38. //使用dmidecode命令获取CPU序列号
  39. String[] shell = {"/bin/bash","-c","dmidecode -t processor | grep 'ID' | awk -F ':' '{print $2}' | head -n 1"};
  40. Process process = Runtime.getRuntime().exec(shell);
  41. process.getOutputStream().close();
  42. BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
  43. String line = reader.readLine().trim();
  44. if(StringUtils.isNotBlank(line)){
  45. serialNumber = line;
  46. }
  47. reader.close();
  48. return serialNumber;
  49. }
  50. @Override
  51. protected String getMainBoardSerial() throws Exception {
  52. //序列号
  53. String serialNumber = "";
  54. //使用dmidecode命令获取主板序列号
  55. String[] shell = {"/bin/bash","-c","dmidecode | grep 'Serial Number' | awk -F ':' '{print $2}' | head -n 1"};
  56. Process process = Runtime.getRuntime().exec(shell);
  57. process.getOutputStream().close();
  58. BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
  59. String line = reader.readLine().trim();
  60. if(StringUtils.isNotBlank(line)){
  61. serialNumber = line;
  62. }
  63. reader.close();
  64. return serialNumber;
  65. }
  66. }

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

  1. package com.modules.license;
  2. import java.net.InetAddress;
  3. import java.util.List;
  4. import java.util.Scanner;
  5. import java.util.stream.Collectors;
  6. /**
  7. * 用于获取客户Windows服务器的基本信息
  8. *
  9. */
  10. public class WindowsServerInfos extends AbstractServerInfos {
  11. @Override
  12. protected List<String> getIpAddress() throws Exception {
  13. List<String> result = null;
  14. //获取所有网络接口
  15. List<InetAddress> inetAddresses = getLocalAllInetAddress();
  16. if(inetAddresses != null && inetAddresses.size() > 0){
  17. result = inetAddresses.stream().map(InetAddress::getHostAddress).distinct().map(String::toLowerCase).collect(Collectors.toList());
  18. }
  19. return result;
  20. }
  21. @Override
  22. protected List<String> getMacAddress() throws Exception {
  23. List<String> result = null;
  24. //1. 获取所有网络接口
  25. List<InetAddress> inetAddresses = getLocalAllInetAddress();
  26. if(inetAddresses != null && inetAddresses.size() > 0){
  27. //2. 获取所有网络接口的Mac地址
  28. result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
  29. }
  30. return result;
  31. }
  32. @Override
  33. protected String getCPUSerial() throws Exception {
  34. //序列号
  35. String serialNumber = "";
  36. //使用WMIC获取CPU序列号
  37. Process process = Runtime.getRuntime().exec("wmic cpu get processorid");
  38. process.getOutputStream().close();
  39. Scanner scanner = new Scanner(process.getInputStream());
  40. if(scanner.hasNext()){
  41. scanner.next();
  42. }
  43. if(scanner.hasNext()){
  44. serialNumber = scanner.next().trim();
  45. }
  46. scanner.close();
  47. return serialNumber;
  48. }
  49. @Override
  50. protected String getMainBoardSerial() throws Exception {
  51. //序列号
  52. String serialNumber = "";
  53. //使用WMIC获取主板序列号
  54. Process process = Runtime.getRuntime().exec("wmic baseboard get serialnumber");
  55. process.getOutputStream().close();
  56. Scanner scanner = new Scanner(process.getInputStream());
  57. if(scanner.hasNext()){
  58. scanner.next();
  59. }
  60. if(scanner.hasNext()){
  61. serialNumber = scanner.next().trim();
  62. }
  63. scanner.close();
  64. return serialNumber;
  65. }
  66. }

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

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

  1. package com.modules.license;
  2. import com.modules.entity.LicenseCheckModel;
  3. import de.schlichtherle.license.*;
  4. import de.schlichtherle.xml.GenericCertificate;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.apache.commons.lang3.StringUtils;
  7. import java.beans.XMLDecoder;
  8. import java.io.BufferedInputStream;
  9. import java.io.ByteArrayInputStream;
  10. import java.io.UnsupportedEncodingException;
  11. import java.util.Date;
  12. import java.util.List;
  13. /**
  14. * 自定义LicenseManager,用于增加额外的服务器硬件信息校验
  15. *
  16. */
  17. @Slf4j
  18. public class CustomLicenseManager extends LicenseManager{
  19. //XML编码
  20. private static final String XML_CHARSET = "UTF-8";
  21. //默认BUFSIZE
  22. private static final int DEFAULT_BUFSIZE = 8 * 1024;
  23. public CustomLicenseManager() {
  24. }
  25. public CustomLicenseManager(LicenseParam param) {
  26. super(param);
  27. }
  28. /**
  29. * 复写create方法
  30. * @param
  31. * @return byte[]
  32. */
  33. @Override
  34. protected synchronized byte[] create(
  35. LicenseContent content,
  36. LicenseNotary notary)
  37. throws Exception {
  38. initialize(content);
  39. this.validateCreate(content);
  40. final GenericCertificate certificate = notary.sign(content);
  41. return getPrivacyGuard().cert2key(certificate);
  42. }
  43. /**
  44. * 复写install方法,其中validate方法调用本类中的validate方法,校验IP地址、Mac地址等其他信息
  45. * @param
  46. * @return de.schlichtherle.license.LicenseContent
  47. */
  48. @Override
  49. protected synchronized LicenseContent install(
  50. final byte[] key,
  51. final LicenseNotary notary)
  52. throws Exception {
  53. final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
  54. notary.verify(certificate);
  55. final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());
  56. this.validate(content);
  57. setLicenseKey(key);
  58. setCertificate(certificate);
  59. return content;
  60. }
  61. /**
  62. * 复写verify方法,调用本类中的validate方法,校验IP地址、Mac地址等其他信息
  63. * @param
  64. * @return de.schlichtherle.license.LicenseContent
  65. */
  66. @Override
  67. protected synchronized LicenseContent verify(final LicenseNotary notary)
  68. throws Exception {
  69. GenericCertificate certificate = getCertificate();
  70. // Load license key from preferences,
  71. final byte[] key = getLicenseKey();
  72. if (null == key){
  73. throw new NoLicenseInstalledException(getLicenseParam().getSubject());
  74. }
  75. certificate = getPrivacyGuard().key2cert(key);
  76. notary.verify(certificate);
  77. final LicenseContent content = (LicenseContent)this.load(certificate.getEncoded());
  78. this.validate(content);
  79. setCertificate(certificate);
  80. return content;
  81. }
  82. /**
  83. * 校验生成证书的参数信息
  84. * @param content 证书正文
  85. */
  86. protected synchronized void validateCreate(final LicenseContent content)
  87. throws LicenseContentException {
  88. final LicenseParam param = getLicenseParam();
  89. final Date now = new Date();
  90. final Date notBefore = content.getNotBefore();
  91. final Date notAfter = content.getNotAfter();
  92. if (null != notAfter && now.after(notAfter)){
  93. throw new LicenseContentException("证书失效时间不能早于当前时间");
  94. }
  95. if (null != notBefore && null != notAfter && notAfter.before(notBefore)){
  96. throw new LicenseContentException("证书生效时间不能晚于证书失效时间");
  97. }
  98. final String consumerType = content.getConsumerType();
  99. if (null == consumerType){
  100. throw new LicenseContentException("用户类型不能为空");
  101. }
  102. }
  103. /**
  104. * 复写validate方法,增加IP地址、Mac地址等其他信息校验
  105. * @param content LicenseContent
  106. */
  107. @Override
  108. protected synchronized void validate(final LicenseContent content)
  109. throws LicenseContentException {
  110. //1. 首先调用父类的validate方法
  111. super.validate(content);
  112. //2. 然后校验自定义的License参数
  113. //License中可被允许的参数信息
  114. LicenseCheckModel expectedCheckModel = (LicenseCheckModel) content.getExtra();
  115. //当前服务器真实的参数信息
  116. LicenseCheckModel serverCheckModel = getServerInfos();
  117. if(expectedCheckModel != null && serverCheckModel != null){
  118. //校验IP地址
  119. if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
  120. throw new LicenseContentException("当前服务器的IP没在授权范围内");
  121. }
  122. //校验Mac地址
  123. if(!checkIpAddress(expectedCheckModel.getMacAddress(),serverCheckModel.getMacAddress())){
  124. throw new LicenseContentException("当前服务器的Mac地址没在授权范围内");
  125. }
  126. //校验主板序列号
  127. if(!checkSerial(expectedCheckModel.getMainBoardSerial(),serverCheckModel.getMainBoardSerial())){
  128. throw new LicenseContentException("当前服务器的主板序列号没在授权范围内");
  129. }
  130. //校验CPU序列号
  131. if(!checkSerial(expectedCheckModel.getCpuSerial(),serverCheckModel.getCpuSerial())){
  132. throw new LicenseContentException("当前服务器的CPU序列号没在授权范围内");
  133. }
  134. }else{
  135. throw new LicenseContentException("不能获取服务器硬件信息");
  136. }
  137. }
  138. /**
  139. * 重写XMLDecoder解析XML
  140. * @param encoded XML类型字符串
  141. * @return java.lang.Object
  142. */
  143. private Object load(String encoded){
  144. BufferedInputStream inputStream = null;
  145. XMLDecoder decoder = null;
  146. try {
  147. inputStream = new BufferedInputStream(new ByteArrayInputStream(encoded.getBytes(XML_CHARSET)));
  148. decoder = new XMLDecoder(new BufferedInputStream(inputStream, DEFAULT_BUFSIZE),null,null);
  149. return decoder.readObject();
  150. } catch (UnsupportedEncodingException e) {
  151. e.printStackTrace();
  152. } finally {
  153. try {
  154. if(decoder != null){
  155. decoder.close();
  156. }
  157. if(inputStream != null){
  158. inputStream.close();
  159. }
  160. } catch (Exception e) {
  161. log.error("XMLDecoder解析XML失败",e.getMessage());
  162. }
  163. }
  164. return null;
  165. }
  166. /**
  167. * 获取当前服务器需要额外校验的License参数
  168. * @return demo.LicenseCheckModel
  169. */
  170. private LicenseCheckModel getServerInfos(){
  171. //操作系统类型
  172. String osName = System.getProperty("os.name").toLowerCase();
  173. AbstractServerInfos abstractServerInfos = null;
  174. //根据不同操作系统类型选择不同的数据获取方法
  175. if (osName.startsWith("windows")) {
  176. abstractServerInfos = new WindowsServerInfos();
  177. } else if (osName.startsWith("linux")) {
  178. abstractServerInfos = new LinuxServerInfos();
  179. }else{//其他服务器类型
  180. abstractServerInfos = new LinuxServerInfos();
  181. }
  182. return abstractServerInfos.getServerInfos();
  183. }
  184. /**
  185. * 校验当前服务器的IP/Mac地址是否在可被允许的IP范围内<br/>
  186. * 如果存在IP在可被允许的IP/Mac地址范围内,则返回true
  187. * @return boolean
  188. */
  189. private boolean checkIpAddress(List<String> expectedList,List<String> serverList){
  190. if(expectedList != null && expectedList.size() > 0){
  191. if(serverList != null && serverList.size() > 0){
  192. for(String expected : expectedList){
  193. if(serverList.contains(expected.trim())){
  194. return true;
  195. }
  196. }
  197. }
  198. return false;
  199. }else {
  200. return true;
  201. }
  202. }
  203. /**
  204. * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
  205. * @return boolean
  206. */
  207. private boolean checkSerial(String expectedSerial,String serverSerial){
  208. if(StringUtils.isNotBlank(expectedSerial)){
  209. if(StringUtils.isNotBlank(serverSerial)){
  210. if(expectedSerial.equals(serverSerial)){
  211. return true;
  212. }
  213. }
  214. return false;
  215. }else{
  216. return true;
  217. }
  218. }
  219. }

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

  1. package com.modules.license;
  2. import com.modules.entity.LicenseCreatorParam;
  3. import de.schlichtherle.license.*;
  4. import lombok.extern.slf4j.Slf4j;
  5. import javax.security.auth.x500.X500Principal;
  6. import java.io.File;
  7. import java.text.MessageFormat;
  8. import java.util.prefs.Preferences;
  9. /**
  10. * License生成类
  11. *
  12. */
  13. @Slf4j
  14. public class LicenseCreator {
  15. private final static X500Principal DEFAULT_HOLDER_AND_ISSUER = new X500Principal("CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN");
  16. private LicenseCreatorParam param;
  17. public LicenseCreator(LicenseCreatorParam param) {
  18. this.param = param;
  19. }
  20. /**
  21. * 生成License证书
  22. * @return boolean
  23. */
  24. public boolean generateLicense(){
  25. try {
  26. LicenseManager licenseManager = new CustomLicenseManager(initLicenseParam());
  27. LicenseContent licenseContent = initLicenseContent();
  28. licenseManager.store(licenseContent,new File(param.getLicensePath()));
  29. return true;
  30. }catch (Exception e){
  31. log.error(MessageFormat.format("证书生成失败:{0}",param),e.getMessage());
  32. return false;
  33. }
  34. }
  35. /**
  36. * 初始化证书生成参数
  37. * @return de.schlichtherle.license.LicenseParam
  38. */
  39. private LicenseParam initLicenseParam(){
  40. Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
  41. //设置对证书内容加密的秘钥
  42. CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
  43. KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
  44. ,param.getPrivateKeysStorePath()
  45. ,param.getPrivateAlias()
  46. ,param.getStorePass()
  47. ,param.getKeyPass());
  48. LicenseParam licenseParam = new DefaultLicenseParam(param.getSubject()
  49. ,preferences
  50. ,privateStoreParam
  51. ,cipherParam);
  52. return licenseParam;
  53. }
  54. /**
  55. * 设置证书生成正文信息
  56. * @return de.schlichtherle.license.LicenseContent
  57. */
  58. private LicenseContent initLicenseContent(){
  59. LicenseContent licenseContent = new LicenseContent();
  60. licenseContent.setHolder(DEFAULT_HOLDER_AND_ISSUER);
  61. licenseContent.setIssuer(DEFAULT_HOLDER_AND_ISSUER);
  62. licenseContent.setSubject(param.getSubject());
  63. licenseContent.setIssued(param.getIssuedTime());
  64. licenseContent.setNotBefore(param.getIssuedTime());
  65. licenseContent.setNotAfter(param.getExpiryTime());
  66. licenseContent.setConsumerType(param.getConsumerType());
  67. licenseContent.setConsumerAmount(param.getConsumerAmount());
  68. licenseContent.setInfo(param.getDescription());
  69. //扩展校验服务器硬件信息
  70. licenseContent.setExtra(param.getLicenseCheckModel());
  71. return licenseContent;
  72. }
  73. }

2.7 添加一个生成证书的Controller

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

  1. package com.modules.controller;
  2. import com.modules.entity.LicenseCheckModel;
  3. import com.modules.entity.LicenseCreatorParam;
  4. import com.modules.license.*;
  5. import org.apache.commons.lang3.StringUtils;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.web.bind.annotation.RequestBody;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RequestParam;
  11. import org.springframework.web.bind.annotation.RestController;
  12. import java.util.HashMap;
  13. import java.util.Map;
  14. /**
  15. *
  16. * 用于生成证书文件,不能放在给客户部署的代码里
  17. * @since 1.0.0
  18. */
  19. @RestController
  20. @RequestMapping("/license")
  21. public class LicenseCreatorController {
  22. /**
  23. * 证书生成路径
  24. */
  25. @Value("${license.licensePath}")
  26. private String licensePath;
  27. /**
  28. * 获取服务器硬件信息
  29. * @param osName 操作系统类型,如果为空则自动判断
  30. * @return com.ccx.models.license.LicenseCheckModel
  31. */
  32. @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
  33. public LicenseCheckModel getServerInfos(@RequestParam(value = "osName",required = false) String osName) {
  34. //操作系统类型
  35. if(StringUtils.isBlank(osName)){
  36. osName = System.getProperty("os.name");
  37. }
  38. osName = osName.toLowerCase();
  39. AbstractServerInfos abstractServerInfos = null;
  40. //根据不同操作系统类型选择不同的数据获取方法
  41. if (osName.startsWith("windows")) {
  42. abstractServerInfos = new WindowsServerInfos();
  43. } else if (osName.startsWith("linux")) {
  44. abstractServerInfos = new LinuxServerInfos();
  45. }else{//其他服务器类型
  46. abstractServerInfos = new LinuxServerInfos();
  47. }
  48. return abstractServerInfos.getServerInfos();
  49. }
  50. /**
  51. * 生成证书
  52. * @param param 生成证书需要的参数,如:{"subject":"ccx-models","privateAlias":"privateKey","keyPass":"5T7Zz5Y0dJFcqTxvzkH5LDGJJSGMzQ","storePass":"3538cef8e7","licensePath":"C:/Users/zifangsky/Desktop/license.lic","privateKeysStorePath":"C:/Users/zifangsky/Desktop/privateKeys.keystore","issuedTime":"2018-04-26 14:48:12","expiryTime":"2018-12-31 00:00:00","consumerType":"User","consumerAmount":1,"description":"这是证书描述信息","licenseCheckModel":{"ipAddress":["192.168.245.1","10.0.5.22"],"macAddress":["00-50-56-C0-00-01","50-7B-9D-F9-18-41"],"cpuSerial":"BFEBFBFF000406E3","mainBoardSerial":"L1HF65E00X9"}}
  53. * @return java.util.Map<java.lang.String,java.lang.Object>
  54. */
  55. @RequestMapping(value = "/generateLicense",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
  56. public Map<String,Object> generateLicense(@RequestBody LicenseCreatorParam param) {
  57. Map<String,Object> resultMap = new HashMap<>(2);
  58. if(StringUtils.isBlank(param.getLicensePath())){
  59. param.setLicensePath(licensePath);
  60. }
  61. LicenseCreator licenseCreator = new LicenseCreator(param);
  62. boolean result = licenseCreator.generateLicense();
  63. if(result){
  64. resultMap.put("result","ok");
  65. resultMap.put("msg",param);
  66. }else{
  67. resultMap.put("result","error");
  68. resultMap.put("msg","证书文件生成失败!");
  69. }
  70. return resultMap;
  71. }
  72. }

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

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

  1. #生成命令
  2. keytool -genkeypair -keysize 1024 -validity 3650 -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -keypass "private_password1234" -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"
  3. #导出命令
  4. keytool -exportcert -alias "privateKey" -keystore "privateKeys.keystore" -storepass "public_password1234" -file "certfile.cer"
  5. #导入命令
  6. keytool -import -alias "publicCert" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "public_password1234"

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

2.9 为客户生成license文件

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

获取服务器硬件信息

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

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

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

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

3.1 pom.xml

  1. <!-- License -->
  2. <dependency>
  3. <groupId>de.schlichtherle.truelicense</groupId>
  4. <artifactId>truelicense-core</artifactId>
  5. <version>1.33</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>net.sourceforge.nekohtml</groupId>
  9. <artifactId>nekohtml</artifactId>
  10. <version>1.9.18</version>
  11. </dependency>

3.2 application.properties

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

3.3 License校验类

  1. package com.modules.license;
  2. import com.modules.entity.LicenseVerifyParam;
  3. import de.schlichtherle.license.*;
  4. import lombok.extern.slf4j.Slf4j;
  5. import java.io.File;
  6. import java.text.DateFormat;
  7. import java.text.MessageFormat;
  8. import java.text.SimpleDateFormat;
  9. import java.util.prefs.Preferences;
  10. /**
  11. * License校验类
  12. */
  13. @Slf4j
  14. public class LicenseVerify {
  15. /**
  16. * 安装License证书
  17. */
  18. public synchronized LicenseContent install(LicenseVerifyParam param){
  19. LicenseContent result = null;
  20. DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  21. //1. 安装证书
  22. try{
  23. LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
  24. licenseManager.uninstall();
  25. result = licenseManager.install(new File(param.getLicensePath()));
  26. log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",format.format(result.getNotBefore()),format.format(result.getNotAfter())));
  27. }catch (Exception e){
  28. log.error("证书安装失败!",e);
  29. }
  30. return result;
  31. }
  32. /**
  33. * 校验License证书
  34. * @return boolean
  35. */
  36. public boolean verify(){
  37. LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
  38. DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  39. //2. 校验证书
  40. try {
  41. LicenseContent licenseContent = licenseManager.verify();
  42. // System.out.println(licenseContent.getSubject());
  43. log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",format.format(licenseContent.getNotBefore()),format.format(licenseContent.getNotAfter())));
  44. return true;
  45. }catch (Exception e){
  46. log.error("证书校验失败!",e);
  47. return false;
  48. }
  49. }
  50. /**
  51. * 初始化证书生成参数
  52. * @param param License校验类需要的参数
  53. * @return de.schlichtherle.license.LicenseParam
  54. */
  55. private LicenseParam initLicenseParam(LicenseVerifyParam param){
  56. Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
  57. CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
  58. KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
  59. ,param.getPublicKeysStorePath()
  60. ,param.getPublicAlias()
  61. ,param.getStorePass()
  62. ,null);
  63. return new DefaultLicenseParam(param.getSubject()
  64. ,preferences
  65. ,publicStoreParam
  66. ,cipherParam);
  67. }
  68. }

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

  1. package com.modules.license;
  2. import com.modules.entity.LicenseVerifyParam;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.apache.commons.lang3.StringUtils;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.context.ApplicationContext;
  7. import org.springframework.context.ApplicationListener;
  8. import org.springframework.context.event.ContextRefreshedEvent;
  9. import org.springframework.stereotype.Component;
  10. /**
  11. * 在项目启动时安装证书
  12. */
  13. @Slf4j
  14. @Component
  15. public class LicenseCheckListener implements ApplicationListener<ContextRefreshedEvent> {
  16. /**
  17. * 证书subject
  18. */
  19. @Value("${license.subject}")
  20. private String subject;
  21. /**
  22. * 公钥别称
  23. */
  24. @Value("${license.publicAlias}")
  25. private String publicAlias;
  26. /**
  27. * 访问公钥库的密码
  28. */
  29. @Value("${license.storePass}")
  30. private String storePass;
  31. /**
  32. * 证书生成路径
  33. */
  34. @Value("${license.licensePath}")
  35. private String licensePath;
  36. /**
  37. * 密钥库存储路径
  38. */
  39. @Value("${license.publicKeysStorePath}")
  40. private String publicKeysStorePath;
  41. @Override
  42. public void onApplicationEvent(ContextRefreshedEvent event) {
  43. //root application context 没有parent
  44. ApplicationContext context = event.getApplicationContext().getParent();
  45. if(context == null){
  46. if(StringUtils.isNotBlank(licensePath)){
  47. log.info("++++++++ 开始安装证书 ++++++++");
  48. LicenseVerifyParam param = new LicenseVerifyParam();
  49. param.setSubject(subject);
  50. param.setPublicAlias(publicAlias);
  51. param.setStorePass(storePass);
  52. param.setLicensePath(licensePath);
  53. param.setPublicKeysStorePath(publicKeysStorePath);
  54. LicenseVerify licenseVerify = new LicenseVerify();
  55. //安装证书
  56. licenseVerify.install(param);
  57. log.info("++++++++ 证书安装结束 ++++++++");
  58. }
  59. }
  60. }
  61. }

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

  1. package com.modules.common.config;
  2. import com.modules.license.LicenseCheckInterceptor;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. import javax.annotation.Resource;
  7. /**
  8. * @Description: 拦截器配置
  9. */
  10. @Configuration
  11. public class InterceptorConfig implements WebMvcConfigurer {
  12. @Resource
  13. private LicenseCheckInterceptor licenseCheckInterceptor;
  14. @Override
  15. public void addInterceptors(InterceptorRegistry registry) {
  16. //添加要拦截的url
  17. registry.addInterceptor(licenseCheckInterceptor)
  18. // 拦截的路径
  19. .addPathPatterns("/**");
  20. // 放行的路径
  21. // .excludePathPatterns("/admin/login");
  22. }
  23. }

3.6 模拟登陆LoginController

  1. package com.modules.controller;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.stereotype.Controller;
  4. import org.springframework.web.bind.annotation.*;
  5. import java.text.MessageFormat;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. /**
  9. * 登录
  10. */
  11. @Slf4j
  12. @CrossOrigin
  13. @RestController
  14. public class LoginController {
  15. /**
  16. * 模拟登录验证
  17. * @param loginName 用户名
  18. * @param password 密码
  19. */
  20. @GetMapping(value = "login")
  21. public Map<String,Object> test(@RequestParam(required = true) String loginName, @RequestParam(required = true) String password){
  22. Map<String,Object> result = new HashMap<>(1);
  23. log.info(MessageFormat.format("登录名称:{0},密码:{1}",loginName,password));
  24. //模拟登录
  25. log.info("模拟登录被拦截检查证书可用性");
  26. result.put("code",200);
  27. return result;
  28. }
  29. }

3.7 测试

3.7.1 验证失败测试

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

3.7.2 验证成功测试

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

四 源码

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/663850
推荐阅读
相关标签
  

闽ICP备14008679号