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

1.需求分析

2.实现方案

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

3.加密过程

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

4.解密过程

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

5.工具类

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

5.1Base32工具类

代码如下:

java 复制代码
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;
   }
}

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比较,一致则通过

代码如下:

java 复制代码
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;
    }
}
相关推荐
小冉在学习13 分钟前
day53 图论章节刷题Part05(并查集理论基础、寻找存在的路径)
java·算法·图论
代码之光_19801 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi1 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
对许1 小时前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
小鑫记得努力2 小时前
Java类和对象(下篇)
java
binishuaio2 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE2 小时前
【Java SE】StringBuffer
java·开发语言