• 兑换码生成与解析-个人笔记(java)


    1.需求分析

    2.实现方案

    • 使用自增ID作为唯一ID,长度为32位整数。
    • 使用Base32编码将每5位二进制转换为一个字符。
    • 使用按位加权签名和随机新鲜值来防止刷取攻击

    3.加密过程

    • 生成4位新鲜值
    • 拼接新鲜值和序列号得到载荷
    • 用新鲜值选择加权数组计算校验码
    • 用校验码选择异或密钥异或混淆载荷
    • 拼接校验码和混淆载荷,Base32编码

    4.解密过程

    • Base32解码得到数值
    • 分离校验码和载荷
    • 用校验码选择异或密钥恢复载荷
    • 重新计算校验码验证一致性

    5.工具类

    • Base32:用于编码和解码算法。
    • CodeUtil:用于生成和解析兑换码。

    5.1Base32工具类

    代码如下:

    import cn.hutool.core.text.StrBuilder;
    
    /**
    * 将整数转为base32字符的工具,因为是32进制,所以每5个bit位转一次
    */
    public class Base32 {
       private final static String baseChars = "6CSB7H8DAKXZF3N95RTMVUQG2YE4JWPL";
    
       public static String encode(long raw) {
           StrBuilder sb = new StrBuilder();
           while (raw != 0) {
               int i = (int) (raw & 0b11111);
               sb.append(baseChars.charAt(i));
               raw = raw >>> 5;
           }
           return sb.toString();
       }
    
       public static long decode(String code) {
           long r = 0;
           char[] chars = code.toCharArray();
           for (int i = chars.length - 1; i >= 0; i--) {
               long n = baseChars.indexOf(chars[i]);
               r = r | (n << (5*i));
           }
           return r;
       }
    
       public static String encode(byte[] raw) {
           StrBuilder sb = new StrBuilder();
           int size = 0;
           int temp = 0;
           for (byte b : raw) {
               if (size == 0) {
                   // 取5个bit
                   int index = (b >>> 3) & 0b11111;
                   sb.append(baseChars.charAt(index));
                   // 还剩下3位
                   size = 3;
                   temp = b & 0b111;
               } else {
                   int index = temp << (5 - size) | (b >>> (3 + size) & ((1 << 5 - size) - 1)) ;
                   sb.append(baseChars.charAt(index));
                   int left = 3 + size;
                   size = 0;
                   if(left >= 5){
                       index = b >>> (left - 5) & ((1 << 5) - 1);
                       sb.append(baseChars.charAt(index));
                       left = left - 5;
                   }
                   if(left == 0){
                       continue;
                   }
                   temp = b & ((1 << left) - 1);
                   size = left;
               }
           }
           if(size > 0){
               sb.append(baseChars.charAt(temp));
           }
           return sb.toString();
       }
    
       public static byte[] decode2Byte(String code) {
           char[] chars = code.toCharArray();
           byte[] bytes = new byte[(code.length() * 5 )/ 8];
           byte tmp = 0;
           byte byteSize = 0;
           int index = 0;
           int i = 0;
           for (char c : chars) {
               byte n = (byte) baseChars.indexOf(c);
               i++;
               if (byteSize == 0) {
                   tmp = n;
                   byteSize = 5;
               } else {
                   int left = Math.min(8 - byteSize, 5);
                   if(i == chars.length){
                       bytes[index] =(byte) (tmp << left | (n & ((1 << left) - 1)));
                       break;
                   }
                   tmp = (byte) (tmp << left | (n >>> (5 - left)));
                   byteSize += left;
                   if (byteSize >= 8) {
                       bytes[index++] = tmp;
                       byteSize = (byte) (5 - left);
                       if (byteSize == 0) {
                           tmp = 0;
                       } else {
                           tmp = (byte) (n & ((1 << byteSize) - 1));
                       }
                   }
               }
           }
           return bytes;
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    5.2CodeUtil工具类

    5.2.1.兑换码算法说明:

    • ​ 兑换码分为明文和密文,明文是50位二进制数,密文是长度为10的Base32编码的字符串

    5.2.2.兑换码的明文结构:

    14(校验码) + 4 (新鲜值) + 32(序列号)

    • ​ 序列号:一个单调递增的数字,可以通过Redis来生成
    • ​ 新鲜值:可以是优惠券id的最后4位,同一张优惠券的兑换码就会有一个相同标记
    • ​ 载荷:将新鲜值(4位)拼接序列号(32位)得到载荷
    • ​ 校验码:将载荷4位一组,每组乘以加权数,最后累加求和,然后对2^14求余得到

    5.2.3.兑换码的加密过程:

    1. ​ 首先利用优惠券id计算新鲜值 f
    2. ​ 将f和序列号s拼接,得到载荷payload
    3. ​ 然后以f为角标,从提前准备好的16组加权码表中选一组
    4. ​ 对payload做加权计算,得到校验码 c
    5. ​ 利用c的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key
    6. ​ 将payload与key做异或,作为新payload2
    7. ​ 然后拼接兑换码明文:f (4位) + payload2(36位)
    8. ​ 利用Base32对密文转码,生成兑换码

    5.2.4.兑换码的解密过程:

    1. ​ 首先利用Base32解码兑换码,得到明文数值num
    2. ​ 取num的高14位得到c1,取num低36位得payload
    3. ​ 利用c1的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key
    4. ​ 将payload与key做异或,作为新payload2
    5. ​ 利用加密时的算法,用payload2和s1计算出新校验码c2,把c1和c2比较,一致则通过

    代码如下:

    import com.tianji.common.constants.RegexConstants;
    import com.tianji.common.exceptions.BadRequestException;
    
    
    public class CodeUtil {
        /**
         * 异或密钥表,用于最后的数据混淆
         */
        private final static long[] XOR_TABLE = {
                61261925471L, 61261925523L, 58169127203L, 64169927267L,
                64169927199L, 61261925629L, 58169127227L, 64169927363L,
                59169127063L, 64169927359L, 58169127291L, 61261925739L,
                59169127133L, 55139281911L, 56169127077L, 59169127167L
        };
        /**
         * fresh值的偏移位数
         */
        private final static int FRESH_BIT_OFFSET = 32;
        /**
         * 校验码的偏移位数
         */
        private final static int CHECK_CODE_BIT_OFFSET = 36;
        /**
         * fresh值的掩码,4位
         */
        private final static int FRESH_MASK = 0xF;
        /**
         * 验证码的掩码,14位
         */
        private final static int CHECK_CODE_MASK = 0b11111111111111;
        /**
         * 载荷的掩码,36位
         */
        private final static long PAYLOAD_MASK = 0xFFFFFFFFFL;
        /**
         * 序列号掩码,32位
         */
        private final static long SERIAL_NUM_MASK = 0xFFFFFFFFL;
        /**
         * 序列号加权运算的秘钥表
         */
        private final static int[][] PRIME_TABLE = {
                {23, 59, 241, 61, 607, 67, 977, 1217, 1289, 1601},
                {79, 83, 107, 439, 313, 619, 911, 1049, 1237},
                {173, 211, 499, 673, 823, 941, 1039, 1213, 1429, 1259},
                {31, 293, 311, 349, 431, 577, 757, 883, 1009, 1657},
                {353, 23, 367, 499, 599, 661, 719, 929, 1301, 1511},
                {103, 179, 353, 467, 577, 691, 811, 947, 1153, 1453},
                {213, 439, 257, 313, 571, 619, 743, 829, 983, 1103},
                {31, 151, 241, 349, 607, 677, 769, 823, 967, 1049},
                {61, 83, 109, 137, 151, 521, 701, 827, 1123},
                {23, 61, 199, 223, 479, 647, 739, 811, 947, 1019},
                {31, 109, 311, 467, 613, 743, 821, 881, 1031, 1171},
                {41, 173, 367, 401, 569, 683, 761, 883, 1009, 1181},
                {127, 283, 467, 577, 661, 773, 881, 967, 1097, 1289},
                {59, 137, 257, 347, 439, 547, 641, 839, 977, 1009},
                {61, 199, 313, 421, 613, 739, 827, 941, 1087, 1307},
                {19, 127, 241, 353, 499, 607, 811, 919, 1031, 1301}
        };
    
        /**
         * 生成兑换码
         *
         * @param serialNum 递增序列号
         * @return 兑换码
         */
        public static String generateCode(long serialNum, long fresh) {
            // 1.计算新鲜值
            fresh = fresh & FRESH_MASK;
            // 2.拼接payload,fresh(4位) + serialNum(32位)
            long payload = fresh << FRESH_BIT_OFFSET | serialNum;
            // 3.计算验证码
            long checkCode = calcCheckCode(payload, (int) fresh);
            System.out.println("checkCode = " + checkCode);
            // 4.payload做大质数异或运算,混淆数据
            payload ^= XOR_TABLE[(int) (checkCode & FRESH_MASK)];
            // 5.拼接兑换码明文: 校验码(14位) + payload(36位)
            long code = checkCode << CHECK_CODE_BIT_OFFSET | payload;
            // 6.转码
            return Base32.encode(code);
        }
    
        private static long calcCheckCode(long payload, int fresh) {
            // 1.获取码表
            int[] table = PRIME_TABLE[fresh];
            // 2.生成校验码,payload每4位乘加权数,求和,取最后13位结果
            long sum = 0;
            int index = 0;
            while (payload > 0) {
                sum += (payload & 0xf) * table[index++];
                payload >>>= 4;
            }
            return sum & CHECK_CODE_MASK;
        }
    
        public static long parseCode(String code) {
            if (code == null || !code.matches(RegexConstants.COUPON_CODE_PATTERN)) {
                // 兑换码格式错误
                throw new BadRequestException("无效兑换码");
            }
            // 1.Base32解码
            long num = Base32.decode(code);
            // 2.获取低36位,payload
            long payload = num & PAYLOAD_MASK;
            // 3.获取高14位,校验码
            int checkCode = (int) (num >>> CHECK_CODE_BIT_OFFSET);
            // 4.载荷异或大质数,解析出原来的payload
            payload ^= XOR_TABLE[(checkCode & FRESH_MASK)];
            // 5.获取高4位,fresh
            int fresh = (int) (payload >>> FRESH_BIT_OFFSET & FRESH_MASK);
            // 6.验证格式:
            if (calcCheckCode(payload, fresh) != checkCode) {
                throw new BadRequestException("无效兑换码");
            }
            return payload & SERIAL_NUM_MASK;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
  • 相关阅读:
    Flink 系统性学习笔记系列
    .NET 7 中的新增功能
    7000字+24张图带你彻底弄懂线程池
    CSS3提高:CSS3 2D转换
    荧光染料AF488 azide,AF488 叠氮化物,CAS:1679326-36-3
    基于docker和cri-dockerd部署kubernetes v1.25.3
    基于linux的基础线程知识大总结
    第14章 操作重载与类型转换【C++】
    PHP反序列化漏洞
    RabbitMQ 怎么保证可靠性、幂等性、消费顺序?
  • 原文地址:https://blog.csdn.net/Bilal_0/article/details/134019028