TOTP:Time-based One-Time Password写,基于对称密钥与时间戳算法的一次性认证码。 时间同步,基于客户端的动态口令和动态口令验证服务器的时间比对,默认每30秒产生一个新口令,要求客户端和服务器能够十分精确的保持正确的时钟,客户端和服务端基于时间计算的动态口令才能一致。
算法安全的核心在于密钥 , 每个人通过对应账户生成的密钥是不同的 . 当他们用同一个算法加密时 , 会生成不同的随机密码,认证时客户端需要使用阿里身份宝,Goole 身份验证器等工具生成认证码。
public class OptUtil {
private OptUtil() {}
/** 生成32位的otp密钥 */
@SneakyThrows
public static String generateSecretKey() {
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(Base64.decodeBase64("b5rpa6kcdux2fpq0tkrt2wwsqeqmho0d"));
byte[] buffer = sr.generateSeed(20);
Base32 codec = new Base32();
return codec.encodeToString(buffer);
}
/** 校验 opt code */
public static boolean checkCode(String secret, int code) {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
long nowInSeconds = System.currentTimeMillis() / 1000L;
//考虑到网络延迟,预留1秒钟容错
for (int i = 0; i < 2; i++) {
long time = (nowInSeconds - i) / 30L;
int hash = verifyCode(decodedKey, time);
if (hash == code) {
return true;
}
}
//Opt校验不通过
return false;
}
@SneakyThrows
private static int verifyCode(byte[] key, long t) {
byte[] data = new byte[8];
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
final String algorithm = "HmacSHA1";
SecretKeySpec signKey = new SecretKeySpec(key, algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(signKey);
byte[] hash = mac.doFinal(data);
int offset = hash[20 - 1] & 0xF;
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return (int) truncatedHash;
}
}
<dependency>
<groupId>com.google.zxinggroupId>
<artifactId>coreartifactId>
<version>3.3.3version>
dependency>
/**
* @description 生成二维码工具
*/
public class QrCodeUtil {
private QrCodeUtil() { }
/**
* @description: 生成一个普通的黑白二维码
* @param content 二维码内容
* @param width 生成图片矿都
* @param height 生成图片高度
* @return 二维码图片字节流
**/
public static byte[] drawQrCode(String content, int width, int height) throws WriterException, IOException {
MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, width, height, new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
put(EncodeHintType.CHARACTER_SET, "UTF-8");
put(EncodeHintType.MARGIN, 0);
}
});
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 开始利用二维码数据图片,分别设为黑(0xFFFFFFFF)白(0xFF000000)两色
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
image.flush();
//分配13Kb
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(13312);
ImageIO.write(image, "png", outputStream);
return outputStream.toByteArray();
}
}
/** 生成密钥绑定二维码 */
@GetMapping(path = "/img/zx")
@SneakyThrows
public String keyZxImg(){
String key = OptUtil.generateSecretKey();
final String QRCODE_TEMPLATE = "otpauth://totp/xxx:{0}?secret={1}&issuer={2}";
String content = MessageFormat.format(QRCODE_TEMPLATE, "user@app", key, "DAS_TOTP");
byte[] contents = QrCodeUtil.drawQrCode(content, 320, 320);
return Base64.getEncoder().encodeToString(contents);
}
客户端使用类似阿里身份宝等工具扫描“密钥绑定二维码”接口生成的base64编码图片即可绑定密钥
/** 使用opt验证码登录 */
@GetMapping(path = "/check")
public boolean login(@RequestParam int optCode){
return OptUtil.checkCode(key_cache, optCode);
}