如何实现应用系统离线授权详解方案支持(SpringBoot、SpringCloud),可以防止修改系统的方式绕过授权


系统支持内置时间校验器,不依赖于系统时间戳, 可以防止修改系统的方式绕过授权
离线授权可以让应用系统在不请求外部网络的情况下进行授权验证的手段;也就是说应用自身持有一把锁,该锁相当于一个门闸,访问应用时需要需要使用门禁卡;在开门时会验证门禁卡是否已经过期,是否为当前小区的卡;而这些验证操作都不会依赖于第三方应用,自身就可以完成;

1、说明

离线授权方案分为授权申请和授权验证两个过程,其中授权申请在授权有效期内,只会进行一次;授权验证会验证每天的第一个请求或者定时任务验证一次,流程如下图:

离线授权申请流程:
在这里插入图片描述

离线授权验证流程:
在这里插入图片描述

2、平台系统

搭建系统: license-platform

2.1 加密工具类

主要使用了凯撒加密、RSA非对称加密,在工具类中生成私钥和公钥,并且将时间、机器码等参数通过私钥加密形成加密文件,在应用系统中使用公钥进行解密,流程如下:
在这里插入图片描述
代码实现 - EncryptUtil


import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;

import javax.crypto.Cipher;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

/**
 * @Author: LiHuaZhi
 * @Description: 加密工具类
 **/
public class EncryptUtil {

    public final static String RSA = "RSA";


    /**
     * 打印密钥对并且保存到文件
     * pubPath 公钥生成目录
     * priPath 私钥生成目录
     *
     * @return
     */
    public static void generateKeyPair(String pubPath, String priPath) {
        try {
            //  创建密钥对生成器对象
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
            // 生成密钥对
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            PrivateKey privateKey = keyPair.getPrivate();
            PublicKey publicKey = keyPair.getPublic();
            String privateKeyString = Base64.encode(privateKey.getEncoded());
            String publicKeyString = Base64.encode(publicKey.getEncoded());

            System.out.println("私钥:" + privateKeyString);
            System.out.println("公钥:" + publicKeyString);

            // 保存文件
            if (pubPath != null) {
                // 在公钥文件中追加一个当前时间戳,使用改时间与授权结束时间进行比较
                // 并且授权方会定时更新一个最新的时间,到公钥文件中
                // 获取比较时间
                String compareTime = CompareTimeUtil.generateCompareTime(startTime, licenseCode);
                publicKeyString = publicKeyString.concat("&").concat(compareTime);

                FileUtils.writeStringToFile(new File(pubPath), publicKeyString, String.valueOf(StandardCharsets.UTF_8));
            }
            if (priPath != null) {
                FileUtils.writeStringToFile(new File(priPath), privateKeyString, String.valueOf(StandardCharsets.UTF_8));
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("生成密钥对失败!");
        }
    }

    /**
     * 从文件中加载公钥
     *
     * @param filePath : 文件路径
     * @return : 公钥
     * @throws Exception
     */
    public static PublicKey loadPublicKeyFromFile(String filePath) {
        try {
            // 将文件内容转为字符串
            String keyString = FileUtils.readFileToString(new File(filePath), String.valueOf(StandardCharsets.UTF_8));

            return loadPublicKeyFromString(keyString);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取公钥文件字符串失败!");
        }
    }

    /**
     * 从文件中加载私钥
     *
     * @param filePath : 文件路径
     * @return : 私钥
     * @throws Exception
     */
    public static PrivateKey loadPrivateKeyFromFile(String filePath) {
        try {
            // 将文件内容转为字符串
            String keyString = FileUtils.readFileToString(new File(filePath), String.valueOf(StandardCharsets.UTF_8));
            return loadPrivateKeyFromString(keyString);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取私钥文件字符串失败!");
        }
    }

    /**
     * 从字符串中加载公钥
     *
     * @param keyString : 公钥
     * @return : 公钥
     * @throws Exception
     */
    public static PublicKey loadPublicKeyFromString(String keyString) {
        try {
            // 进行Base64解码
            byte[] decode = Base64.decode(keyString);
            // 获取密钥工厂
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            // 构建密钥规范
            X509EncodedKeySpec key = new X509EncodedKeySpec(decode);
            // 获取公钥
            return keyFactory.generatePublic(key);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取公钥失败!");
        }
    }

    /**
     * 从字符串中加载私钥
     *
     * @param keyString : 私钥
     * @return : 私钥
     * @throws Exception
     */
    public static PrivateKey loadPrivateKeyFromString(String keyString) {
        try {
            // 进行Base64解码
            byte[] decode = Base64.decode(keyString);
            // 获取密钥工厂
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            // 构建密钥规范
            PKCS8EncodedKeySpec key = new PKCS8EncodedKeySpec(decode);
            // 生成私钥
            return keyFactory.generatePrivate(key);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取私钥失败!");
        }
    }


    /**
     * 非对称加密数据
     *
     * @param input : 原文
     * @param key   : 密钥
     * @return : 密文
     * @throws Exception
     */
    public static String encryptByAsymmetric(String input, Key key) {
        try {
            // 获取Cipher对象
            Cipher cipher = Cipher.getInstance(RSA);
            // 初始化模式(加密)和密钥
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] resultBytes = getMaxResultEncrypt(input, cipher);
            return Base64.encode(resultBytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加密失败!");
        }
    }

    /**
     * 非对称解密数据
     *
     * @param encrypted : 密文
     * @param key       : 密钥
     * @return : 原文
     * @throws Exception
     */
    public static String decryptByAsymmetric(String encrypted, Key key) {
        try {
            // 获取Cipher对象
            Cipher cipher = Cipher.getInstance(RSA);
            // 初始化模式(解密)和密钥
            cipher.init(Cipher.DECRYPT_MODE, key);
            return new String(getMaxResultDecrypt(encrypted, cipher));
        } catch (
                Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密失败!");
        }
    }

    /**
     * 分段处理加密数据
     *
     * @param input  : 加密文本
     * @param cipher : Cipher对象
     * @return
     */
    private static byte[] getMaxResultEncrypt(String input, Cipher cipher) throws Exception {
        byte[] inputArray = input.getBytes();
        int inputLength = inputArray.length;
        // 最大加密字节数,超出最大字节数需要分组加密
        int MAX_ENCRYPT_BLOCK = 117;
        // 标识
        int offSet = 0;
        byte[] resultBytes = {};
        byte[] cache = {};
        while (inputLength - offSet > 0) {
            if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
                offSet += MAX_ENCRYPT_BLOCK;
            } else {
                cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
                offSet = inputLength;
            }
            resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
            System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
        }
        return resultBytes;
    }

    /**
     * 分段处理解密数据
     *
     * @param decryptText : 加密文本
     * @param cipher      : Cipher对象
     * @throws Exception
     */
    private static byte[] getMaxResultDecrypt(String decryptText, Cipher cipher) throws Exception {
        byte[] inputArray = Base64.decode(decryptText.getBytes(StandardCharsets.UTF_8));
        int inputLength = inputArray.length;

        // 最大解密字节数,超出最大字节数需要分组加密
        int MAX_ENCRYPT_BLOCK = 128;
        // 标识
        int offSet = 0;
        byte[] resultBytes = {};
        byte[] cache = {};
        while (inputLength - offSet > 0) {
            if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
                offSet += MAX_ENCRYPT_BLOCK;
            } else {
                cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
                offSet = inputLength;
            }
            resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
            System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
        }
        return resultBytes;
    }


    /**
     * 使用凯撒加密方式解密数据
     *
     * @param encryptedData :密文
     * @param key           :位移数量
     * @return : 源数据
     */
    public static String decryptKaiser(String encryptedData, int key) {
        // 将字符串转为字符数组
        char[] chars = encryptedData.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char aChar : chars) {
            // 获取字符的ASCII编码
            int asciiCode = aChar;
            // 偏移数据
            asciiCode -= key;
            // 将偏移后的数据转为字符
            char result = (char) asciiCode;
            // 拼接数据
            sb.append(result);
        }
        return sb.toString();
    }

    /**
     * 使用凯撒加密方式加密数据
     *
     * @param original :原文
     * @param key      :位移数量
     * @return :加密后的数据
     */
    public static String encryptKaiser(String original, int key) {
        // 将字符串转为字符数组
        char[] chars = original.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char aChar : chars) {
            // 获取字符的ascii编码
            int asciiCode = aChar;
            // 偏移数据
            asciiCode += key;
            // 将偏移后的数据转为字符
            char result = (char) asciiCode;
            // 拼接数据
            sb.append(result);
        }
        return sb.toString();
    }
}

2.2 授权工具类

将应用系统的机器码、授权日期、授权Key等参数通过EncryptUtil工具,把公钥、私钥、授权码以文件的形式输出到指定位置,再由平台管理员将公钥、授权吗发放给应用系统;

代码实现 - LicenseUtil


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: LiHuaZhi
 * @Description: 生成公钥私钥及授权码
 **/
@Slf4j
public class LicenseUtil {

    /**
     * 凯撒加密key
     */
    public final static Integer[] KAISER_KEY = {5, 11, 2, 19, 9, 12, 20, 8, 10};

    /**
     * 生成密钥对以及授权密文文件
     * 私钥加密,公钥解密
     * 加密时使用公钥加密,需要将生成的license.key和license.pub拷贝给用户,并且放到服务器的指定目录下
     * 私钥由平台管理员保存
     *
     * @param priPath     私钥文件地址 ,yml配置
     * @param licensePath 授权文件生成地址 ,yml配置
     * @param pubPath     公钥文件地址 ,yml配置
     * @param startTime   授权开始时间
     * @param endTime     授权开始时间
     * @param licenseCode 应用授权码,由用户通过接口根据一定规则生成
     * @return
     */
    public static void setLicense(String priPath, String licensePath, String pubPath, Long startTime, Long endTime, String licenseCode) {
        try {
            EncryptUtil.generateKeyPair(startTime, licenseCode, pubPath, priPath);

            // 生成随机key,只包含大写及数字
            String key = UUIDUtils.getUuId().toUpperCase();
            // 设置签名,key各字符的asc码,并且都为2位
            StringBuilder signBuilder = new StringBuilder();
            char[] chars = key.toCharArray();
            for (char c : chars) {
                String s = String.valueOf((int) c);
                if (s.length() != 2) {
                    throw new RuntimeException("生成的key格式错误");
                }
                signBuilder.append(s);
            }
            String sign = signBuilder.toString();

            // 生成参数
            StringBuilder paramBuilder = new StringBuilder();
            paramBuilder.append("key=").append(key).append("&startTime=").append(startTime)
                    .append("&endTime=").append(endTime)
                    .append("&sign=").append(sign)
                    .append("&licenseCode=").append(licenseCode);
            // 查看参数长度
            int length = paramBuilder.length();
            paramBuilder.append("&").append(length);
            String param = paramBuilder.toString();

            // 私钥加密参数
            // 从文件中加载私钥进行加密
            PrivateKey privateKey = EncryptUtil.loadPrivateKeyFromFile(priPath);
            // 公钥加密,私钥解密
            String encrypted = EncryptUtil.encryptByAsymmetric(param, privateKey);
            // 凯撒加密
            StringBuilder kaiserBuilder = new StringBuilder();
            char[] encryptedChars = encrypted.toCharArray();
            // 将私钥加密后密文的每个字符进行凯撒位移加密
            for (int index = 0; index < encryptedChars.length; index++) {
                int keyIndex = index % KAISER_KEY.length;
                char c = encryptedChars[index];
                String s = String.valueOf(c);
                String encryptKaiser = EncryptUtil.encryptKaiser(s, KAISER_KEY[keyIndex]);
                kaiserBuilder.append(encryptKaiser);
            }
            String encryptKaiser = kaiserBuilder.toString();

            // 将密文写入文件
            FileUtils.writeStringToFile(new File(licensePath), encryptKaiser, String.valueOf(StandardCharsets.UTF_8));
        } catch (Exception e) {
            log.error("生成授权文件失效!");
            throw new RuntimeException("生成授权文件失效!");
        }
    }


    /**
     * 自测授权文件与密钥是否正确
     * 加载授权密文文件进行校验
     * 私钥加密,公钥解密
     *
     * @param code        加密时用户的机器码
     * @param licensePath 授权文件位置
     * @param pubPath     公钥位置
     * @return
     */
    public static Map<String, String> testLicense(String code, String licensePath, String pubPath) {
        try {
            // 读取密文内容
            String licenseText = FileUtils.readFileToString(new File(licensePath), String.valueOf(StandardCharsets.UTF_8));
            // 凯撒解密
            StringBuilder kaiserBuilder = new StringBuilder();
            char[] decryptChars = licenseText.toCharArray();
            for (int index = 0; index < decryptChars.length; index++) {
                int keyIndex = index % KAISER_KEY.length;
                char c = decryptChars[index];
                String s = String.valueOf(c);
                String encryptKaiser = EncryptUtil.decryptKaiser(s, KAISER_KEY[keyIndex]);
                kaiserBuilder.append(encryptKaiser);
            }
            String decryptLicense = kaiserBuilder.toString();
            // 使用私密进行解密获取加密参数
            // 从文件中加载公钥
            PublicKey publicKey = EncryptUtil.loadPublicKeyFromFile(pubPath);
            // 获取原文参数
            String params = EncryptUtil.decryptByAsymmetric(decryptLicense, publicKey);

            // 验证参数的长度
            int length = Integer.parseInt(params.substring(params.lastIndexOf("&") + 1));
            String param = params.substring(0, params.lastIndexOf("&"));
            if (param.length() != length) {
                throw new RuntimeException("验证参数长度校验失败!");
            }
            Map<String, String> paramMap = getParamMap(param);
            String key = paramMap.get("key");
            String sign = paramMap.get("sign");
            long startTime = Long.parseLong(paramMap.get("startTime"));
            long endTime = Long.parseLong(paramMap.get("endTime"));
            String licenseCode = paramMap.get("licenseCode");

            // 将key再次转为sign,验证key的签名是否正确
            StringBuilder signBuilder = new StringBuilder();
            char[] chars = key.toCharArray();
            for (char c : chars) {
                String s = String.valueOf((int) c);
                if (s.length() != 2) {
                    throw new RuntimeException("生成的key格式错误");
                }
                signBuilder.append(s);
            }
            String signKey = signBuilder.toString();
            if (!signKey.equals(sign)) {
                throw new RuntimeException("解析key的签名错误");
            }

            // 判断授权时间
            long now = System.currentTimeMillis();
            if (now > endTime || now < startTime) {
                throw new RuntimeException("授权时间无效!");
            }

            // 验证授权码是否正确
            // 对解析授权码与传入的授权码进行对比
            if (!licenseCode.equals(code)) {
                throw new RuntimeException("授权码不匹配!");
            }

            System.out.println("授权验证成功!");
            return paramMap;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static Map<String, String> getParamMap(String param) {
        Map<String, String> map = new HashMap<>(8);
        // param为:key=1111&startTime=123.....
        String[] split = param.split("&");
        for (String s : split) {
            // 找到第一个"="位置,然后进行分割
            int indexOf = s.indexOf("=");
            String key = s.substring(0, indexOf);
            String value = s.substring(indexOf + 1);
            map.put(key, value);
        }
        return map;
    }

}

2.3 时间校验工具类

代码实现 - CompareTimeUtil


import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.sun.org.apache.xml.internal.security.utils.Base64;

/**
 * @Author: LiHuaZhi
 * @Date: 2022/9/11 16:42
 * @Description: 获取比较时间工具类
 **/
public class CompareTimeUtil {

    /**
     * AES偏移量16位,安装规则固定即可
     */
    private final static String AES_IV = "A64BF30925883C04";

    /**
     * 设置为CBC加密
     */
    private final static String TRANSFORMATION = "AES/CBC/PKCS5Padding";

    public static void main(String[] args) throws ParseException {
        // 加密
        Long startTime = 1653204114000L;
        String licenseCode = "cpu=BFEBFBFF000906A3&bios=422038Z0L795224236C&mainBoard=DefaultstringbefilledbyO.E.M";
        String time = generateCompareTime(startTime, licenseCode);
        System.out.println(time);

        // 解密
        String key = licenseCode.replaceAll("\\s*", "").replaceAll("[^(A-Za-z)]", "");
        key = key.length() > 16 ? key.substring(0, 16) : generateKey(key);

        String symmetry = decryptBySymmetry("qMmdu6I9vjpN0NcuDtaSNQ==", key);
        // 转回时间戳
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        Long nowTime = sdf.parse(symmetry).getTime();
        System.out.println(nowTime);
    }

    /**
     * 构建加密比较时间
     *
     * @param startTime   授权开始时间
     * @param licenseCode 授权机器码
     * @return
     */
    public static String generateCompareTime(Long startTime, String licenseCode) {

        Date date = new Date(startTime);
        DateFormat bf = new SimpleDateFormat("yyyyMMddHHmmss");
        String compareTime = bf.format(date);
        // 将字符转为ASCII码
        // 遍历字符串
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < compareTime.length(); i++) {
            sb.append((compareTime.charAt(i) - 0));
            if (i < compareTime.length() - 1) {
                sb.append("-");
            }
        }

        // 获取AES加密key,当前应用方的机器码的前16个字母,如果字母不管则使用`0`补充
        String key = licenseCode.replaceAll("\\s*", "").replaceAll("[^(A-Za-z)]", "");
        key = key.length() > 16 ? key.substring(0, 16) : generateKey(key);
        // 进行AES加密
        return encryptBySymmetry(sb.toString(), key);
    }

    private static String generateKey(String key) {
        for (int i = key.length(); i < 16; i++) {
            key = key.concat("0");
        }
        return key;
    }

    /**
     * 对称加密数据
     *
     * @param input : 原文
     * @param key   : 密钥
     * @return : 密文
     * @throws Exception
     */
    public static String encryptBySymmetry(String input, String key) {
        try {
            // 获取加密对象
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            // 创建加密规则
            // 第一个参数key的字节
            // 第二个参数表示加密算法
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), "AES");

            // ENCRYPT_MODE:加密模式
            // DECRYPT_MODE: 解密模式
            // 初始化加密模式和算法
            // 默认采用ECB加密:同样的原文生成同样的密文,并行进行
            // CBC加密:同样的原文生成的密文不一样,串行进行
            // 使用CBC模式
            IvParameterSpec iv = new IvParameterSpec(AES_IV.getBytes());
            cipher.init(Cipher.ENCRYPT_MODE, sks, iv);


            // 加密
            byte[] bytes = cipher.doFinal(input.getBytes());

            // 输出加密后的数据
            return Base64.encode(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加密失败!");
        }
    }

    /**
     * 对称解密
     *
     * @param input : 密文
     * @param key   : 密钥
     * @throws Exception
     * @return: 原文
     */
    public static String decryptBySymmetry(String input, String key) {
        try {
            // 1,获取Cipher对象
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            // 指定密钥规则
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), "AES");
            // 默认采用ECB加密:同样的原文生成同样的密文
            // CBC加密:同样的原文生成的密文不一样

            // 使用CBC模式
            IvParameterSpec iv = new IvParameterSpec(AES_IV.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, sks, iv);

            // 3. 解密,上面使用的base64编码,下面直接用密文
            byte[] bytes = cipher.doFinal(Base64.decode(input));
            //  因为是明文,所以直接返回
            return new String(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密失败!");
        }
    }
}

2.4 properties配置

系统相关的配置

server.port=9191
server.servlet.context-path=/license-platform
sys.license.key = C:\\Users\\lhz12\\Desktop\\test\\license.key
sys.license.pri = C:\\Users\\lhz12\\Desktop\\test\\license.pri
sys.license.pub = C:\\Users\\lhz12\\Desktop\\test\\license.pub

2.4 授权码Controller

Controller作为入口类,根据传入的机器码、授权日期调用LicenseUtil,并且提供接口进行自测

import com.example.licensedemo.entity.CipherLicense;
import com.example.licensedemo.utils.LicenseUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @Author: LiHuaZhi
 * @Description:
 **/
@Api(tags = "加密授权接口管理")
@RestController
@RequestMapping("/cipher")
@Slf4j
public class CipherController {
    @Value("${sys.license.key}")
    private String keyPath;

    @Value("${sys.license.pri}")
    private String priPath;

    @Value("${sys.license.pub}")
    private String pubPath;


    /**
     * 生成公钥和授权文件-内部接口,实际情况下这两个接口应该独立出来,不会在包里面
     *
     * @return
     */
    @ApiOperation(value = "生成公钥和授权文件", notes = "生成公钥和授权文件")
    @ApiOperationSupport(order = 5)
    @PostMapping("/set")
    public Object setLicense(@RequestBody CipherLicense param) {
        LicenseUtil.setLicense(priPath, keyPath, pubPath, param.getStartTime(), param.getEndTime(), param.getLicenseCode());
        return "操作成功";
    }

    /**
     * @return
     */
    @ApiOperation(value = "手动通过公钥和授权文件实现授权验证", notes = "手动通过公钥和授权文件实现授权验证")
    @ApiOperationSupport(order = 5)
    @GetMapping("/load")
    public Object loadLicense(String code) {
        Map<String, String> map = LicenseUtil.testLicense(code, keyPath, pubPath);
        System.out.println(map);
        if (map != null && map.size() > 0) {
            return "操作成功";
        } else {
            return "操作失败";
        }
    }
}

2.5 测试

通过swagger调用接口进行测试,地址:http://localhost:9191/license-platform/doc.html

第一步:生成授权码、公钥、私钥文件
在这里插入图片描述

生成的公钥、私钥、授权码文件:
在这里插入图片描述

第二步:自测验证授权码是否正确
该步骤为模拟应用系统的验证授权的一个过程,只有用到公钥、授权码文件、机器码,这些都是应该让应用系统拥有的,不能将私钥暴漏给应用系统;

在这里插入图片描述

授权验证成功:
在这里插入图片描述

3、应用系统

搭建系统: license-application
在这里插入图片描述

3.1 解密工具类

主要使用了凯撒解密、RSA非对称解密,在工具类中利用公钥解析授权码文件,获取时间、机器码等参数;

代码实现 - DecodeUtil


/**
 * @Author: LiHuaZhi
 * @Description: 解密工具类
 **/

public class DecodeUtil {

    public final static String RSA = "RSA";

    private final static String DES = "DES";

    public final static String AES = "AES";

    /**
     * AES加密算法,key的大小必须是16个字节,可以任意,必须与平台管理端保持一致
     */
    public final static String AES_KEY = "5LiN6KaB56C06Kej";

    /**
     * 设置为CBC加密,默认情况下ECB比CBC更高效
     */
    private final static String CBC = "/CBC/PKCS5Padding";


    /**
     * 从文件中加载公钥
     *
     * @param filePath : 文件路径
     * @return : 公钥
     * @throws Exception
     */
    public static PublicKey loadPublicKeyFromFile(String filePath) {
        try {
            // 将文件内容转为字符串
            String keyString = FileUtils.readFileToString(new File(filePath), String.valueOf(StandardCharsets.UTF_8));
            // 获取加密时间
            CompareTimeUtil.COMPARE_TIME = keyString.substring(keyString.indexOf("&") + 1);
            // 不要公钥文件的比较时间属性
            keyString = keyString.substring(0, keyString.indexOf("&"));
            return loadPublicKeyFromString(keyString);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取公钥文件字符串失败!");
        }
    }

    /**
     * 从字符串中加载公钥
     *
     * @param keyString : 公钥
     * @return : 公钥
     * @throws Exception
     */
    public static PublicKey loadPublicKeyFromString(String keyString) {
        try {
            // 进行Base64解码
            byte[] decode = Base64.decode(keyString);
            // 获取密钥工厂
            KeyFactory keyFactory = KeyFactory.getInstance(RSA);
            // 构建密钥规范
            X509EncodedKeySpec key = new X509EncodedKeySpec(decode);
            // 获取公钥
            return keyFactory.generatePublic(key);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取公钥失败!");
        }
    }

    /**
     * 非对称解密数据
     *
     * @param encrypted : 密文
     * @param key       : 密钥
     * @return : 原文
     * @throws Exception
     */
    public static String decryptByAsymmetric(String encrypted, Key key) {
        try {
            // 获取Cipher对象
            Cipher cipher = Cipher.getInstance(RSA);
            // 初始化模式(解密)和密钥
            cipher.init(Cipher.DECRYPT_MODE, key);
            return new String(getMaxResultDecrypt(encrypted, cipher));
        } catch (
                Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密失败!");
        }
    }


    /**
     * 分段处理解密数据
     *
     * @param decryptText : 加密文本
     * @param cipher      : Cipher对象
     * @throws Exception
     */
    private static byte[] getMaxResultDecrypt(String decryptText, Cipher cipher) throws Exception {
        byte[] inputArray = Base64.decode(decryptText.getBytes(StandardCharsets.UTF_8));
        int inputLength = inputArray.length;

        // 最大解密字节数,超出最大字节数需要分组加密
        int MAX_ENCRYPT_BLOCK = 128;
        // 标识
        int offSet = 0;
        byte[] resultBytes = {};
        byte[] cache = {};
        while (inputLength - offSet > 0) {
            if (inputLength - offSet > MAX_ENCRYPT_BLOCK) {
                cache = cipher.doFinal(inputArray, offSet, MAX_ENCRYPT_BLOCK);
                offSet += MAX_ENCRYPT_BLOCK;
            } else {
                cache = cipher.doFinal(inputArray, offSet, inputLength - offSet);
                offSet = inputLength;
            }
            resultBytes = Arrays.copyOf(resultBytes, resultBytes.length + cache.length);
            System.arraycopy(cache, 0, resultBytes, resultBytes.length - cache.length, cache.length);
        }
        return resultBytes;
    }


    /**
     * 使用凯撒加密方式解密数据
     *
     * @param encryptedData :密文
     * @param key           :位移数量
     * @return : 源数据
     */
    public static String decryptKaiser(String encryptedData, int key) {
        // 将字符串转为字符数组
        char[] chars = encryptedData.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (char aChar : chars) {
            // 获取字符的ASCII编码
            int asciiCode = aChar;
            // 偏移数据
            asciiCode -= key;
            // 将偏移后的数据转为字符
            char result = (char) asciiCode;
            // 拼接数据
            sb.append(result);
        }
        return sb.toString();
    }

    /**
     * 对称加密
     *
     * @param input     : 密文
     * @param key       : 密钥
     * @param algorithm : 类型:DES、AES
     * @return
     */
    public static String encryptBySymmetry(String input, String key, String algorithm) {
        return encryptBySymmetry(input, key, algorithm, false);
    }

    /**
     * 对称加密数据
     *
     * @param input     : 原文
     * @param key       : 密钥
     * @param algorithm : 类型:DES、AES
     * @param cbc       : CBC加密模式:同样的原文生成的密文不一样,串行进行,加密使用CBC解密也需要CBC
     * @return : 密文
     * @throws Exception
     */
    public static String encryptBySymmetry(String input, String key, String algorithm, Boolean cbc) {
        try {
            // 根据加密类型判断key字节数
            checkAlgorithmAndKey(key, algorithm);

            // CBC模式
            String transformation = cbc ? algorithm + CBC : algorithm;
            // 获取加密对象
            Cipher cipher = Cipher.getInstance(transformation);
            // 创建加密规则
            // 第一个参数key的字节
            // 第二个参数表示加密算法
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);

            // ENCRYPT_MODE:加密模式
            // DECRYPT_MODE: 解密模式
            // 初始化加密模式和算法
            // 默认采用ECB加密:同样的原文生成同样的密文,并行进行
            // CBC加密:同样的原文生成的密文不一样,串行进行
            if (cbc) {
                // 使用CBC模式,此处偏移量和密钥一致
                IvParameterSpec iv = new IvParameterSpec(key.getBytes());
                cipher.init(Cipher.ENCRYPT_MODE, sks, iv);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, sks);
            }

            // 加密
            byte[] bytes = cipher.doFinal(input.getBytes());

            // 输出加密后的数据
            return Base64.encode(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加密失败!");
        }
    }

    /**
     * 对称解密
     *
     * @param input     : 密文
     * @param key       : 密钥
     * @param algorithm : 类型:DES、AES
     * @return
     */
    public static String decryptBySymmetry(String input, String key, String algorithm) {
        return decryptBySymmetry(input, key, algorithm, false);
    }

    /**
     * 对称解密
     *
     * @param input     : 密文
     * @param key       : 密钥
     * @param algorithm : 类型:DES、AES
     * @param cbc       : CBC加密模式:同样的原文生成的密文不一样,串行进行,加密使用CBC解密也需要CBC
     * @throws Exception
     * @return: 原文
     */
    public static String decryptBySymmetry(String input, String key, String algorithm, Boolean cbc) {
        try {
            // 根据加密类型判断key字节数
            checkAlgorithmAndKey(key, algorithm);

            // CBC模式
            String transformation = cbc ? algorithm + CBC : algorithm;

            // 1,获取Cipher对象
            Cipher cipher = Cipher.getInstance(transformation);
            // 指定密钥规则
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), algorithm);
            // 默认采用ECB加密:同样的原文生成同样的密文
            // CBC加密:同样的原文生成的密文不一样
            if (cbc) {
                // 使用CBC模式
                IvParameterSpec iv = new IvParameterSpec(key.getBytes());
                cipher.init(Cipher.DECRYPT_MODE, sks, iv);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, sks);
            }
            // 3. 解密,上面使用的base64编码,下面直接用密文
            byte[] bytes = cipher.doFinal(Base64.decode(input));
            //  因为是明文,所以直接返回
            return new String(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密失败!");
        }
    }

    private static void checkAlgorithmAndKey(String key, String algorithm) {
        // 根据加密类型判断key字节数
        int length = key.getBytes().length;
        boolean typeEnable = false;
        if (DES.equals(algorithm)) {
            typeEnable = length == 8;
        } else if (AES.equals(algorithm)) {
            typeEnable = length == 16;
        } else {
            throw new RuntimeException("加密类型不存在");
        }
        if (!typeEnable) {
            throw new RuntimeException("加密Key错误");
        }
    }
}

3.2 授权工具类

将应用系统的机器码、授权日期、等参数与通过DecodeUtil工具类中利用公钥解析授权码文件,获取时间、机器码的参数进行比较,如果匹配则表示授权成功;

代码实现 - LicenseUtil


import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Author: LiHuaZhi
 * @Description: 生成公钥私钥及授权码
 **/

@Slf4j
public class LicenseUtil {


    /**
     * 凯撒加密key
     */
    public final static Integer[] KAISER_KEY = {5, 11, 2, 19, 9, 12, 20, 8, 10};

    public final static long MAX_ERROR_FILE = 10 * 1024 * 1024;
    public final static int MAX_ERROR_FILE_NUM = 10;
    public final static String ERROR_FILE_DEFAULT = "error.log";
    public final static String ERROR_FILE_PREFIX = "error-";
    public final static String ERROR_FILE_SUFFIX = ".log";


    /**
     * 加载授权码文件,判断是否授权成功
     *
     * @param errorPath   错误日志位置
     * @param licensePath 授权码文件位置
     * @param pubPath     公钥位置
     * @return
     */
    public static Map<String, String> loadLicense(String errorPath, String licensePath, String pubPath) {
        try {
            // 读取密文内容
            String licenseText = FileUtils.readFileToString(new File(licensePath), String.valueOf(StandardCharsets.UTF_8));
            // 凯撒解密
            StringBuilder kaiserBuilder = new StringBuilder();
            char[] decryptChars = licenseText.toCharArray();
            for (int index = 0; index < decryptChars.length; index++) {
                int keyIndex = index % KAISER_KEY.length;
                char c = decryptChars[index];
                String s = String.valueOf(c);
                String encryptKaiser = DecodeUtil.decryptKaiser(s, KAISER_KEY[keyIndex]);
                kaiserBuilder.append(encryptKaiser);
            }
            String decryptLicense = kaiserBuilder.toString();
            // 使用私密进行解密获取加密参数
            PublicKey publicKey = DecodeUtil.loadPublicKeyFromFile(pubPath);
            // 获取原文参数
            String params = DecodeUtil.decryptByAsymmetric(decryptLicense, publicKey);

            // 验证参数的长度
            int length = Integer.parseInt(params.substring(params.lastIndexOf("&") + 1));
            String param = params.substring(0, params.lastIndexOf("&"));
            if (param.length() != length) {
                throw new RuntimeException("验证参数长度校验失败!");
            }
            Map<String, String> paramMap = getParamMap(param);
            String key = paramMap.get("key");
            String sign = paramMap.get("sign");
            long startTime = Long.parseLong(paramMap.get("startTime"));
            long endTime = Long.parseLong(paramMap.get("endTime"));
            String licenseCode = paramMap.get("licenseCode");

            // 将key再次转为sign,验证key的签名是否正确
            StringBuilder signBuilder = new StringBuilder();
            char[] chars = key.toCharArray();
            for (char c : chars) {
                String s = String.valueOf((int) c);
                if (s.length() != 2) {
                    throw new RuntimeException("生成的key格式错误");
                }
                signBuilder.append(s);
            }
            String signKey = signBuilder.toString();
            if (!signKey.equals(sign)) {
                throw new RuntimeException("解析key的签名错误");
            }

            // 获取授权比较时间
            long compareTime = CompareTimeUtil.getCompareTime();
            // 判断授权时间
            if (compareTime > endTime || compareTime < startTime) {
                throw new RuntimeException("授权时间无效!");
            }

            // 验证授权码是否正确
            // 获取服务器的硬件信息编码
            String applicationInfo = CipherUtil.getApplicationInfo();
            // 对授权码进行解密
            String encryptData = DecodeUtil.decryptBySymmetry(licenseCode, DecodeUtil.AES_KEY, DecodeUtil.AES, true);

            // 对授权码进行与硬件信息编码进行匹配
            if (!applicationInfo.equals(encryptData)) {
                throw new RuntimeException("授权码不匹配!");
            }

            System.out.println("授权验证成功!");
            return paramMap;
        } catch (Exception e) {
            e.printStackTrace();
            try {
                writeErrorToFile(e, errorPath);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return null;
        }
    }

    /**
     * 将授权的错误信息写入文件
     *
     * @param e
     * @param errorPath
     * @throws IOException
     */
    private static void writeErrorToFile(Exception e, String errorPath) throws IOException {
        File parentFile = new File(errorPath);
        // 获取当前文件夹,的所有文件列表
        File[] listFiles = parentFile.listFiles();
        int fileNum = 1;
        List<File> fileList = new ArrayList<>();
        if (listFiles != null) {
            // 按修改日期排行
            for (File sonFile : listFiles) {
                if (sonFile.getName().contains(ERROR_FILE_PREFIX)) {
                    fileNum++;
                    fileList.add(sonFile);
                }
            }
        }

        // 如果文件过大(10MB),则先复制副本,再进行删除
        File file = new File(errorPath + File.separator + ERROR_FILE_DEFAULT);
        if (file.exists() && file.length() > MAX_ERROR_FILE) {
            // 按照日期排序
            List<File> fileCollect = fileList.stream().sorted(Comparator.comparing(File::lastModified)).collect(Collectors.toList());
            // 如果大于10个,则删除文件,只保留10个
            while (fileCollect.size() >= MAX_ERROR_FILE_NUM) {
                boolean delete = fileCollect.get(0).delete();
                fileCollect.remove(0);
            }
            for (int index = 0; index < fileCollect.size(); index++) {
                String name = errorPath + File.separator + ERROR_FILE_PREFIX + (index + 1) + ERROR_FILE_SUFFIX;
                boolean b = fileCollect.get(index).renameTo(new File(name));
            }
            fileNum = Math.min(fileNum, MAX_ERROR_FILE_NUM);
            String newErrorFile = errorPath + File.separator + ERROR_FILE_PREFIX + fileNum + ERROR_FILE_SUFFIX;
            // 重新命名
            boolean b = file.renameTo(new File(newErrorFile));
            // 删除原文件
            boolean delete = file.delete();
        }
        // 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String format = dateTimeFormatter.format(now);
        String message = format + " " + e.getMessage() + "\n";
        FileUtils.writeStringToFile(file, message, String.valueOf(StandardCharsets.UTF_8), true);
    }

    private static Map<String, String> getParamMap(String param) {
        Map<String, String> map = new HashMap<>(8);
        // param为:key=1111&startTime=123.....
        String[] split = param.split("&");
        for (String s : split) {
            // 找到第一个"="位置,然后进行分割
            int indexOf = s.indexOf("=");
            String key = s.substring(0, indexOf);
            String value = s.substring(indexOf + 1);
            map.put(key, value);
        }
        return map;
    }

}

3.3 时间校验工具类

代码实现 - CompareTimeUtil


import com.sun.org.apache.xml.internal.security.utils.Base64;
import org.apache.commons.io.FileUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author: LiHuaZhi
 * @Date: 2022/9/11 16:42
 * @Description: 获取比较时间工具类
 **/
public class CompareTimeUtil {

    /**
     * AES偏移量16位,安装规则固定即可
     */
    private final static String AES_IV = "A64BF30925883C04";

    /**
     * 设置为CBC加密
     */
    private final static String TRANSFORMATION = "AES/CBC/PKCS5Padding";

    public static String COMPARE_TIME = null;

    /**
     * 获取比较时间
     *
     * @return
     */
    public static Long getCompareTime() throws ParseException {

        // 解密比较时间
        String applicationInfo = CipherUtil.getApplicationInfo();
        String licenseCode = DecodeUtil.encryptBySymmetry(applicationInfo, DecodeUtil.AES_KEY, DecodeUtil.AES, true);
        String key = licenseCode.replaceAll("\\s*", "").replaceAll("[^(A-Za-z)]", "");
        key = key.length() > 16 ? key.substring(0, 16) : generateKey(key);

        // 解码
        String decrypt = decryptBySymmetry(CompareTimeUtil.COMPARE_TIME, key);
        // ASCII码转为数字
        String[] splits = decrypt.split("-");
        StringBuilder sb = new StringBuilder();
        for (String s : splits) {
            sb.append((char) (Integer.parseInt(s)));
        }
        String date = sb.toString();
        // 转化为时间戳
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        return sdf.parse(date).getTime();
    }


    /**
     * 重新加载比较时间
     *
     * @param filePath
     * @param endTime
     */
    public static void reLoadCompareTime(String filePath, long endTime) {
        try {
            // 截取原有比较时间
            long compareTime = CompareTimeUtil.getCompareTime();
            // 追加最新比较时间,增加一小时时间
            compareTime = compareTime + 60 * 60 * 1000;
            compareTime = Math.min(compareTime, endTime);

            // 设置新的比较时间
            String applicationInfo = CipherUtil.getApplicationInfo();
            String licenseCode = DecodeUtil.encryptBySymmetry(applicationInfo, DecodeUtil.AES_KEY, DecodeUtil.AES, true);
            String newCompareTime = generateCompareTime(compareTime, licenseCode);

            // 截取公钥部分
            PublicKey publicKey = DecodeUtil.loadPublicKeyFromFile(filePath);
            String keyString = Base64.encode(publicKey.getEncoded());

            // 写回文件
            keyString = keyString.concat("&").concat(newCompareTime);
            FileUtils.writeStringToFile(new File(filePath), keyString, String.valueOf(StandardCharsets.UTF_8));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 重置
            CompareTimeUtil.COMPARE_TIME = null;
        }
    }

    /**
     * 构建加密比较时间
     *
     * @param startTime   授权开始时间
     * @param licenseCode 授权机器码
     * @return
     */
    public static String generateCompareTime(Long startTime, String licenseCode) {

        Date date = new Date(startTime);
        DateFormat bf = new SimpleDateFormat("yyyyMMddHHmmss");
        String compareTime = bf.format(date);
        // 将字符转为ASCII码
        // 遍历字符串
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < compareTime.length(); i++) {
            sb.append((compareTime.charAt(i) - 0));
            if (i < compareTime.length() - 1) {
                sb.append("-");
            }
        }

        // 获取AES加密key,当前应用方的机器码的前16个字母,如果字母不管则使用`0`补充
        String key = licenseCode.replaceAll("\\s*", "").replaceAll("[^(A-Za-z)]", "");
        key = key.length() > 16 ? key.substring(0, 16) : generateKey(key);
        // 进行AES加密
        return encryptBySymmetry(sb.toString(), key);
    }

    private static String generateKey(String key) {
        for (int i = key.length(); i < 16; i++) {
            key = key.concat("0");
        }
        return key;
    }

    /**
     * 对称解密
     *
     * @param input : 密文
     * @param key   : 密钥
     * @throws Exception
     * @return: 原文
     */
    public static String decryptBySymmetry(String input, String key) {
        try {
            // 1,获取Cipher对象
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            // 指定密钥规则
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), "AES");
            // 默认采用ECB加密:同样的原文生成同样的密文
            // CBC加密:同样的原文生成的密文不一样

            // 使用CBC模式
            IvParameterSpec iv = new IvParameterSpec(AES_IV.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, sks, iv);

            // 3. 解密,上面使用的base64编码,下面直接用密文
            byte[] bytes = cipher.doFinal(Base64.decode(input));
            //  因为是明文,所以直接返回
            return new String(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("解密失败!");
        }
    }

    /**
     * 对称加密数据
     *
     * @param input : 原文
     * @param key   : 密钥
     * @return : 密文
     * @throws Exception
     */
    public static String encryptBySymmetry(String input, String key) {
        try {
            // 获取加密对象
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            // 创建加密规则
            // 第一个参数key的字节
            // 第二个参数表示加密算法
            SecretKeySpec sks = new SecretKeySpec(key.getBytes(), "AES");

            // ENCRYPT_MODE:加密模式
            // DECRYPT_MODE: 解密模式
            // 初始化加密模式和算法
            // 默认采用ECB加密:同样的原文生成同样的密文,并行进行
            // CBC加密:同样的原文生成的密文不一样,串行进行
            // 使用CBC模式
            IvParameterSpec iv = new IvParameterSpec(AES_IV.getBytes());
            cipher.init(Cipher.ENCRYPT_MODE, sks, iv);


            // 加密
            byte[] bytes = cipher.doFinal(input.getBytes());

            // 输出加密后的数据
            return Base64.encode(bytes);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("加密失败!");
        }
    }
}

3.4 授权处理器

import com.example.licensedemo.utils.CompareTimeUtil;
import com.example.licensedemo.utils.LicenseUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @Author: LiHuaZhi
 * @Date: 2021/8/20 15:38
 * @Description:
 **/
@Component
@Slf4j
public class LicenseHandler {

    public static Boolean license = false;

    private static int FAIL_NUM = 0;
    private static final int FAIL_MAX_NUM = 10;

    @Value("${sys.license.log}")
    private String errorPath;

    @Value("${sys.license.pub}")
    private String pubPath;

    @Value("${sys.license.key}")
    private String licensePath;

    public boolean loadLicense() {
        // 10次请求出现授权失败,则不加载授权文件
        if (FAIL_NUM >= FAIL_MAX_NUM) {
            return false;
        }
        // 当授权验证为false时,进行重新验证,让前十次验证时即使没有授权也能成功,直到连续失败十次
        if (!license) {
            Map<String, String> map = LicenseUtil.loadLicense(errorPath, licensePath, pubPath);
            if (map != null && map.size() > 0) {
                FAIL_NUM = 0;
                license = true;
            } else {
                FAIL_NUM++;
                license = false;
            }
        }
        return true;
    }

    /**
     * 定时任务,bean不初始化时不会执行,每隔一个小时执行一次
     */
    @Scheduled(cron = "0 0 * * * ?")
    public void scheduled() {
        Map<String, String> map = LicenseUtil.loadLicense(errorPath, licensePath, pubPath);
        if (map != null && map.size() > 0) {
            FAIL_NUM = 0;
            license = true;

            long endTime = Long.parseLong(map.get("endTime"));
            // 将公钥文件中的 比较时间延长一小时,但是不能大于授权结束时间
            CompareTimeUtil.reLoadCompareTime(pubPath, endTime);
        } else {
            license = false;
        }
    }
}

3.5 properties配置

系统相关的配置

server.port=9292
server.servlet.context-path=/license-application
sys.license.log = C:\\Users\\lhz12\\Desktop\\license
sys.license.key = C:\\Users\\lhz12\\Desktop\\license\\license.key
sys.license.pub = C:\\Users\\lhz12\\Desktop\\license\\license.pub

3.6 授权 Controller

Controller作为入口类,根据传入的机器码、授权日期调用LicenseUtil,并且提供接口进行自测

import com.example.licensedemo.utils.CipherUtil;
import com.example.licensedemo.utils.DecodeUtil;
import com.example.licensedemo.utils.LicenseUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: LiHuaZhi
 * @Description:
 **/
@Api(tags = "加密授权接口管理")
@RestController
@RequestMapping("/cipher")
@Slf4j
public class CipherController {
    @Value("${sys.license.key}")
    private String keyPath;

    @Value("${sys.license.log}")
    private String logPath;

    @Value("${sys.license.pub}")
    private String pubPath;


    /**
     * 用户在点击,查看`授权信息`按钮时,请求check接口,进行一次授权验证(每天第一次通过其他接口访问系统时,也会验证一次 )
     * 如果通过则返回授权信息(开始+结束时间)
     * 如果失败则返回授权码
     *
     * @return
     */
    @ApiOperation(value = "获取以及验证授权信息", notes = "获取以及验证授权信息")
    @ApiOperationSupport(order = 5)
    @GetMapping("/check")
    public Object check() {
        // 验证是否通过了授权,通过了返回授权信息(开始+结束时间)
        try {
            Map<String, String> map = LicenseUtil.loadLicense(logPath, keyPath, pubPath);
            if (map != null && map.size() > 0) {
                long startTime = Long.parseLong(map.get("startTime"));
                long endTime = Long.parseLong(map.get("endTime"));
                // 只返回授权开始和结束时间给页面
                Map<String, Long> resultMap = new HashMap<>(4);
                resultMap.put("startTime", startTime);
                resultMap.put("endTime", endTime);
                return resultMap;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        String applicationInfo = CipherUtil.getApplicationInfo();
        String encryptAes = DecodeUtil.encryptBySymmetry(applicationInfo, DecodeUtil.AES_KEY, DecodeUtil.AES, true);
        log.debug("授权码:" + encryptAes);
        return encryptAes;

    }
    @ApiOperation(value = "测试请求拦截验证", notes = "测试请求拦截验证")
    @ApiOperationSupport(order = 5)
    @GetMapping("/test")
    public String test() {
        return "请求成功";
    }

}

3.7 拦截器实现

在拦截器中,拦截除了/cipher/check以外的所有接口,在拦截器中需要判断应用是否授权成功;

@Component
@Slf4j
public class LicenseInterceptor implements HandlerInterceptor {

    @Resource
    private LicenseHandler licenseHandler;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.debug("进入拦截器,URL:{}",request.getServletPath());

        // 查看是否授权成功
        boolean license = licenseHandler.loadLicense();
            if (!license) {
                throw new RemoteException("系统暂未授权");
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

3.8 测试

将平台管理端与应用系统端都启动起来

1、访问应用系统的/test接口:
在这里插入图片描述
查看控制台:
打印了错误信息,因为系统还没有开始授权
在这里插入图片描述

2、访问应用系统的/check接口:
再请求接口后,没有提示验证成功,而是返回了需要进行授权的机器码
在这里插入图片描述

3、拷贝机器码,访问管理平台的生成公钥和授权文件接口:
在这里插入图片描述

4、拷贝公钥、授权码文件
将管理平台的生成的公钥、授权码文件拷贝到C:\Users\lhz12\Desktop\license目录下,改目录为应用系统的application.properties配置的读取目录,此过程模拟实际场景中通过U盘等方式的拷贝情景;
在这里插入图片描述
文件拷贝:
在这里插入图片描述

5、再次访问应用系统的/test接口:

再次访问时,系统已经提示成功了
在这里插入图片描述

6、再次访问应用系统的/check接口:

check接口返回了,系统授权的开始与结束时间,表示授权成功
在这里插入图片描述

4、总结

授权申请处理逻辑如下:

1、生成一个由大写及数字组成的key
2、生成sign,该sign就是key的各个字符的ascII码
3、设置授权时间startTime、endTime
4、将key、sign、startTime、endTime、机器码拼接起来生成param参数
5、计算param参数的长度,并且将计算后的长度拼接到param后面
6、使用私钥对参数进行加密、并且再对密文进行凯撒加密处理
7、将最终加密的文件保存到本地
8、将密文(授权)文件和公钥文件给用户放到服务器的指定位置

授权验证处理逻辑如下:

1、项目启动时,在LicenseHandler中的license默认为false
2、请求到达过滤器时,首先判断license是否为false,如果是则请求loadLicense()方法加载授权文件
3、如果加载失败,将则失败信息写入到日志文件中,如果成功则更新license为true
4、如果过滤器时连续10次请求出现授权失败,则不加载授权文件,会使每次请求都提示授权失败,需要用户在首页点击查看授权信息按钮,获取最新的授权码,然后给管理平台,获取最新的授权文件和公钥文件放到服务器指定位置,再次在首页点击查看授权信息按钮,如果验证成功则返回授权的开始和结束日期。
5、有一个定时任务,每天都进行一次授权校验,如果失败则将license设置为false

5、完整代码

Github:https://github.com/lxlhz/LicenseDemo.git
Gitee:https://gitee.com/lhzlx/LicenseDemo.git

内容概要:本文档详细介绍了在三台CentOS 7服务器(IP地址分别为192.168.0.157、192.168.0.158和192.168.0.159)上安装和配置Hadoop、Flink及其他大数据组件(如Hive、MySQL、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala)的具体步骤。首先,文档说明了环境准备,包括配置主机名映射、SSH免密登录、JDK安装等。接着,详细描述了Hadoop集群的安装配置,包括SSH免密登录、JDK配置、Hadoop环境变量设置、HDFS和YARN配置文件修改、集群启动与测试。随后,依次介绍了MySQL、Hive、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala和Flink的安装配置过程,包括解压、环境变量配置、配置文件修改、服务启动等关键步骤。最后,文档提供了每个组件的基本测试方法,确保安装成功。 适合人群:具备一定Linux基础和大数据组件基础知识的运维人员、大数据开发工程师以及系统管理员。 使用场景及目标:①为大数据平台建提供详细的安装指南,确保各组件能够顺利安装和配置;②帮助技术人员快速掌握Hadoop、Flink等大数据组件的安装与配置,提升工作效率;③适用于企业级大数据平台的建与维护,确保集群稳定运行。 其他说明:本文档不仅提供了详细的安装步骤,还涵盖了常见的配置项解释和故障排查建议。建议读者在安装过程中仔细阅读每一步骤,并根据实际情况调整配置参数。此外,文档中的命令和配置文件路径均为示例,实际操作时需根据具体环境进行适当修改。
在无线通信领域,天线阵列设计对于信号传播方向和覆盖范围的优化至关重要。本题要求设计一个广播电台的天线布局,形成特定的水平面波瓣图,即在东北方向实现最大辐射强度,在正东到正北的90°范围内辐射衰减最小且无零点;而在其余270°范围内允许出现零点,且正西和西南方向必须为零。为此,设计了一个由4个铅垂铁塔组成的阵列,各铁塔上的电流幅度相等,相位关系可自由调整,几何布置和间距不受限制。设计过程如下: 第一步:构建初级波瓣图 选取南北方向上的两个点源,间距为0.2λ(λ为电磁波波长),形成一个端射阵。通过调整相位差,使正南方向的辐射为零,计算得到初始相位差δ=252°。为了满足西南方向零辐射的要求,整体相位再偏移45°,得到初级波瓣图的表达式为E1=cos(36°cos(φ+45°)+126°)。 第二步:构建次级波瓣图 再选取一个点源位于正北方向,另一个点源位于西南方向,间距为0.4λ。调整相位差使西南方向的辐射为零,计算得到相位差δ=280°。同样整体偏移45°,得到次级波瓣图的表达式为E2=cos(72°cos(φ+45°)+140°)。 最终组合: 将初级波瓣图E1和次级波瓣图E2相乘,得到总阵的波瓣图E=E1×E2=cos(36°cos(φ+45°)+126°)×cos(72°cos(φ+45°)+140°)。通过编程实现计算并绘制波瓣图,可以看到三个阶段的波瓣图分别对应初级波瓣、次级波瓣和总波瓣,最终得到满足广播电台需求的总波瓣图。实验代码使用MATLAB编写,利用polar函数在极坐标下绘制波瓣图,并通过subplot分块显示不同阶段的波瓣图。这种设计方法体现了天线阵列设计的基本原理,即通过调整天线间的相对位置和相位关系,控制电磁波的辐射方向和强度,以满足特定的覆盖需求。这种设计在雷达、卫星通信和移动通信基站等无线通信系统中得到了广泛应用。
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一恍过去

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值