ToB企服应用市场:ToB评测及商务社交产业平台

标题: 软件产品许可证书 Licence 全流程研发(利用非对称加密技术,既安全又简朴 [打印本页]

作者: 九天猎人    时间: 2024-6-23 17:19
标题: 软件产品许可证书 Licence 全流程研发(利用非对称加密技术,既安全又简朴
本篇博客对应的代码所在:
Gitee 堆栈所在:https://gitee.com/biandanLoveyou/licence

1、背景介绍

   公司是做软件 SAAS 服务的,一样平常来说软件部署有以下几种常见的模式:
  1、自己研发和部署到自己的云服务器,然后有偿提供账号给客户利用。代码开辟和服务运维都是本公司自己的,客户不须要关心软件的事情。这种模式只须要对提供的账号做权限管控就行。这种模式一样平常适用于客户没有自己的研发团队,或者客户的研发团队不涉及该领域。
  
  2、自己研发,但是须要部署到客户的云服务器或者私有化服务器,但是不交付源代码。这种模式有一个须要考虑的点:一旦部署到客户的云服务器或者私有化服务器,系统就变得不好管控或者说失去了控制。表现在:一旦过了条约有效期,想停掉运行在客户服务器的系统,那就变得很困难,或者对方不配合,或者对方也可以找运维职员自启动(本文先不考虑客户拿到 jar 包反编译源代码的情况,如果条约有代码协议的束缚,那就是违法的)。本篇博客重要针对这种模式做 Licence 研发,让私有化部署的系统变得可管控。
  
  3、自己研发,资助客户部署到指定服务器,并且交付源代码。
  
2、研发思路

2.1 不可行的方案

   1、条约束缚或者口头束缚。一样平常来说,两家公司不合作之后,基本上关系就变得淡漠或者陌路,跟你在一家公司去职的情况差不多。想要口头束缚客户别用你们之前的系统,险些不大概,只要还能用,客户也会偷偷用,实在用不了才会考虑替代方案,这是人之常情。
  2、把“能用”和“不能用”的控制逻辑放在数据库或者设置文件中。这种方案也不太行,懂一点技术人会顺藤摸瓜,把他们的数据库或者设置文件看一下,哪些是重要信息,修改一下,就能继续用了。
  因此,我们须要考虑一个万全之策,既能做到系统的管控,又能做到后期的简朴维护。那就是把控制逻辑嵌入到代码中(本文先不考虑客户拿到 jar 包反编译源代码的情况,如果条约有代码协议的束缚,那就是违法的)

2.2 什么是证书?

   证书相当于一个许可证,各行各业都有自己的标准和形式。
  
  软件行业的证书也是因情况而定,可以根据公司的发展来制定属于你们公司的证书。最终解释权都属于你们公司,只要你们公司承认,那就是有效。

2.3 利用证书的方案

思路如下:
   管理背景生成证书 —> 编写证书的校验逻辑并打成 jar 包 —> 把 jar 包嵌入到私有化部署的代码中 —> 考虑证书到期的时间可以方便更换(且无需重启服务) —> 考虑证书到期前的提示
  我们可以用学过的技术,把证书的方案落地。焦点技术采用【非对称加密+拦截器】,旨在“让天下没有难写的代码”。
不懂非对称加密?查看我的博客:利用 Java 原生或 Hutool 工具包编写非对称加解密的工具类-CSDN博客

3、代码实现

3.1 代码结构


说明:
     
3.2 管理背景(服务端)代码实现

管理背景代码结构:

3.2.1 证书模型介绍

证书的模型(实体)我们写在 LicenceEntity 这个实体类,焦点字段如下:
  1. public class LicenceEntity implements Serializable {
  2.     private static final long serialVersionUID = -4048081970386334457L;
  3.     /**
  4.      * 证书 ID
  5.      */
  6.     private String licenceId;
  7.     /**
  8.      * 证书名称
  9.      */
  10.     private String licenceName;
  11.     /**
  12.      * 客户端机器的网卡物理地址
  13.      */
  14.     private String mac;
  15.     /**
  16.      * 秘钥(指的是公钥)
  17.      */
  18.     private String key;
  19.     /**
  20.      * 证书生效开始日期,格式:yyyy-MM-dd
  21.      */
  22.     private String effectStartDate;
  23.     /**
  24.      * 证书生效结束日期,格式:yyyy-MM-dd
  25.      */
  26.     private String effectEndDate;
  27.     /**
  28.      * 颁发证书联系人
  29.      */
  30.     private String contactName;
  31.     /**
  32.      * 颁发证书人的联系方式
  33.      */
  34.     private String contactWay;
  35.     /**
  36.      * 证书所有者
  37.      */
  38.     private String owner;
  39.     /**
  40.      * 加密的内容。这个字段是将其它字段加密后的完整内容
  41.      */
  42.     private String content;
  43. }
复制代码
说明
   1、mac:这是客户端的网卡物理所在,每台呆板的物理所在都不一样,基本上可以保证你们公司服务的客户是唯一的。这个 mac 所在,一样平常是负责部署的职员去获取,或者让客户自己提供。这个字段的意义在于:如果你们公司要进行严酷的证书校验,必须是一个证书只允许在一台服务器上利用,那这个字段就显得非常重要。固然,你可以换成自己喜欢的字段名。
  
  2、effectStartDate、effectEndDate:证书的见效起止时间字段。这两个字段是证书的焦点字段,用来判断证书是否在有效期内。
  
  3、key:公钥。根据非对称加密的内容,我们须要把公钥对外,客户端拿到公钥后,可以解密我们用私钥加密的内容。
  
  4、content:证书数据的加密内容。这个字段,是把证书实体转成 JSON 字符串后,再进行非对称加密后的数据。为什么要有这个字段呢?考虑的因素是:①充实利用了非对称加解密的技术,私钥加密的内容只允许公钥解密。如果这个数据被篡改,证书就失效。②别的字段的明文,比如:证书的见效起止时间、证书的联系人等,方便客户拿到证书后,直观的看到这些信息。
  
  5、以上证书的字段,可以根据自己的业务须要去拓展。但是焦点的几个字段,最好能生存。
  3.2.2 焦点实现类

管理背景的焦点,就是准确的生成证书文件 licence.txt,焦点代码如下:
  1. package com.study.service.impl;
  2. import com.study.constant.CommonKeys;
  3. import com.study.entity.LicenceEntity;
  4. import com.study.service.LicenceService;
  5. import com.study.util.LicenceJsonUtil;
  6. import com.study.util.NativeSecurityUtil;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.beans.BeanUtils;
  10. import org.springframework.beans.factory.annotation.Value;
  11. import org.springframework.scheduling.annotation.Async;
  12. import org.springframework.stereotype.Service;
  13. import org.springframework.util.StringUtils;
  14. import javax.servlet.http.HttpServletResponse;
  15. import java.io.BufferedOutputStream;
  16. import java.io.IOException;
  17. import java.nio.file.Files;
  18. import java.nio.file.Path;
  19. import java.nio.file.Paths;
  20. import java.nio.file.StandardOpenOption;
  21. /**
  22. * @author CSDN 流放深圳
  23. * @description 证书生成核心实现类
  24. * @create 2024-04-13 15:55
  25. * @since 1.0.0
  26. */
  27. @Service
  28. public class LicenceServiceImpl implements LicenceService {
  29.     private static Logger log = LoggerFactory.getLogger(LicenceServiceImpl.class);
  30.     /**
  31.      * 获取配置文件的私钥
  32.      */
  33.     @Value("${licence.privateKey}")
  34.     private String privateKey;
  35.     /**
  36.      * 获取配置文件的公钥
  37.      */
  38.     @Value("${licence.publicKey}")
  39.     private String publicKey;
  40.     /**
  41.      * 创建证书 licence 内容
  42.      * @param dtoEntity
  43.      * @return
  44.      */
  45.     @Override
  46.     public LicenceEntity createLicence(LicenceEntity dtoEntity) {
  47.         LicenceEntity entity = new LicenceEntity();
  48.         //赋值相同属性
  49.         BeanUtils.copyProperties(dtoEntity, entity);
  50.         //content 和 key 需要额外处理
  51.         if(StringUtils.isEmpty(privateKey) || StringUtils.isEmpty(publicKey)) return null;
  52.         entity.setKey(publicKey);//把公钥放进去,否则客户端无法获取公钥,就无法解密
  53.         //把实体转成字符串
  54.         String json = LicenceJsonUtil.objectToStr(entity);
  55.         //把整个字符串加密
  56.         String content = NativeSecurityUtil.encryptByPrivateKey(json, privateKey);
  57.         //把加密后的字符串赋值给 content
  58.         entity.setContent(content);
  59.         return entity;
  60.     }
  61.     /**
  62.      * 下载证书文件
  63.      * @param dtoEntity
  64.      * @param response
  65.      */
  66.     @Override
  67.     public void downLoadLicence(LicenceEntity dtoEntity, HttpServletResponse response) {
  68.         LicenceEntity licenceEntity = createLicence(dtoEntity);
  69.         if(null == licenceEntity){
  70.             log.error("证书的秘钥未配置!");
  71.             return;
  72.         }
  73.         //把实体转为字符串
  74.         String result = LicenceJsonUtil.objectToStr(licenceEntity);
  75.         BufferedOutputStream out = null;
  76.         try {
  77.             //证书文件名
  78.             String fileName = CommonKeys.CERTIFICATE_FILE;
  79.             response.setContentType("application/octet-stream");
  80.             response.setHeader("Content-Disposition", "attachment; filename="" + fileName + """);
  81.             response.setCharacterEncoding("UTF-8");
  82.             out = new BufferedOutputStream(response.getOutputStream());
  83.             out.write(result.getBytes("UTF-8"));
  84.             out.flush();
  85.         } catch (IOException e) {
  86.             log.error(e.getMessage(), e);
  87.         } finally {
  88.             try {
  89.                 if (out != null) {
  90.                     out.close();
  91.                 }
  92.             } catch (Exception e) {
  93.                 log.error(e.getMessage(), e);
  94.             }
  95.         }
  96.         //TODO 另外可以保存一份到指定目录下,方便测试(这是使用异步的形式)。上线的时候记得注释掉
  97.         saveLicenceToCertificate(result);
  98.     }
  99.     /**
  100.      * 异步保存证书文件到指定目录
  101.      * @param result
  102.      */
  103.     @Async
  104.     public void saveLicenceToCertificate(String result){
  105.         try{
  106.             // 创建证书目录(如果尚未创建)
  107.             Path directory = Paths.get(CommonKeys.CERTIFICATE_DIRECTORY);
  108.             Files.createDirectories(directory);
  109.             // 构建证书文件的完整路径
  110.             Path filePath = directory.resolve(CommonKeys.CERTIFICATE_FILE);
  111.             // 检查文件是否存在,存在则删掉
  112.             Files.deleteIfExists(filePath);
  113.             //创建文件(如果文件已经存在,此步骤可能会抛出 FileAlreadyExistsException)
  114.             Files.createFile(filePath);
  115.             //写入内容到文件,使用 StandardOpenOption.APPEND 可以追加内容而不是覆盖
  116.             Files.write(filePath, result.getBytes("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
  117.         }catch (Exception e){
  118.             log.error(e.getMessage(), e);
  119.         }
  120.     }
  121. }
复制代码
说明:
   1、我们采用私钥加密的方式,把实体转为 JSON 字符串加密后赋值给 content 字段。
  2、对外袒露公钥,用于给客户端拿到证书后,通过公钥解密出 content,然后做校验。
  3.2.3 业务层

案例中的业务层 ServerController 比力简朴,可以根据现实业务中去拓展,这里只做了模拟数据:
  1. package com.study.controller;
  2. import com.study.entity.LicenceEntity;
  3. import com.study.service.LicenceService;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.web.bind.annotation.*;
  6. import javax.servlet.http.HttpServletResponse;
  7. import java.util.UUID;
  8. /**
  9. * @author CSDN 流放深圳
  10. * @description 控制层
  11. * @create 2024-04-13 15:55
  12. * @since 1.0.0
  13. */
  14. @RestController
  15. @RequestMapping("/server/licence")
  16. public class ServerController {
  17.     @Autowired
  18.     private LicenceService licenceService;
  19.     /**
  20.      * 下载证书文件
  21.      * 实际项目可以根据前端传递的参数来创建证书字段属性。这里为了测试,直接写测试数据
  22.      * @param response
  23.      */
  24.     @PostMapping("/downLoadLicence")
  25.     public void downLoadLicence(HttpServletResponse response){
  26.         LicenceEntity dtoEntity = createEntity();
  27.         licenceService.downLoadLicence(dtoEntity, response);
  28.     }
  29.     /**
  30.      * 创建测试实体
  31.      * @return
  32.      */
  33.     private LicenceEntity createEntity(){
  34.         LicenceEntity entity = new LicenceEntity();
  35.         entity.setLicenceId(UUID.randomUUID().toString().replaceAll("-", ""));//证书 ID
  36.         entity.setLicenceName("我的 Licence 证书");//证书名称
  37.         entity.setMac("");//客户端机器的网卡物理地址,要么留空,要么输入客户端的 Mac 地址
  38.         entity.setEffectStartDate("2024-04-15");//证书生效开始日期,格式:yyyy-MM-dd
  39.         entity.setEffectEndDate("2025-04-15");//证书生效结束日期,格式:yyyy-MM-dd
  40.         entity.setContactName("CSDN 流放深圳");//联系人
  41.         entity.setContactWay("https://blog.csdn.net/BiandanLoveyou");//联系方式
  42.         entity.setOwner("CSDN 流放深圳");//证书所有者
  43.         return entity;
  44.     }
  45. }
复制代码
说明:
   1、管理背景的业务流程一样平常是先生成证书文件,然后把证书文件下载,给到客户端。
  2、如果须要严酷的校验一个证书对应一台服务器,那就须要赋值 mac 字段
  3、别的字段可以按需赋值
  更多详细详情,请查看源代码。

3.3 证书校验焦点 Jar 代码实现

代码结构:

代码说明:
   1、CheckLicence:证书注解类。一样平常来说,都是全部的接口都要校验证书是否有效,但是在一些场景下,只须要一部分接口校验证书是否有效,比如:可以让客户利用基础的功能,如果涉及到焦点的功能,就须要证书授权。在业务层(Controller)加了这个注解,表现该方法须要校验证书是否有效。固然,还可以通过 yml 设置文件来设置是否全量校验证书。详细看代码。
  
  2、DirectoryInitializer:初始化证书的目录。在项目启动后,会在项目下创建用来存放证书文件的目录,方便程序去找到证书文件来解析。
  
  3、LicenceInterceptor:web拦截器。在哀求进入业务层(Controller)做一个前置的拦截,用来判断证书是否有效,有效才放行。
  
  4、LicenceWebConfig:WebMVC设置类,用来设置 LicenceInterceptor 拦截器。拦截所有的哀求。
  
  5、CommonKeys:定义常量类。
  
  6、LicenceEntity:证书模型实体类。与管理背景的模型字段保持同等。
  
  7、LicenceEnum:证书校验信息枚举类。证书校验会有很多种类的异常,可以在这里同一定义。
  
  8、LicenceExceptionAdvance:全局异常捕捉类。这是企业级开辟基本会有的内容,这里只捕捉了运行时异常。而全局异常捕捉,应该交给外层去处理。
  
  9、LicenceRuntimeException:运行时异常。用来给利用 jar 包的程序抛出运行时异常信息。
  
  10、LicenceJob:证书校验的定时使命类。现在设定 10 分钟执行一次,判断证书是否有效,并把有效(或者无效)的信息放入到内存(LicenceInterceptor 拦截器利用)中。避免每次哀求都去读取证书文件再判断,那样的话性能急剧降低。10分钟的频率还可以接受,如果要更换证书文件,最长的时间窗口就是等待 10 分钟。如果旧的证书还没过期,更换新的证书,那就没有等待期。
  
  11、LicenceCheckServiceImpl:证书校验的焦点实现类。详细看代码,备注齐全!
  
  12、LicenceCheckService:证书校验的接口类。
  
  13、CallResult:接口调用同一返回对象。
  
  14、LicenceDateUtil:Java原生的日期处理工具类。
  
  15、LicenceJsonUtil:Java原生的 JSON 工具类。
  
  16、LicenceSecurityUtil:Java原生的非对称加、解密工具类。
  
  17、MachineAddrUtil:Java原生的呆板信息获取工具类。
  
  说明:这里的工具类全部用 Java 原生的代码编写,重要是避免依靠第三方组件。如果依靠第三方组件,那就要把第三方组件的 Jar 包也打进来,就会导致“胖 Jar 包”,显得臃肿。一样平常来说,良好的开源组件,都是“瘦 Jar 包”,自己封装 Java 原生的代码自己利用,极少引入第三方组件。
  
pom.xml
  1.     <dependencies>
  2.         <!-- web支持,注意:请一定要在引入该 jar 包的主程序中增加 web 支持(2.x 以上的版本),否则拦截器将失效!! -->
  3.         <dependency>
  4.             <groupId>org.springframework.boot</groupId>
  5.             <artifactId>spring-boot-starter-web</artifactId>
  6.         </dependency>
  7.     </dependencies>
复制代码
注意:
   由于 core 焦点包利用到 web 拦截器,所以须要 web 组件的依靠。本项目利用的是 SpringBoot 框架,如果是 SpringMVC 框架则不适用或者 SSH 框架则不适用。
  在 core 的 pom 里加这个依靠,重要是提示利用者,须要在外层的应用中有这个依靠。此依靠的版本要求是 SpringBoot 2.X 以上。
  如果没有加这个依靠,core 的焦点代码就会失效,也就是无法校验证书。
  补充:
  如果 SpringBoot 项目的启动类有加基础包的扫描 @ComponentScan(basePackages = "xx.xxx"),请一定要加上 core 包对应Java类的扫描,否则 Spring 容器无法管理 core 包里的 bean,导致无法校验证书。
  更多详细详情,请查看源代码。

3.4 客户端代码实现及注意事项

3.4.1 client 客户端

代码结构:

说明:
   1、pom.xml 重要是利用 maven 的方式依靠了 core 包,这种方式一样平常是项目都在同一个父级中开辟,直接利用 maven 坐标就可以找到 core 包。
  1.     <dependencies>
  2.         <!-- core 包的依赖 -->
  3.         <dependency>
  4.             <groupId>com.study</groupId>
  5.             <artifactId>core</artifactId>
  6.             <version>1.0.0.RELEASE</version>
  7.             <scope>compile</scope>
  8.         </dependency>
  9.     </dependencies>
复制代码
2、业务层只是简朴校验是否可以访问。由于 core 里面利用了【拦截器】来校验证书,所以只须要一个 Controller 就可以测试证书的有效性了。
  1.     @GetMapping("/hello")
  2.     public CallResult hello() {
  3.         return CallResult.success("证书验证通过!If you can see this message, it means your licence is effective.");
  4.     }
复制代码

  
3.4.2 client-offline

代码结构:

说明:
   client-offline 跟 client 差不多。但是须要注意的是,离线版的客户端,须要我们把 core 包打好,复制到 resource 下的 lib 文件夹,然后作为第三方库加入进来利用。
  offline-client 的 pom.xml 代码如下:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5.     <parent>
  6.         <artifactId>licence</artifactId>
  7.         <groupId>com.study</groupId>
  8.         <version>1.0.0.RELEASE</version>
  9.     </parent>
  10.     <modelVersion>4.0.0</modelVersion>
  11.     <artifactId>client-offline</artifactId>
  12.     <properties>
  13.         <maven.compiler.source>8</maven.compiler.source>
  14.         <maven.compiler.target>8</maven.compiler.target>
  15.     </properties>
  16.     <dependencies>
  17.         <dependency>
  18.             <groupId>com.study</groupId>
  19.             <artifactId>core</artifactId>
  20.             <version>1.0.0.RELEASE</version>
  21.             <!-- 引用一个本地的 JAR 文件,而不是从 Maven 的中央仓库或其他远程仓库中获取 -->
  22.             <scope>system</scope>
  23.             <!-- 指定该 JAR 文件的路径 -->
  24.             <systemPath>${project.basedir}/src/main/resources/lib/core-1.0.0.RELEASE.jar</systemPath>
  25.         </dependency>
  26.     </dependencies>
  27.     <!-- 构建工具 -->
  28.     <build>
  29.         <plugins>
  30.             <!-- 打包插件 -->
  31.             <plugin>
  32.                 <groupId>org.springframework.boot</groupId>
  33.                 <artifactId>spring-boot-maven-plugin</artifactId>
  34.                 <configuration>
  35.                     <includeSystemScope>true</includeSystemScope>
  36.                 </configuration>
  37.             </plugin>
  38.         </plugins>
  39.     </build>
  40. </project>
复制代码
补充:
   请在启动类增加注解:@ComponentScan(basePackages = "com.study"),默认扫描的包,把 core 包加入Spring容器管理。
  
3.5 利用过程详解(保姆式)

3.5.1 启动管理背景,生成证书

启动好管理背景,利用 POST 方式调用接口(利用 Postman 或者 Apipost 工具都可以):
http://127.0.0.1:9000/server/licence/downLoadLicence
Apipost 测试效果(点击右边箭头处可以下载文档): 


另外,可以在项目下的 certificate 目录看到 licence.txt 证书文件


3.5.2 启动 client 客户端

可以看到项目启动后,就立刻执行 core 包里的定时使命 LicenceJob 里面校验证书的方法。

访问接口:http://127.0.0.1:8000/client/licence/hello

证书校验通过。

3.5.3 处理  client-offline 客户端

client-offline 是通过导入 core 的 jar 包方式的,然后再通过 maven 坐标依靠进去,所以须要单独处理。
起首把 core 包打出来。在 IDEA 工具右侧 Maven,找到 core 包下的 package,双击:

效果:

去对应的目录下,找到该 jar 包:

把打好的 core-1.0.0.RELEASE.jar 复制出来,粘贴到 client-offline 的 resource 下的 lib 目录下。
这时间 pom.xml 里的设置就会读取到放在 resource 下的 lib 目录下的 jar 包。
启动 client-offline 服务:

 访问接口:http://127.0.0.1:8888/offline/hello
效果(中文乱码不要紧,测试而已):

至此,完整流程搞定。
   剩下另有几个内容可以自己去验证:
  1、mac 所在,验证一台呆板是否对应一个 licence
  2、验证证书的有效起止时间
  3、@CheckLicence 注解的验证,看下非全量验证的情况下,加与不加 @CheckLicence 注解是否正常放行。
  4、如果客户端不加 core 包须要的 web 依靠,证书验证是否见效
  5、修改证书的部分内容(特别是 content)部分,并且比及下一个定时使命运行,看下证书的校验是否通过。
  
Gitee 堆栈所在:https://gitee.com/biandanLoveyou/licence


—  end —






免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4