软件产品许可证书 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 代码结构

说明:

  1. certificate:这是存放证书的目录,在开发阶段我们方便去做演示。如果实际应用,要考虑 Linux 环境和 docker 环境(需要考虑挂载)。后续如果更换证书,直接用新证书替换旧的证书即可,无需重启服务,对业务没有任何影响
  2. client:简单的测试客户端,代码只有几行。
  3. client-offline:私有化部署时的客户端,需要把 core 包打进去,代码也是几行,做演示
  4. core:证书的核心校验包(重点)
  5. server:管理后台或者用于生成证书的服务,一般是业务系统的管理平台。

3.2 管理后台(服务端)代码实现

管理后台代码结构:

3.2.1 证书模型介绍

证书的模型(实体)我们写在 LicenceEntity 这个实体类,核心字段如下:

public class LicenceEntity implements Serializable {

    private static final long serialVersionUID = -4048081970386334457L;

    /**
     * 证书 ID
     */
    private String licenceId;

    /**
     * 证书名称
     */
    private String licenceName;

    /**
     * 客户端机器的网卡物理地址
     */
    private String mac;

    /**
     * 秘钥(指的是公钥)
     */
    private String key;

    /**
     * 证书生效开始日期,格式:yyyy-MM-dd
     */
    private String effectStartDate;

    /**
     * 证书生效结束日期,格式:yyyy-MM-dd
     */
    private String effectEndDate;

    /**
     * 颁发证书联系人
     */
    private String contactName;

    /**
     * 颁发证书人的联系方式
     */
    private String contactWay;

    /**
     * 证书所有者
     */
    private String owner;

    /**
     * 加密的内容。这个字段是将其它字段加密后的完整内容
     */
    private String content;

}

说明

1、mac:这是客户端的网卡物理地址,每台机器的物理地址都不一样,基本上可以保证你们公司服务的客户是唯一的。这个 mac 地址,一般是负责部署的人员去获取,或者让客户自己提供。这个字段的意义在于:如果你们公司要进行严格的证书校验,必须是一个证书只允许在一台服务器上使用,那这个字段就显得非常重要。当然,你可以换成自己喜欢的字段名。

2、effectStartDate、effectEndDate:证书的生效起止时间字段。这两个字段是证书的核心字段,用来判断证书是否在有效期内。

3、key:公钥。根据非对称加密的内容,我们需要把公钥对外,客户端拿到公钥后,可以解密我们用私钥加密的内容。

4、content:证书数据的加密内容。这个字段,是把证书实体转成 JSON 字符串后,再进行非对称加密后的数据。为什么要有这个字段呢?考虑的因素是:①充分利用了非对称加解密的技术,私钥加密的内容只允许公钥解密。如果这个数据被篡改,证书就失效。②其它字段的明文,比如:证书的生效起止时间、证书的联系人等,方便客户拿到证书后,直观的看到这些信息。

5、以上证书的字段,可以根据自己的业务需要去拓展。但是核心的几个字段,最好能保留。

3.2.2 核心实现类

管理后台的核心,就是准确的生成证书文件 licence.txt,核心代码如下:

package com.study.service.impl;

import com.study.constant.CommonKeys;
import com.study.entity.LicenceEntity;
import com.study.service.LicenceService;
import com.study.util.LicenceJsonUtil;
import com.study.util.NativeSecurityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * @author CSDN 流放深圳
 * @description 证书生成核心实现类
 * @create 2024-04-13 15:55
 * @since 1.0.0
 */
@Service
public class LicenceServiceImpl implements LicenceService {

    private static Logger log = LoggerFactory.getLogger(LicenceServiceImpl.class);

    /**
     * 获取配置文件的私钥
     */
    @Value("${licence.privateKey}")
    private String privateKey;

    /**
     * 获取配置文件的公钥
     */
    @Value("${licence.publicKey}")
    private String publicKey;


    /**
     * 创建证书 licence 内容
     * @param dtoEntity
     * @return
     */
    @Override
    public LicenceEntity createLicence(LicenceEntity dtoEntity) {
        LicenceEntity entity = new LicenceEntity();
        //赋值相同属性
        BeanUtils.copyProperties(dtoEntity, entity);
        //content 和 key 需要额外处理
        if(StringUtils.isEmpty(privateKey) || StringUtils.isEmpty(publicKey)) return null;
        entity.setKey(publicKey);//把公钥放进去,否则客户端无法获取公钥,就无法解密
        //把实体转成字符串
        String json = LicenceJsonUtil.objectToStr(entity);
        //把整个字符串加密
        String content = NativeSecurityUtil.encryptByPrivateKey(json, privateKey);
        //把加密后的字符串赋值给 content
        entity.setContent(content);
        return entity;
    }

    /**
     * 下载证书文件
     * @param dtoEntity
     * @param response
     */
    @Override
    public void downLoadLicence(LicenceEntity dtoEntity, HttpServletResponse response) {
        LicenceEntity licenceEntity = createLicence(dtoEntity);
        if(null == licenceEntity){
            log.error("证书的秘钥未配置!");
            return;
        }
        //把实体转为字符串
        String result = LicenceJsonUtil.objectToStr(licenceEntity);
        BufferedOutputStream out = null;
        try {
            //证书文件名
            String fileName = CommonKeys.CERTIFICATE_FILE;
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
            response.setCharacterEncoding("UTF-8");
            out = new BufferedOutputStream(response.getOutputStream());
            out.write(result.getBytes("UTF-8"));
            out.flush();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }

        //TODO 另外可以保存一份到指定目录下,方便测试(这是使用异步的形式)。上线的时候记得注释掉
        saveLicenceToCertificate(result);

    }

    /**
     * 异步保存证书文件到指定目录
     * @param result
     */
    @Async
    public void saveLicenceToCertificate(String result){
        try{
            // 创建证书目录(如果尚未创建)
            Path directory = Paths.get(CommonKeys.CERTIFICATE_DIRECTORY);
            Files.createDirectories(directory);
            // 构建证书文件的完整路径
            Path filePath = directory.resolve(CommonKeys.CERTIFICATE_FILE);
            // 检查文件是否存在,存在则删掉
            Files.deleteIfExists(filePath);
            //创建文件(如果文件已经存在,此步骤可能会抛出 FileAlreadyExistsException)
            Files.createFile(filePath);
            //写入内容到文件,使用 StandardOpenOption.APPEND 可以追加内容而不是覆盖
            Files.write(filePath, result.getBytes("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
        }catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

}

说明:

1、我们采用私钥加密的方式,把实体转为 JSON 字符串加密后赋值给 content 字段。

2、对外暴露公钥,用于给客户端拿到证书后,通过公钥解密出 content,然后做校验。

3.2.3 业务层

案例中的业务层 ServerController 比较简单,可以根据实际业务中去拓展,这里只做了模拟数据:

package com.study.controller;

import com.study.entity.LicenceEntity;
import com.study.service.LicenceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * @author CSDN 流放深圳
 * @description 控制层
 * @create 2024-04-13 15:55
 * @since 1.0.0
 */
@RestController
@RequestMapping("/server/licence")
public class ServerController {

    @Autowired
    private LicenceService licenceService;

    /**
     * 下载证书文件
     * 实际项目可以根据前端传递的参数来创建证书字段属性。这里为了测试,直接写测试数据
     * @param response
     */
    @PostMapping("/downLoadLicence")
    public void downLoadLicence(HttpServletResponse response){
        LicenceEntity dtoEntity = createEntity();
        licenceService.downLoadLicence(dtoEntity, response);
    }

    /**
     * 创建测试实体
     * @return
     */
    private LicenceEntity createEntity(){
        LicenceEntity entity = new LicenceEntity();
        entity.setLicenceId(UUID.randomUUID().toString().replaceAll("-", ""));//证书 ID
        entity.setLicenceName("我的 Licence 证书");//证书名称
        entity.setMac("");//客户端机器的网卡物理地址,要么留空,要么输入客户端的 Mac 地址
        entity.setEffectStartDate("2024-04-15");//证书生效开始日期,格式:yyyy-MM-dd
        entity.setEffectEndDate("2025-04-15");//证书生效结束日期,格式:yyyy-MM-dd
        entity.setContactName("CSDN 流放深圳");//联系人
        entity.setContactWay("https://blog.csdn.net/BiandanLoveyou");//联系方式
        entity.setOwner("CSDN 流放深圳");//证书所有者
        return entity;
    }

}

说明:

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

    <dependencies>
        <!-- web支持,注意:请一定要在引入该 jar 包的主程序中增加 web 支持(2.x 以上的版本),否则拦截器将失效!! -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </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 包。

    <dependencies>
        <!-- core 包的依赖 -->
        <dependency>
            <groupId>com.study</groupId>
            <artifactId>core</artifactId>
            <version>1.0.0.RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

2、业务层只是简单校验是否可以访问。因为 core 里面使用了【拦截器】来校验证书,所以只需要一个 Controller 就可以测试证书的有效性了。

    @GetMapping("/hello")
    public CallResult hello() {
        return CallResult.success("证书验证通过!If you can see this message, it means your licence is effective.");
    }

3.4.2 client-offline

代码结构:

说明:

client-offline 跟 client 差不多。但是需要注意的是,离线版的客户端,需要我们把 core 包打好,复制到 resource 下的 lib 文件夹,然后作为第三方库加入进来使用。

offline-client 的 pom.xml 代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>licence</artifactId>
        <groupId>com.study</groupId>
        <version>1.0.0.RELEASE</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>client-offline</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.study</groupId>
            <artifactId>core</artifactId>
            <version>1.0.0.RELEASE</version>
            <!-- 引用一个本地的 JAR 文件,而不是从 Maven 的中央仓库或其他远程仓库中获取 -->
            <scope>system</scope>
            <!-- 指定该 JAR 文件的路径 -->
            <systemPath>${project.basedir}/src/main/resources/lib/core-1.0.0.RELEASE.jar</systemPath>
        </dependency>
    </dependencies>

    <!-- 构建工具 -->
    <build>
        <plugins>
            <!-- 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
    </build>

</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 —

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Web 许可证(licence)是指用于授权使用 Java Web 技术的许可证。Java 是一种广泛使用的计算机编程语言,而 Java Web 则是基于 Java 的一种用于 Web 开发技术。Java Web 许可证是 Oracle 公司(之前是 Sun 公司)针对 Java Web 技术制定的许可协议。 根据 Java Web 许可证的规定,使用者可以获得 Java Web 技术的许可,从而可以使用 Java Web 技术进行 Web 应用的开发和部署。许可证的内容包括许可使用的范围、使用期限、许可费用等等。使用者需要遵守许可证的条款和限制,否则可能会面临法律风险。 Java Web 许可证的主要目的是保护 Java Web 技术的知识产权,并控制其使用范围。由于 Java Web 技术在 Web 开发领域具有广泛的应用和影响力,Oracle 公司通过许可证可以确保只有经过许可的使用者才能使用技术,从而维护了该技术的商业价值。 对于开发者和企业来说,遵守 Java Web 许可证是非常重要的。只有获得合法的许可证才能使用 Java Web 技术进行开发和部署。此外,还需要遵守许可证中关于知识产权、许可费用等方面的规定,避免违反许可证条款和限制,以免引发法律纠纷。 总结来说,Java Web 许可证是 Oracle 公司对 Java Web 技术授权许可,使用者需要获取合法的许可证并遵守其中的规定。通过许可证,Oracle 公司保护了 Java Web 技术的知识产权,并控制了其使用范围,同时也为开发者和企业提供了一种合法、有序的使用方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值