兑换码生成与解析-个人笔记(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;
    }
}
相关推荐
java干货2 分钟前
优雅停机!Spring Boot 应用如何使用 Hook 线程完成“身后事”?
java·spring boot·后端
tealcwu5 分钟前
【Unity技巧】实现在Play时自动保存当前场景
java·unity·游戏引擎
uup5 分钟前
Java 多线程下的可见性问题
java
用户8307196840825 分钟前
通过泛型限制集合只读或只写
java
Pluchon10 分钟前
硅基计划4.0 算法 记忆化搜索
java·数据结构·算法·leetcode·决策树·深度优先
CoderYanger10 分钟前
动态规划算法-简单多状态dp问题:18.买卖股票的最佳时机Ⅳ
开发语言·算法·leetcode·动态规划·1024程序员节
大飞哥~BigFei10 分钟前
deploy发布项目到国外中央仓库报如下错误Project name is missing
java
白羊无名小猪11 分钟前
正则表达式(捕获组)
java·mysql·正则表达式
狂奔小菜鸡12 分钟前
Day23 | Java泛型详解
java·后端·java ee
onejson13 分钟前
idea中一键执行maven和应用重启
java·maven·intellij-idea