TOTP原理:
TOTP(Time-Based One-Time Password)算法是基于时间的一次性密码算法,根据客户端(被验方)和服务端(校验方)的密钥与当前时间(时间戳)计算一次性密码。使用TOTP算法,要求客户端(被验方)和服务端(校验方)保持时钟一致,且双方预先设置好一个共享密钥的前提下,在同一个时间(时间戳)算出来的TOTP值是一样的。
使用TOTP条件:
1.要求客户端(被验方)和服务端(校验方)保持时钟一致;
2.共享密钥一致;
既然要实现类型微信、支付宝付款码支付,首先以微信线下付款码支付为例(微信官付款码支付场景介绍如下):
商家扫码-->订单信息+商家信息+付款码提交给微信服务-->微信服务拿到付款码解密用户信息+订单信息+商家信息进行后续生成订单支付扣费操作;
这个过程中订单信息(消费金额)是由商家(商品)决定,那么消费者是谁呢?消费者就是这个“付款码”;付款码包含了消费者信息(这个应该是由付款码算法生成决定的,具体怎么实现不得而知),这一部分是个人理解,大致如此,今天的重点不是微信或者支付宝,今天主要说下怎么用totp算法实现一个类似微信、支付宝付款码支付的例子。
totp算法:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
public class TOTP {
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
private TOTP(){
}
/**
* This method uses the JCE to provide the crypto algorithm. HMAC computes a
* Hashed Message Authentication Code with the crypto hash algorithm as a
* parameter.
*
* @param crypto : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
* @param keyBytes : the bytes to use for the HMAC key
* @param text : the message or text to be authenticated
*/
private static byte[] hmacSha(String crypto, byte[] keyBytes, byte[] text) {
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey); return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
}
/**
* This method generates a TOTP value for the given set of parameters.
*
* @param key : the shared secret
* @param time : a value that reflects a time
* @param digits : number of digits to return
* @param crypto : the crypto function to use
* @return: digits
*/
public static int generateTOTP(byte[] key, long time, int digits, String crypto) {
byte[] msg = ByteBuffer.allocate(8).putLong(time).array();
byte[] hash = hmacSha(crypto, key, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[digits];
return otp;
}
}
TOTPUtil工具类
import org.apache.commons.codec.binary.Base32;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;public class TOTPUtils {
private static final int SECRET_SIZE = 10;
private static final int PASS_CODE_LENGTH = 6;
private static final int INTERVAL = 30;
private static final int WINDOW = 2;
private static final String CRYPTO = "HmacSHA1";
private static final Random rand = new Random();
public static String generateSecret() {
// Allocating the buffer
byte[] buffer = new byte[SECRET_SIZE];
// Filling the buffer with random numbers.
rand.nextBytes(buffer);
// Getting the key and converting it to Base32
Base32 codec = new Base32();
byte[] secretKey = Arrays.copyOf(buffer, SECRET_SIZE);
byte[] encodedKey = codec.encode(secretKey);
return new String(encodedKey);
}
public static Long generateTOTP(String secret, long time) {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
int totp = TOTP.generateTOTP(decodedKey, time, PASS_CODE_LENGTH, CRYPTO);
return Long.valueOf(totp);
}
public static boolean checkCode(String secret, long code) throws NoSuchAlgorithmException, InvalidKeyException {
Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret);
// Window is used to check codes generated in the near past.
// You can use this value to tune how far you're willing to go.
int window = WINDOW;
long currentInterval = getCurrentInterval();
for (int i = -window; i <= window; ++i) {
long hash = TOTP.generateTOTP(decodedKey, currentInterval + i, PASS_CODE_LENGTH, CRYPTO);
if (hash == code) { return true; } }
// The validation code is invalid.
return false;
}
private static long getCurrentInterval() {
long currentTimeSeconds = System.currentTimeMillis() / 1000;
return currentTimeSeconds / INTERVAL;
}
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException, InterruptedException, ParseException {
final String secret = "G3XFFLRBUXY7AQXB";
// final String secret = generateSecret();
System.out.println("secret:" + secret);
String dateFormat = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
// Date date = simpleDateFormat.parse("2024-12-12 23:59:59");
Date date = new Date();
System.out.println("生成totp时间:" + simpleDateFormat.format(date));
System.out.println("当前 时间:" + simpleDateFormat.format(new Date(System.currentTimeMillis())));
//时间戳
long timeSeconds = date.getTime() / 1000 / INTERVAL;
//当前时间时间戳
//long timeSeconds = getCurrentInterval();
Long totp = generateTOTP(secret, timeSeconds);
System.out.println("totp:" + totp);
//假设系统最大用户量最大1000亿(很极限了)
//大于用户量最小质数
Long zhishu = 1000000000007L;
//用户Id
Long userId = 2L;
//付款码
Long fukuanma = totp * zhishu + userId;
System.out.println("用户Id:" + userId);
System.out.println("付款码:" + fukuanma + ",付款码长度:" + String.valueOf(fukuanma).length());
System.out.println("反向解密付款码开始》》");
System.out.println("解密后用户Id:" + fukuanma % zhishu);
Long jmtotp = fukuanma / zhishu;
System.out.println("解密后totp:" + jmtotp);
boolean checkCodeResult = checkCode(secret, jmtotp);
System.out.println("totp校验结果:" + checkCodeResult);
}
}
测试结果:
客户端(被验方)生成付款码:付款码 = totp * 质数因子+ 用户ID;
服务端(校验方) totp = 付款码 / 质数因子;
服务端(校验方)用户ID = 付款码 % 质数因子;
服务端拿到用户ID+totp即可检验用户已经totp有效性;
服务端大致这些,等后面时候就写一个小程序前端(客户端(被验方)生成付款码)案例整合一下服务端(校验方)+服务端(校验方);