1.需求分析
-
兑换码长度为10字符,包含24个大写字母和8个数字。
-
兑换码需要保证唯一性,不可重复兑换。
-
需要防止猜测和爆刷攻击。
-
兑换码生成和验证的算法需要高效,避免对数据库带来较大的压力。
导航
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.兑换码的加密过程:
- 首先利用优惠券id计算新鲜值 f
- 将f和序列号s拼接,得到载荷payload
- 然后以f为角标,从提前准备好的16组加权码表中选一组
- 对payload做加权计算,得到校验码 c
- 利用c的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key
- 将payload与key做异或,作为新payload2
- 然后拼接兑换码明文:f (4位) + payload2(36位)
- 利用Base32对密文转码,生成兑换码
5.2.4.兑换码的解密过程:
- 首先利用Base32解码兑换码,得到明文数值num
- 取num的高14位得到c1,取num低36位得payload
- 利用c1的后4位做角标,从提前准备好的异或密钥表中选择一个密钥:key
- 将payload与key做异或,作为新payload2
- 利用加密时的算法,用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;
}
}