本文分享了支付动态码设计实现,及TOTP算法和Luhm算法过程解析。
支付动态码
业务需求会员余额可被扫动态码直接扣款,暂未查询到相关资料,于是在引用成熟算法的基础上简单设计了一个支付动态码规则。大佬指点请发邮件。
设计思路
参考现有成熟支付码为18位数字,且每分钟变化一次,同时动态码唯一并识别到会员,并且有一定安全性。
- 1.会员id不能明文出现在支付码中,且位数太长,只能映射到存储
- 2.支付码有业务规则,不能通过uuid或者其他类似规则生成,并且要区别于其他支付码,使用前俩位识别
- 3.每分钟更新可通过设置过期时间实现,定为TOTP算法生成部分位数
- 4.映射会员需要校验识别会员,定为4位随机数缓存映射+会员id十进制转八进制后三位校验
- 5.参考银行卡号等生成规则使用Luhm算法校验支付码的正确性。
- 6.保证支付码的当前唯一性,通过支付码为key保存,确保支付码唯一。
示例
xx,xxxxxxxx,xxxx,xxx,x
43,55878487,6107,262,9
- 1-2位:识别码,固定数字xx(2位),区别于其他支付码,如支付宝11-15开头等。
- 3-10位:TOTP算法,每60秒更新一次(8位)。
- 11-14位:随机数(4位)通过获取登录用户的随机数在缓存中校验。
- 15-17:会员id十进制转八进制后截取最后三位,校验用户(3位)。
- 18:Luhm算法,付款码正确性校验位(1位)。
TOTP算法
public static String generate(){
//使用会员id+会员支付密码密文+共享密码组成作为HmacSHA256秘钥
String key="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
long step=60000;
String crypto="HmacSHA256";
//取时间
long now = new Date().getTime();
//(时间-0)/60000后转换为16进制
//time=19B4AD8
String time = Long.toHexString((now-0)/step).toUpperCase();
StringBuilder timeBuilder = new StringBuilder(time);
//不够十六位前补0
//time=00000000019B4AD8
while (timeBuilder.length() < 16)
timeBuilder.insert(0, "0");
time = timeBuilder.toString();
//time转byte
byte[] msg = hexStr2Bytes(time);
//key转byte
byte[] k = key.getBytes();
//Mac HmacSHA256算法加密 Mac还包含HmacSHA1等其他算法
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey = new SecretKeySpec(k, "AES");
hmac.init(macKey);
byte[] target = hmac.doFinal(msg);
StringBuilder result;
//取最后一个字节(60)和0xf做按位与操作,取低四位 offset=1101(13)
//111101&1111->1101(13)
int offset = target[target.length - 1] & 0xf;
//从offset开始取4个字节,大端模式组成整数
int binary = ((target[offset] & 0x7f) << 24)
| ((target[offset + 1] & 0xff) << 16)
| ((target[offset + 2] & 0xff) << 8) | (target[offset + 3] & 0xff);
//取余
int otp = binary % 10000000;
result = new StringBuilder(Integer.toString(otp));
//不够位数前补0
while (result.length() < 10000000) {
result.insert(0, "0");
}
//得到加密后8位字符
return result.toString();
}
Luhm算法
public static char getCheckCode(String nonCheckCode) {
//43558784876107262
char[] chs = nonCheckCode.trim().toCharArray();
int luhmSum = 0;
//i从最后一位开始
//j从第一位开始
for (int i = chs.length - 1, j = 0; i >= 0; i--, j++) {
int k = chs[i] - '0';
if (j % 2 == 0) {
//如果为奇数位,则*2
//从最后一位开始,校验的时候为(包含校验位)偶数位,要得到校验位则是奇数位,也就是除了校验位第一位
k *= 2;
//如果是俩位数则十位数+个位数(-9)
k = k / 10 + k % 10;
}
//其他位不做处理,将所有值相加
luhmSum += k;
}
//示例43558784876107262
//2->4 2->4 0->0 6->3 8->7 8->7 8->7 5->1 4->8
//4+4+0+3+7+7+7+1+8+6+7+1+7+4+7+5+3=81
//如果为0取0,10-luhmSum%10得到最后校验位
//10-81%10=9
//得到的校验位是9 所以正确号码为435587848761072629
return (luhmSum % 10 == 0) ? '0' : (char) ((10 - luhmSum % 10) + '0');
}