Hutool 秒速实现 2FA 两步验证
前言
随着网络安全威胁的日益复杂,传统的用户名和密码认证方式已不足以提供足够的安全保障。为了增强用户账户的安全性,越来越多的应用和服务开始采用多因素认证(MFA)。基于时间的一次性密码(TOTP, Time-based One-Time Password)是多因素认证的一种流行实现方式,它通过生成随时间变化的一次性密码来提供额外的安全层。TOTP 算法由 RFC 6238 定义,广泛应用于各种安全应用中,如 Google Authenticator。
本文展示如何使用 Hutool 工具包实现 TOTP 功能,并结合 Google 的二维码生成工具 来简化密钥分发和用户配置过程。Hutool 是一个功能丰富的 Java 工具类库,提供了大量实用的功能,使得开发者能够更高效地开发应用程序。特别是其内置的支持 TOTP 和 QR Code 生成的功能,为实现多因素认证提供了极大的便利。
引入Hutool的依赖
Hutool 工具包
<!-- Hutool 工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
Google 验证码工具包
<!-- Google 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.3</version>
</dependency>
说明:
Hutool 可以按需引入依赖的,这里我没有太过于讲究,直接引入 all 所有包,另外生成 TOTP 的一次性密码需要生成二维码,而 Hutool 刚好就支持Google 的zxing二维码工具包,只不过使用 Hutool 的二维码工具包需要用户手动引入Zxing 包来完成支持
实现代码
package com.hsqyz.web.utils;
import cn.hutool.crypto.digest.otp.TOTP;
import cn.hutool.core.codec.Base32;
import cn.hutool.extra.qrcode.QrCodeUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.time.Instant;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* TOTP 测试类
*/
@Slf4j
public class TOTP_Test {
public static void main(String[] args) throws InterruptedException {
// 用户账户名
String account = "花伤情犹在"; // 用户账户名
// 指定密钥长度为 15 字节(120 位)
int numBytes = 15;
// 生成 QR Code URL
String qrCodeUrl = TOTP.generateGoogleSecretKey(account, numBytes);
System.out.println("QR Code URL for Google Authenticator: " + qrCodeUrl);
// 提取密钥并打印
String secretKey = extractSecretFromUri(qrCodeUrl);
System.out.println("提取的密钥是: " + secretKey);
// 创建 TOTP 对象,默认时间步长为30秒
byte[] keyBytes = Base32.decode(secretKey); // 将 Base32 编码的字符串转换为字节数组
TOTP totp = new TOTP(keyBytes);
// 生成二维码图片并保存为文件
QrCodeUtil.generate(qrCodeUrl, 200, 200, new File("totp_qrcode.png"));
System.out.println("二维码已保存为 'totp_qrcode.png'");
// 开始无限循环,持续打印最新的一次性密码
while (true) {
// 获取当前的一次性密码
int otpCode = totp.generate(Instant.now());
System.out.println("当前的一次性密码是: " + String.format("%06d", otpCode));
// 等待 1 秒,以匹配 TOTP 的时间窗口
Thread.sleep(1000 * 1);
}
}
/**
* 使用正则表达式从 URI 中提取密钥。
*
* @param uri 要解析的 URI 字符串
* @return 提取的密钥字符串
*/
private static String extractSecretFromUri(String uri) {
// 定义正则表达式
String regex = "secret=([^&]+)";
// 编译正则表达式
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(uri);
// 查找并提取密钥
if (matcher.find()) {
return matcher.group(1); // 返回第一个捕获组的内容
} else {
throw new IllegalArgumentException("URI 中没有找到 secret 参数");
}
}
}
运行效果:
使用 Google Authenticator 扫描加入查看:
为什么要使用Base32 进行密钥解码
Base32解码
使用 Base32 解码的原因在于 TOTP(基于时间的一次性密码)算法的工作方式。为了确保生成的一次性密码是安全且可验证的,TOTP 使用共享密钥(secret key)作为输入之一。这个共享密钥通常是通过 Base32 编码进行表示和传输的
Base32 编码的字符集
Base32 编码使用的是一个特定的字符集,包括:
- 大写字母:
A-Z
- 数字:
2-7
- 填充字符(可选):
=
完整字符集
Base32 的完整字符集是:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 2 3 4 5 6 7
如果密钥包含特殊字符会怎么样?
如果密钥中包含特殊字符并且尝试使用 Base32 编码,可能会导致一系列问题。Base32
编码有其特定的字符集和格式要求,如果密钥中包含不属于 Base32 字符集的字符,编码和解码过程可能会失败或产生不正确的结果。
- 编码失败:
如果密钥中包含不属于上述字符集的字符(例如 @, !, # 等),在进行 Base32 编码时,编码器可能会抛出异常或生成无效的编码字符串。 - 解码失败:
即使编码成功,但生成的 Base32 字符串中包含了非法字符,在解码时也会失败,因为这些字符无法被正确解析为原始字节数组。 - 数据损坏:
如果某些特殊字符被错误地处理或替换,可能会导致最终解码出来的字节数组与原始密钥不一致,从而影响 TOTP 一次性密码的生成和验证。
解决方法
为了避免这些问题,建议确保密钥只包含合法的 Base32 字符。如果您需要生成一个新的密钥,可以使用专门的工具或库来生成符合 Base32 格式的随机密钥。
示例:生成合法的 Base32 密钥
import cn.hutool.core.codec.Base32;
import cn.hutool.crypto.digest.DigestUtil;
public class GenerateValidBase32Key {
public static void main(String[] args) {
// 生成随机字节数组(例如 15 字节)
byte[] randomBytes = DigestUtil.randomBytes(15);
// 将字节数组编码为 Base32 字符串
String base32Key = Base32.encode(randomBytes);
System.out.println("生成的合法 Base32 密钥是: " + base32Key);
}
}