侧边栏壁纸
博主头像
昂洋编程 博主等级

鸟随鸾凤飞腾远,人伴贤良品自高

  • 累计撰写 71 篇文章
  • 累计创建 79 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Springboot集成TrueLicense生成证书&校验证书(含校验Mac地址&CPU序列号&过期时间)

Administrator
2024-09-09 / 0 评论 / 0 点赞 / 46 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2024-09-09,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

参考 SpringBoot实现License认证(只校验有效期),扩展了校验Mac地址和CPU序列

License也就是版权许可证书,一般用于 收费软件给付费用户提供的 访问许可证明

License授权原理

  1. 生成密钥对,使用Keytool生成公私钥证书库
  2. 授权者保留私钥,使用私钥和使用日期生成证书license
  3. 公钥与生成的证书给使用者(放在验证的代码中使用),验证证书license是否在有效期内

生成密钥对

如果系统安装了JDK并配置好了环境,则可以直接使用下面的keytool命令

## 1. 生成私匙库
# validity:私钥的有效期(单位:天)
# alias:私钥别称
# keystore: 私钥库文件名称(生成在当前目录)
# storepass:私钥库的密码(获取keystore信息所需的密码) 
# keypass:私钥的密码
# dname 证书个人信息
# 	CN 为你的姓名
#	OU 为你的组织单位名称
#	O 为你的组织名称
#	L 为你所在的城市名称
#	ST 为你所在的省份名称
#	C 为你的国家名称 或 区号
keytool -genkeypair -keysize 1024 -validity 3650 -alias "ayPrivateSecret" -keystore "privateKeys.keystore" -storepass "N30fft1&r8x6pzg" -keypass "1lige.364" -dname "CN=localhost, OU=localhost, O=localhost, L=SH, ST=SH, C=CN"

## 2. 把私匙库内的公匙导出到一个文件当中
# alias:私钥别称
# keystore:私钥库的名称(在当前目录查找)
# storepass: 私钥库的密码
# file:证书名称
keytool -exportcert -alias "ayPrivateSecret" -keystore "privateKeys.keystore" -storepass "N30fft1&r8x6pzg" -file "certfile.cer"

## 3. 再把这个证书文件导入到公匙库
# alias:公钥别称
# file:证书名称
# keystore:公钥文件名称(生成在当前目录)
# storepass:私钥库的密码
keytool -import -alias "ayPublicSecret" -file "certfile.cer" -keystore "publicCerts.keystore" -storepass "N30fft1&r8x6pzg"

  • 上述命令执行完成后会在当前目录生成三个文件:
  1. certfile.cer 认证证书文件,已经没用了,可以删除
  2. privateKeys.keystore 私钥文件,自己保存,以后用于生成license.lic证书
  3. publicKeys.keystore 公钥文件,以后会和license.lic证书一起放到使用者项目里

授权者端

pom.xml

<!-- License -->
<dependency>
    <groupId>de.schlichtherle.truelicense</groupId>
    <artifactId>truelicense-core</artifactId>
    <version>1.33</version>
</dependency>

application.yml

# 证书配置
license:
  # 证书主题
  subject: AY_LICENSE
  # 私钥别名
  private-alias: ayPrivateSecret
  # 私钥密码
  key-pass: '1lige.364'
  # 私钥库的密码
  store-pass: 'N30fft1&r8x6pzg'
  # 私钥存储路径
  private-keys-store-path: license/privateKeys.keystore

注意私钥文件privateKeys.keystore要放到项目resources/license/下

License实体类

package com.angum.licenceserver.config;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

/**
 * 自定义生成licence的参数
 */
@Data
public class LicenseCreatorParam {

    /**
     * 证书subject
     */
    private String subject;

    /**
     * 密钥别称
     */
    private String privateAlias;

    /**
     * 公钥密码(需要妥善保管,不能让使用者知道)
     */
    private String keyPass;

    /**
     * 私钥库的密码
     */
    private String storePass;

    /**
     * 证书生成路径
     */
    private String licensePath;

    /**
     * 私钥存储路径
     */
    private String privateKeysStorePath;

    /**
     * 证书生效时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date issuedTime;

    /**
     * 证书失效时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date expiryTime;

    /**
     * 用户类型
     */
    private String consumerType;

    /**
     * 用户数量
     */
    private Integer consumerAmount;

    /**
     * 描述信息
     */
    private String description;

    /**
     * Mac地址
     */
    private String macAddress;

    /**
     * CPU序列号
     */
    private String cpuSerial;
}

后面两个字段为Mac地址和CPU序列号,这个后面生成证书以及校验证书的时候会单独处理,其他字段为TrueLicense框架需要用到的参数

自定义KeyStoreParam

package com.angum.licenceserver.config;

import de.schlichtherle.license.AbstractKeyStoreParam;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

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(Paths.get(storePath));
    }
}

生成证书工具类LicenseCreator

package com.angum.licenceserver.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import de.schlichtherle.license.*;
import lombok.extern.slf4j.Slf4j;

import javax.security.auth.x500.X500Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.prefs.Preferences;

@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");

    /**
     * 生成License证书
     * @param param 证书生成参数
     */
    public static byte[] generateLicense(LicenseCreatorParam param) {
        try {
            // 初始化证书管理器
            LicenseManager licenseManager = new LicenseManager(initLicenseParam(param));
            // 设置证书内容
            LicenseContent licenseContent = initLicenseContent(param);
            // 保存证书
            return licenseManager.create(licenseContent);
        } catch (Exception e) {
            log.error("证书生成失败", e);
            return null;
        }
    }

    /**
     * 初始化证书生成参数
     * @param param
     * @return
     */
    private static LicenseParam initLicenseParam(LicenseCreatorParam param) {
        Preferences preferences = Preferences.userNodeForPackage(LicenseCreator.class);
        // 设置对证书内容加密的秘钥
        CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
        // 自定义KeyStoreParam
        KeyStoreParam privateStoreParam = new CustomKeyStoreParam(LicenseCreator.class
                , param.getPrivateKeysStorePath()
                , param.getPrivateAlias()
                , param.getStorePass()
                , param.getKeyPass());

        // 组织License参数
        return new DefaultLicenseParam(param.getSubject()
                , preferences
                , privateStoreParam
                , cipherParam);
    }

    /**
     * 设置证书生成正文信息
     * @param param
     * @return
     */
    private static LicenseContent initLicenseContent(LicenseCreatorParam param) {
        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.getExpiryTime());
        licenseContent.setConsumerType(param.getConsumerType());
        licenseContent.setConsumerAmount(param.getConsumerAmount());
        licenseContent.setInfo(param.getDescription());

        // 创建包含 MAC 地址和 CPU 序列的自定义信息
        Map<String, Object> extraInfo = new HashMap<>();
        extraInfo.put("macAddress", param.getMacAddress());
        extraInfo.put("cpuSerial", param.getCpuSerial());
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            String extraJson = objectMapper.writeValueAsString(extraInfo);
            licenseContent.setExtra(extraJson);
        } catch (Exception e) {
            log.error("设置额外信息失败", e);
        }
        return licenseContent;
    }
}

因为Mac地址和CPU序列不在原生的TrueLicense框架中,但框架支持扩展自定义字段,放到extra中即可

image-suyq.png

生成证书逻辑

package com.angum.licenceserver.rest;

import com.angum.licenceserver.service.LicenseService;
import com.angum.licenceserver.config.LicenseCreatorParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/license")
public class LicenseController {

    @Autowired
    private LicenseService licenseService;

    @PostMapping("/generate")
    public void generate(@RequestBody LicenseCreatorParam param, @RequestHeader(name = "Authorization") String token,
                         HttpServletRequest request, HttpServletResponse response) {
        this.licenseService.generate(param, token, request, response);
    }
}

package com.angum.licenceserver.service;

import cn.hutool.core.util.ObjectUtil;
import com.angum.licenceserver.config.LicenseCreator;
import com.angum.licenceserver.config.LicenseCreatorParam;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Date;

@Slf4j
@Service
public class LicenseServiceImpl implements LicenseService{

    @Autowired
    private ResourceLoader resourceLoader;

    @Value("${license.subject}")
    private String subject;

    @Value("${license.private-alias}")
    private String privateAlias;

    @Value("${license.key-pass}")
    private String keyPass;

    @Value("${license.store-pass}")
    private String storePass;

    @Value("${license.private-keys-store-path}")
    private String privateKeysStorePath;

    // 这个是自定义token,用于防止随意发起请求生成证书
    private final String defaultToken = "xxxxxx";

    @Override
    public void generate(LicenseCreatorParam param, String token, HttpServletRequest request, HttpServletResponse response) {
        Date now = new Date();
        if (ObjectUtil.isEmpty(token) || !defaultToken.equals(token)) {
            throw new RuntimeException("请求头缺少参数...");
        }
        // 校验必填参数
        String checkParam = checkParam(param);
        if (ObjectUtil.isNotEmpty(checkParam)) {
            throw new RuntimeException(checkParam);
        }
        // 初始化固定参数
        param.setSubject(subject);
        param.setPrivateAlias(privateAlias);
        param.setKeyPass(keyPass);
        param.setStorePass(storePass);
        // 生效时间-立即生效
        param.setIssuedTime(now);
        // 私钥存储路径
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(privateKeysStorePath);
        if (inputStream == null) {
            throw new RuntimeException("私钥文件不存在");
        }
        File tempFile = null;
        try {
            // 拷贝文件到临时目录--主要为了解决打成jar包不能直接访问classpath下的文件的问题
            tempFile = File.createTempFile("privateKeys", ".keystore");
            Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            param.setPrivateKeysStorePath(tempFile.getAbsolutePath());
            param.setConsumerType("user");
            param.setConsumerAmount(1);
            param.setDescription("授权证书");
            // 生成证书
            byte[] licenseBytes = LicenseCreator.generateLicense(param);
            // 返回证书
            this.downloadFile(request, response, licenseBytes);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            // 删除临时文件
            if (tempFile != null && tempFile.exists()) {
                tempFile.delete();
            }
        }
    }

    private void downloadFile(HttpServletRequest request, HttpServletResponse response, byte[] licenseBytes) {
        response.setCharacterEncoding(request.getCharacterEncoding());
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=license.lic");
        ByteArrayInputStream inputStream = null;
        try {
            inputStream = new ByteArrayInputStream(licenseBytes);
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("证书下载失败");
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * 内部方法--校验参数
     * @param param
     * @return
     */
    private String checkParam(LicenseCreatorParam param) {
        StringBuilder errorMsg = new StringBuilder();
        if (ObjectUtil.isEmpty(param.getExpiryTime())) {
            errorMsg.append("证书失效时间【expiryTime】、");
        }
        if (ObjectUtil.isEmpty(param.getMacAddress())) {
            errorMsg.append("Mac地址【macAddress】、");
        }
        if (ObjectUtil.isEmpty(param.getMacAddress())) {
            errorMsg.append("CPU序列号【cpuSerial】不能为空");
        }
        return errorMsg.toString();
    }
}

到这里授权者端就完成了

使用者端

pom.xml

<!-- trueLicense -->
        <dependency>
            <groupId>de.schlichtherle.truelicense</groupId>
            <artifactId>truelicense-core</artifactId>
            <version>1.33</version>
        </dependency>

application.yml

# 证书配置
license:
  # 证书启用
  enable: true
  # 证书主题
  subject: AY_LICENSE
  # 公钥别名
  public-alias: ayPublicSecret
  # 公钥库密码
  store-pass: 'N30fft1&r8x6pzg'
  # 公钥路径
  public-keys-store-path: license/publicCerts.keystore
  # 上传路径 -- 用户目录拼接下面的路径
  upload-path: /jinzhu/license/license.lic
  • 注意
    1. enable用于是否启用证书校验,总开关;
    2. 公钥库密码store-pass是和授权者端一致的;
    3. 需要将授权者提供的公钥文件publicCerts.keystore放到项目resources/license/下;
    4. 上传路径upload-path用于定义上传授权文件的目录,代码里有在前面拼接用户home目录

License校验实体类

package com.mdd.admin.license;

import lombok.Data;

@Data
public class LicenseVerifyParam {

    /**
     * 证书subject
     */
    private String subject;

    /**
     * 公钥别称
     */
    private String publicAlias;

    /**
     * 访问公钥库的密码
     */
    private String storePass;

    /**
     * 证书路径
     */
    private String licensePath;

    /**
     * 密钥库存储路径
     */
    private String publicKeysStorePath;
}

自定义KeyStoreParam(与授权者一样)

package com.mdd.admin.license;

import de.schlichtherle.license.AbstractKeyStoreParam;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

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(Paths.get(storePath));
    }
}

证书管理类LicenseManagerHolder--单例获取

package com.mdd.admin.license;

import de.schlichtherle.license.LicenseManager;
import de.schlichtherle.license.LicenseParam;

/**
 * 单例获取证书管理类LicenseManager
 */
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 LicenseManager(param);
                }
            }
        }
        return LICENSE_MANAGER;
    }
}

证书校验工具类

package com.mdd.admin.license;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSONObject;
import com.mdd.common.util.ServerUtil;
import de.schlichtherle.license.*;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.prefs.Preferences;

/**
 * 证书校验工具类
 */
@Slf4j
public class LicenseVerify {

    /**
     * 安装License证书
     * @param param
     * @return
     */
    public static synchronized LicenseContent install(LicenseVerifyParam param) {
        LicenseContent result = null;
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 1. 安装证书
        try {
            LicenseManager licenseManager = LicenseManagerHolder.getInstance(initLicenseParam(param));
            licenseManager.uninstall();
            result = licenseManager.install(new File(param.getLicensePath()));
            log.info(MessageFormat.format("证书安装成功,证书有效期:{0} - {1}",
                    format.format(result.getNotBefore()), format.format(result.getNotAfter())));
        } catch (Exception e) {
            log.error("证书安装失败: {}", e.getMessage());
        }
        return result;
    }

    /**
     * 校验License证书
     * @return
     */
    public static boolean verify() {
        LicenseManager licenseManager = LicenseManagerHolder.getInstance(null);
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 2. 校验证书
        try {
            LicenseContent licenseContent = licenseManager.verify();
            // 校验Mac地址何CPU序列号
            String localMacAddress = ServerUtil.getMacAddress();
            String localCPUSerial = ServerUtil.getCPUSerial();

            Object extra = licenseContent.getExtra();
            JSONObject extraInfo = JSONObject.parseObject(String.valueOf(extra));
            if (ObjectUtil.isEmpty(extraInfo) || !localMacAddress.equals(extraInfo.getString("macAddress"))
                    || !localCPUSerial.equals(extraInfo.getString("cpuSerial"))) {
                log.error("证书校验失败:Mac地址或CPU序列错误");
                return false;
            }
            log.info(MessageFormat.format("证书校验通过,证书有效期:{0} - {1}",
                    format.format(licenseContent.getNotBefore()), format.format(licenseContent.getNotAfter())));
            return true;
        } catch (Exception e) {
            log.error("证书校验失败: {}", e.getMessage());
            return false;
        }
    }

    // 初始化证书生成参数
    private static LicenseParam initLicenseParam(LicenseVerifyParam param) {
        Preferences preferences = Preferences.userNodeForPackage(LicenseVerify.class);
        CipherParam cipherParam = new DefaultCipherParam(param.getStorePass());
        KeyStoreParam publicStoreParam = new CustomKeyStoreParam(LicenseVerify.class
                , param.getPublicKeysStorePath()
                , param.getPublicAlias()
                , param.getStorePass()
                , null);
        return new DefaultLicenseParam(param.getSubject()
                , preferences
                , publicStoreParam
                , cipherParam);
    }
}

注意校验方法verify()中咱们自定义校验Mac地址或CPU序列

用于获取MAC地址和CPU序列号的工具类ServerUtil

package com.mdd.common.util;

import cn.hutool.core.net.NetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * 服务器工具类
 */
public class ServerUtil {

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

    /**
     * 获取Mac地址
     * @return
     */
    public static String getMacAddress() {
        return NetUtil.getLocalMacAddress();
    }

    /**
     * 获取CPU序列号
     * @return
     */
    public static String getCPUSerial() {
        String serialNumber = "";
        try {
            Process process;
            // 针对不同操作系统分别处理
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                // Windows 系统使用 wmic 命令
                process = Runtime.getRuntime().exec(new String[] { "wmic", "cpu", "get", "ProcessorId" });
            } else {
                // Linux/Unix 系统可以使用 dmidecode 命令
                process = Runtime.getRuntime().exec("dmidecode -t processor | grep ID");
            }

            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                if (!line.trim().isEmpty() && !line.contains("ProcessorId")) {
                    serialNumber = line.trim();
                    break;
                }
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return serialNumber;
    }
}

接下来提供接口用于前端下载机器码和上传授权文件

下载机器码接口

@GetMapping("/downloadMachineCode")
    @ApiOperation(value="下载机器码")
    public void downloadMachineCode(HttpServletRequest request, HttpServletResponse response) {
        this.iSystemLoginService.downloadMachineCode(request, response);
    }
@Value("${license.subject}")
    private String licenseSubject;
    @Value("${license.public-alias}")
    private String licensePublicAlias;
    @Value("${license.store-pass}")
    private String licenseStorePass;
    @Value("${license.public-keys-store-path}")
    private String licensePublicKeysStorePath;
    @Value("${license.upload-path}")
    private String licenseUploadPath;

@Override
    public void downloadMachineCode(HttpServletRequest request, HttpServletResponse response) {
        // 获取Mac地址
        String macAddress = ServerUtil.getMacAddress();
        // 获取CPU序列号
        String cpuSerial = ServerUtil.getCPUSerial();
        // 下载
        this.downloadFile(request, response, (macAddress + ":" + cpuSerial).getBytes(StandardCharsets.UTF_8));
    }

/**
     * 内部方法-下载机器码
     * @param request
     * @param response
     * @param fileBytes
     */
    private void downloadFile(HttpServletRequest request, HttpServletResponse response, byte[] fileBytes) {
        response.setCharacterEncoding(request.getCharacterEncoding());
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment; filename=machineCode.txt");
        ByteArrayInputStream inputStream = null;
        try {
            inputStream = new ByteArrayInputStream(fileBytes);
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException("机器码下载失败");
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        }
    }

上传授权文件接口

@PostMapping("/uploadLicense")
    @ApiOperation(value="上传证书文件")
    public AjaxResult<Object> uploadLicense(HttpServletRequest request, @RequestParam("file") MultipartFile file) {
        return AjaxResult.success(iSystemLoginService.uploadLicense(request, file));
    }
@Value("${license.subject}")
    private String licenseSubject;
    @Value("${license.public-alias}")
    private String licensePublicAlias;
    @Value("${license.store-pass}")
    private String licenseStorePass;
    @Value("${license.public-keys-store-path}")
    private String licensePublicKeysStorePath;
    @Value("${license.upload-path}")
    private String licenseUploadPath;

@Override
    public String uploadLicense(HttpServletRequest request, MultipartFile file) {
        if (file.isEmpty()) {
            throw new BadRequestException("请选择证书文件...");
        }
        try {
            // 保存证书文件
            FileUtil.writeFromStream(file.getInputStream(), new File(System.getProperty("user.home") + licenseUploadPath));
            // 安装证书
            LicenseVerifyParam param = new LicenseVerifyParam();
            param.setSubject(licenseSubject);
            param.setPublicAlias(licensePublicAlias);
            param.setStorePass(licenseStorePass);
            // 公钥文件路径
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream(licensePublicKeysStorePath);
            if (inputStream == null) {
                throw new LicenseException("公钥文件不存在");
            }
            // 拷贝文件到临时目录--主要为了解决打成jar包不能直接访问classpath下的文件的问题
            tempFile = File.createTempFile("publicCerts", ".keystore");
            Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
            param.setPublicKeysStorePath(tempFile.getAbsolutePath());
            // 授权文件上传路径
            param.setLicensePath(System.getProperty("user.home") + licenseUploadPath);
            LicenseVerify.install(param);
            // 校验证书
            boolean licenseVerify = LicenseVerify.verify();
            if (licenseVerify) {
                return "授权成功";
            } else {
                throw new LicenseException("授权失败");
            }
        } catch (Exception e) {
            throw new LicenseException(e.getMessage());
        } finally {
            // 删除临时文件
            if (tempFile != null && tempFile.exists()) {
                tempFile.delete();
            }
        }
    }

这里的FileUtil为hutool工具类,LicenseException为自定义异常类

package com.mdd.common.exception;

/**
 * 自定义证书异常
 */
public class LicenseException extends RuntimeException {

    public LicenseException(String errorMsg) {
        super(errorMsg);
    }
}

OK,完事具备,接下来考虑如何触发校验证书:

  • 博客原文使用的是拦截器对每个接口拦截进行校验;
  • 我这里采取另一种方案,即只在登录的时候校验,因为这个是后端管理系统,绝大多数接口都是要先登录才能操作;
  • 如果是门户前端页面建议还是所有接口拦截器校验

登录校验证书

@Value("${license.enable}")
    private boolean licenseEnable;

// 校验证书
            if (this.licenseEnable) {
                boolean licenseVerify = false;
                try {
                    licenseVerify = LicenseVerify.verify();
                    if (!licenseVerify) {
                        throw new LicenseException("请先申请软件授权");
                    }
                } catch (Exception e) {
                    throw new LicenseException("请先申请软件授权");
                }
            }

测试

下载机器码

如下图我这里写好了下载机器码和导入授权文件页

image-zasq.png

下载好的机器码文件machineCode.txt文件内容就一行 2c-f0-5d-3b-aa-23:BFEBFBFF000A0655 ,冒号前面是MAC地址,冒号后面是CPU序列号

生成证书,这里就使用postman调用接口

image-btco.png

image-daoy.png

拿到授权文件license.lic,接下来上传文件

上传授权文件

image-hmmp.png

可以看到后台安装证书成功日志

其他说明

这里虽然我们校验了MAC地址和CPU序列,但有一些问题

  • Mac地址容易变,比如加装、卸载网卡就会发生变化,甚至安装虚拟网卡,比如vmware、vbox、docker等都会导致Mac地址发生变化;
  • CPU序列一般不会变化,通常只有更换硬件才会发生变化,如果是虚拟机的话通常也是固定的,如果没手动改的话
0

评论区