场景

我们在一部分场景下需要对用户的一些的参数,进行加密和解密,比如我们公司有两套服务,一套是专门对接银行/微信/支付宝的支付服务,那么这种服务不是任何请求都可以接受处理的。除了引入复杂的jwt雪花相关的加密包外,我们自己想实现一个轻量级加密解密算法的时候到了,在本地百万千万的单元测试中,效率可以达到100w/s,所以完全不用担心效率问题。不废话,上才艺:



文章目录

  • 场景
  • 代码
  • 单元测试
  • 原理
  • 加密原理细节
  • 解密原理细节
  • 总结



代码

工具包:

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import org.apache.commons.lang3.StringUtils;

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

/**
 * 自定义简单的验签工具
 * @author Liaoqian
 * @version 3.0.0
 * 加强了加密后字符串的稳定性,引入偏移量,权重值,随机值,长度校验,校验码校验,时间戳校验,分段校验等
 */
public class MySignUtil {
    /**
     * 过期时长(单位s)
     */
    private static final int EXPIRATION_TIME = 60 * 30;

    private static final String SIGN_REGEX = "^[a-zA-Z0-9|$]{7,}.[a-zA-Z0-9|$]{7,}.[a-zA-Z0-9|$]{7,}$";

    private static final char[] CHECK_CODE = {'Q', 'w', 'E', 'r', 'T', 'y', 'U', 'i', 'O', 'p'};

    private static final String LEGAL_CHAR_STR = "lwZyA07NXugBKfVI6MYUohPDnj3TsrH15JmeLzQtFv9d2qxGbCRac8WkSipOE4$";

    private static final char[] LEGAL_CHAR_ARR = LEGAL_CHAR_STR.toCharArray();

    private static final int LEGAL_CHAR_LEN = LEGAL_CHAR_STR.length();

    private static final int[] WEIGHT_NUMBER = {7, 8, 2, 5, 0, 9, 4, 1, 3, 6};

    /**
     * 正向生成偏移量
     */
    private static final int[] GENERATE_OFFSET = {17, 1, 4, 56, 28, 45, 61, 0, 11, 8, 20};
    /**
     * 反向解析偏移量
     */
    private static final int[] PARSE_OFFSET = {46, 62, 59, 7, 35, 18, 2, 63, 52, 55, 43};

    private static final int[] CHECK_INDEX = {1, 2, 0};

    private static final int TEN = 10;
    private static final int ZERO = 0;
    private static final int ONE = 1;
    private static final int TWO = 2;
    private static final int THREE = 3;

    private static final String SEPARATOR = ".";

    private static final String SEPARATOR_REGEX = "\\.";

    private static final String LENGTH_FORMAT = "%d$%s$%d";

    private static final String LENGTH_SEPARATOR = "\\$";

    private static final String LENGTH_SEP = "$";

    private static Map<Character, Integer> charInedexMap = new HashMap<>();


    static {
        char[] chars = LEGAL_CHAR_STR.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            charInedexMap.put(c, i);
        }
    }

    /**
     * 生成签名字符串
     * 由user用户,role角色,date日期根据一定的规则生成一串乱码,
     * 为了防止被破坏加密后的字符串因删减/移位/增位等一系列操作,引入位置权重,随机码,校验码,校验长度码等来保证加密后的稳定性
     * 效率:解析速度约1000ns,即1/1000ms,即1s可解析100w条,效率是解析的两倍
     *
     * @param user
     * @param role
     * @param date
     * @return
     */
    public static String generateSign(String user, String role, Date date) {
        if (StringUtils.isAnyBlank(user, role) || null == date) {
            return null;
        }

        StringBuilder sb = new StringBuilder();

        // 加密user信息
        int sum = ZERO;
        user = String.format(LENGTH_FORMAT, user.length(), user, user.length());
        char[] chars = user.toCharArray();
        for (int i = ZERO; i < chars.length; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            sum += index * WEIGHT_NUMBER[i % TEN];
            sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
        }
        addCheckCode(sum, sb, ZERO, true);

        // 加密role信息
        sum = ZERO;
        role = String.format(LENGTH_FORMAT, role.length(), role, role.length());
        chars = role.toCharArray();
        for (int i = ZERO; i < chars.length; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            sum += index * WEIGHT_NUMBER[i % TEN];
            sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
        }
        addCheckCode(sum, sb, ONE, true);

        // 加密time信息
        String time = String.valueOf(date.getTime());
        sum = ZERO;
        time = String.format(LENGTH_FORMAT, time.length(), time, time.length());
        chars = time.toCharArray();
        for (int i = ZERO; i < chars.length; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            sum += index * WEIGHT_NUMBER[i % TEN];
            sb.append(LEGAL_CHAR_ARR[(index + GENERATE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN]);
        }
        addCheckCode(sum, sb, TWO, false);

        return sb.toString();
    }

    /**
     * 添加校验码
     * 由两位随机码和一位校验码组成,校验码位置是index
     *
     * @param sum
     * @param sb
     * @param index
     * @param addSep
     */
    private static void addCheckCode(int sum, StringBuilder sb, int index, boolean addSep) {
        int checkIndex = sum % TEN;
        String check = RandomUtil.randomString(LEGAL_CHAR_STR, THREE);
        check = check.replace(check.charAt(CHECK_INDEX[index]), CHECK_CODE[checkIndex]);
        sb.append(check);
        if (addSep) {
            sb.append(SEPARATOR);
        }
    }

    /**
     * 解析签名是否正确
     *
     * @param sign
     * @return
     */
    public static boolean parseSign(String sign) {
        if (StringUtils.isBlank(sign)) {
            return false;
        }
        if (!sign.matches(SIGN_REGEX)) {
            return false;
        }
        MySign mySign = parseToMySign(sign);
        return mySign.getCheck();
    }

    /**
     * @param sign
     * @return
     */
    public static boolean signExpires(String sign) {
        if (StringUtils.isBlank(sign)) {
            return false;
        }
        if (!sign.matches(SIGN_REGEX)) {
            return false;
        }
        MySign mySign = parseToMySign(sign);
        if (!mySign.getCheck()) {
            return false;
        }
        Long timeStamp = mySign.getTimeStamp();
        if (DateUtil.offsetSecond(new Date(timeStamp), EXPIRATION_TIME).isBeforeOrEquals(DateUtil.date())) {
            throw new BusinessException(BusinessExceptionEnum.SIGNATURE_EXPIRED);
        }
        return true;
    }

    /**
     * 解析字符串签名,输出MySign解析结果
     * 效率:解析速度约2000ns,即1/500ms,即1s可解析50w条
     *
     * @param sign 入参签名
     * @return 解析完的MySign
     */
    public static MySign parseToMySign(String sign) {
        MySign mySign = new MySign();
        String[] splits = sign.split(SEPARATOR_REGEX);

        String user = splits[ZERO];
        String role = splits[ONE];
        String timeStamp = splits[TWO];

        // user校验
        StringBuilder sb = new StringBuilder();
        int sum = ZERO;
        char[] chars = user.toCharArray();
        for (int i = ZERO; i < chars.length - THREE; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
            sb.append(LEGAL_CHAR_ARR[index]);
            sum += index * WEIGHT_NUMBER[i % TEN];
        }
        int checkIndex = sum % TEN;
        char userCheck = user.charAt(user.length() - CHECK_INDEX.length + CHECK_INDEX[ZERO]);

        user = sb.toString();
        if (!user.contains(LENGTH_SEP)) {
            return mySign;
        } else {
            String[] split = user.split(LENGTH_SEPARATOR);
            if (split.length != 3) {
                return mySign;
            }
            if (!split[0].equals(split[2])) {
                return mySign;
            }
            if (!String.valueOf(split[1].length()).equals(split[0])) {
                return mySign;
            }
            user = split[1];
        }

        if (CHECK_CODE[checkIndex] != (userCheck)) {
            return mySign;
        }


        // role校验
        sb = new StringBuilder();
        sum = ZERO;
        chars = role.toCharArray();
        for (int i = ZERO; i < chars.length - THREE; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
            sb.append(LEGAL_CHAR_ARR[index]);
            sum += index * WEIGHT_NUMBER[i % TEN];
        }
        checkIndex = sum % TEN;
        userCheck = role.charAt(role.length() - CHECK_INDEX.length + CHECK_INDEX[ONE]);
        role = sb.toString();
        if (!role.contains(LENGTH_SEP)) {
            return mySign;
        } else {
            String[] split = role.split(LENGTH_SEPARATOR);
            if (split.length != 3) {
                return mySign;
            }
            if (!split[0].equals(split[2])) {
                return mySign;
            }
            if (!String.valueOf(split[1].length()).equals(split[0])) {
                return mySign;
            }
            role = split[1];
        }
        if (CHECK_CODE[checkIndex] != (userCheck)) {
            return mySign;
        }


        // timeStamp校验
        sb = new StringBuilder();
        sum = ZERO;
        chars = timeStamp.toCharArray();
        for (int i = ZERO; i < chars.length - THREE; i++) {
            char c = chars[i];
            int index = charInedexMap.get(c);
            index = (index + PARSE_OFFSET[i % TEN]) % LEGAL_CHAR_LEN;
            sb.append(LEGAL_CHAR_ARR[index]);
            sum += index * WEIGHT_NUMBER[i % TEN];
        }
        checkIndex = sum % TEN;
        userCheck = timeStamp.charAt(timeStamp.length() - CHECK_INDEX.length + CHECK_INDEX[TWO]);
        timeStamp = sb.toString();
        if (!timeStamp.contains(LENGTH_SEP)) {
            return mySign;
        } else {
            String[] split = timeStamp.split(LENGTH_SEPARATOR);
            if (split.length != 3) {
                return mySign;
            }
            if (!split[0].equals(split[2])) {
                return mySign;
            }
            if (!String.valueOf(split[1].length()).equals(split[0])) {
                return mySign;
            }
            timeStamp = split[1];
        }
        if (CHECK_CODE[checkIndex] != (userCheck)) {
            return mySign;
        }

        // 校验通过
        mySign.setCheck(true);
        mySign.setUser(user);
        mySign.setRole(role);
        mySign.setTimeStamp(Long.parseLong(timeStamp));
        return mySign;
    }


}

模型:

import lombok.Data;

@Data
public class MySign {
    /**
     * 是否解析成功
     */
    private boolean check;
    private String user;
    private String role;
    private Long timeStamp;

    public boolean getCheck() {
        return check;
    }

    public void setCheck(boolean check) {
        this.check = check;
    }

}

单元测试

public class Test1 {

@Test
public void t1() {

    String s = MySignUtil.generateSign("cc", "adminx", new Date());
    System.out.println(s);
    System.out.println(MySignUtil.signExpires("4lSqT3HT2.JlkL$tPxgnixE.bTyn2dV3dRDTo4JdE1zTV7"));


}
}

原理

加密:拼装-固定-偏移-随机-验证码
解密:分段-偏移-验证码-格式分析-提取-封装

加密原理细节

  1. 拼接:将用户角色时间戳用分隔符".",拼接成如cc.admin.15xxxxxxxxxxx字符串
  2. 固定:2$cc时间戳加密技术 java 加密 时间戳 原理_i++admin$5.13$15xxxxxxxxxxx时间戳加密技术 java 加密 时间戳 原理_校验码_02"是将原来信息分割开,并在首尾加上长度,对后期解密过程中有很大的帮助,防止加密后字符串删减长度
  3. 偏移:2$cc时间戳加密技术 java 加密 时间戳 原理_i++admin$5.13$15xxxxxxxxxxx时间戳加密技术 java 加密 时间戳 原理_i++_04tP$d.bTyn2dV69nCWeuE3E1z的字符串
  4. 随机+验证码:对第三步生成的字符串进行防止篡改,位权相加取模得到校验码,为了进一步混淆随机碰撞的效果,我们再加两位随机码,所以就得到了4lSqT39TF.ClkL时间戳加密技术 java 加密 时间戳 原理_安全_05d6YT.bTyn2dV69nCWeuE3E1zySJ的字符串

解密原理细节

  1. 分段:4lSqT39TF.ClkL时间戳加密技术 java 加密 时间戳 原理_安全_05d6YT.bTyn2dV69nCWeuE3E1zySJ分成按分隔符"."分成三段单元去解析,下面以第一段来分析为例
  2. 偏移:反向偏移得到2$cc$2 9TF 的内容
  3. 验证码:2$cc$2位权相加取模得到校验码T和9TF中我们定义的校验码位置去校验
  4. 格式分析:然后解析2$cc$2正则校验
  5. 提取:然后提取就ok了

总结

解读起来不是很麻烦,在自我实现的过程忽视了很重要的一点,就是长度校验。这个可以固定加密后的字符串长度不能增删,改有校验码校验,另外还有随机码和校验码混合模式,分段校验通过,另外还加入过期时间的概念,让签名失效。目前来看没有什么太大问题