一、TOTP 是什么?
TOTP(Time-based One-Time Password) 即 基于时间的一次性密码算法,是一种广泛用于双因素认证(2FA)的安全机制。其核心原理是通过 时间戳 和 密钥 生成动态密码,每次生成的密码仅在短时间内有效,从而防止重放攻击和密码泄露风险。
二、TOTP 的核心原理
TOTP 基于 HOTP(HMAC-based One-Time Password)算法,并引入 时间因子 实现动态性。流程如下:
密钥生成
用户在启用 TOTP 时,服务器会生成一个 密钥(Secret Key),通常为 Base32 编码的字符串(如 JBSWY3DPEHPK3PXP
)。
密钥需同时存储在服务器和用户的认证设备(如 Google Authenticator、微软 Authenticator)中。
时间步长(Time Step)
将时间划分为固定长度的 时间窗口(通常为 30 秒,即 TIME_STEP = 30
)。
当前时间戳(以秒为单位)除以时间步长,得到 时间计数器(Counter),例如:
counter = current_time_seconds / TIME_STEP
。
动态密码生成
使用 HMAC 算法(如 HmacSHA1、HmacSHA256)对 密钥
和 时间计数器
进行哈希计算,得到哈希值。
从哈希值中提取 动态截取值,转换为 固定长度的数字密码(通常为 6 位或 8 位)。
三、TOTP 的关键要素
要素 | 说明 |
---|---|
密钥(Secret Key) | 长度建议至少 16 字节(Base32 编码后为 20 字符),需安全存储,避免泄露。 |
时间步长 | 常见为 30 秒或 60 秒,时间步长越短,安全性越高,但用户输入密码的时间窗口越小。 |
哈希算法 | 支持 HmacSHA1(默认)、HmacSHA256 等,SHA256 比 SHA1 更安全。 |
密码长度 | 通常为 6 位或 8 位数字,位数越多,暴力破解难度越大。 |
四、TOTP 的工作流程示例
假设:
时间步长 TIME_STEP = 30
秒,
密钥为 JBSWY3DPEHPK3PXP
(Base32 编码,对应原始字节为 1234567890abcdef
),
当前时间戳为 1689000000
秒(对应 counter = 1689000000 / 30 = 56300000
)。
生成时间计数器的字节数组
将 counter
(56300000)转换为 8 字节的大端序(Big-Endian)数组:
data = [0x00, 0x35, 0x82, 0xA8, 0x00, 0x00, 0x00, 0x00]
。
计算 HMAC-SHA1 哈希值
使用密钥和 data
计算哈希值,假设结果为:
0x4A 0x9F 0x...
(实际为 20 字节)。
动态截取与转换
取哈希值最后一个字节的低 4 位作为偏移量 offset
(如 0x0F
),
从 offset
位置开始截取 4 字节,转换为 31 位整数(避免符号位影响),
对 10^6
取模得到 6 位数字密码,例如 123456
。
五、TOTP 的优缺点
优点 | 缺点 |
---|---|
无需网络连接(依赖本地时间同步) | 依赖设备时间同步,时间偏差可能导致验证失败 |
动态密码时效性强(30-60 秒过期) | 无法抵抗中间人攻击(需配合其他安全措施) |
实现简单,兼容多种设备和协议 | 密钥泄露后存在安全风险 |
六、常见问题与解决方案
时间不同步问题
现象:用户设备与服务器时间偏差较大,导致生成的 counter
不一致。
解决方案:
在验证时允许 前后多个时间窗口(如 counter ±1
或 ±2
),扩大匹配范围。
定期同步设备时间(如通过 NTP 协议)。
密钥管理
避免硬编码密钥,建议通过安全通道(如 HTTPS)传输密钥,并使用加密存储(如 AES)。
用户端可通过 QR 码(如 otpauth://
协议)快速导入密钥,减少手动输入错误。
算法选择
优先使用 HmacSHA256 或 HmacSHA512 替代默认的 HmacSHA1,提升抗碰撞能力。
七、TOTP 的应用场景
双因素认证(2FA):如银行账户、云服务(AWS、Google)、企业 VPN 等。
一次性密码登录:替代传统静态密码,用于敏感操作验证。
离线场景:如无网络环境下的设备身份验证。
代码实战:
Controller层:主要生成二维码和SecretKey(把它存到数据库中)
package com.ruoyi.web.controller.system;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.totp.TotpUtils;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* 2FA认证控制器
*/
@RestController
@RequestMapping("/api/system/2fa")
public class AuthController extends BaseController {
@Autowired
private ISysUserService userService;
public static final String BASE64_IMAGE_PREFIX = "data:image/png;base64,";
/**
* 获取2FA设置信息(生成密钥和QR码URL)
* SysUser表 新加字段
* private String secretKey; // 存储TOTP密钥
* private char usingTwoFA; // 是否启用2FA
*/
@GetMapping("/setup")
public AjaxResult setup2FA(String userName) throws Exception {
SysUser user = userService.selectUserByUserName(userName);
Map<String, Object> result = new HashMap<>();
if (user == null) {
return error("用户不存在");
}else {
// if (user.getUsingTwoFA() == '1') {
// return error("用户已启用2FA");
// } else {
// 生成新的密钥
String secretKey = TotpUtils.generateSecretKey();
user.setSecretKey(secretKey);//用户表设置SecretKey和UsingTwoFA
user.setUsingTwoFA('1');
userService.updateUser(user);
// 生成QR码URL
String qrCodeUrl = TotpUtils.getQRCodeUrl(
secretKey,
user.getUserName(),
URLEncoder.encode("若依系统", "UTF-8")
);
// 生成二维码图像
String qrCodeImage = generateQRCodeImage(qrCodeUrl);
result.put("qrCodeImage", BASE64_IMAGE_PREFIX + qrCodeImage); // 二维码图像的Base64编码
result.put("secretKey", secretKey);
// }
return success(result);
}
}
// 生成二维码图像的Base64编码
private String generateQRCodeImage(String content) throws Exception {
int width = 200;
int height = 200;
String format = "png";
// 使用ZXing库生成二维码
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height);
BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
// 将图像转换为Base64编码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
}
}
TotpUtils:SecretKey+时间戳生成的动态验证码
package com.ruoyi.common.utils.totp;
import org.apache.commons.codec.binary.Base32;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
/**
* TOTP工具类,用于生成和验证一次性密码
*/
public class TotpUtils {
private static final int TIME_STEP = 30; // 时间步长30秒
private static final int CODE_DIGITS = 6; // 验证码6位
private static final String HMAC_ALGORITHM = "HmacSHA1"; // 加密算法
/**
* 生成随机密钥
*/
public static String generateSecretKey() {
byte[] buffer = new byte[10];
new java.util.Random().nextBytes(buffer);
return new Base32().encodeToString(buffer);
}
/**
* 生成当前时间的TOTP码
*/
public static String generateTOTP(String secretKey) {
long time = new Date().getTime() / 1000 / TIME_STEP;
return generateHOTP(secretKey, time);
}
public static boolean verifyTOTP(String secretKey, String code) {
if (secretKey == null || code == null) {
return false;
}
long time = System.currentTimeMillis() / 1000 / TIME_STEP; // 统一时间计算方式
for (int i = -1; i <= 1; i++) { // 恢复时间窗口
String expectedCode = generateHOTP(secretKey, time + i);
if (MessageDigest.isEqual(expectedCode.getBytes(), code.getBytes())) { // 常量时间比较
return true;
}
}
return false;
}
private static String generateHOTP(String secretKey, long counter) {
if (secretKey == null) {
throw new IllegalArgumentException("Secret key cannot be null"); // 添加空值校验
}
byte[] key = new Base32().decode(secretKey);
byte[] data = new byte[8];
for (int i = 7; i >= 0; i--) {
data[i] = (byte) (counter & 0xff);
counter >>= 8;
}
try {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(new SecretKeySpec(key, HMAC_ALGORITHM));
byte[] hash = mac.doFinal(data);
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 % (int) Math.pow(10, CODE_DIGITS);
return String.format("%0" + CODE_DIGITS + "d", otp);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("生成HOTP失败", e);
}
}
// 生成QR码URL
public static String getQRCodeUrl(String secret, String username, String issuer) {
return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s",
issuer, username, secret, issuer);
}
}
最后就是修改登录逻辑了,需要根据实际需求更改
@PostMapping("/login")
public AjaxResult login1(@RequestBody LoginBody loginBody) {
AjaxResult ajax = AjaxResult.success();
try {
// 解密用户名和密码
String username = SM2Util.Decrypt(SM2_PRIVATE_KEY, loginBody.getUsername());
String password = SM2Util.Decrypt(SM2_PRIVATE_KEY, loginBody.getPassword());
// 第一步:验证用户名和密码
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
throw new AuthenticationException("登录失败,用户名或密码错误");
}
// 第二步:检查是否需要双因素认证
if ( user.getUsingTwoFA()=='1') {
// 需要双因素认证,但用户未提供验证码
if (StringUtils.isEmpty(loginBody.getCode())) {
// 返回需要2FA的提示,要求用户提供验证码
ajax.put("need2FA", true);
ajax.put("username", username); // 保留用户名用于下一步验证
return ajax;
}
// 验证双因素验证码
if (!TotpUtils.verifyTOTP(user.getSecretKey(), loginBody.getCode())) {
throw new AuthenticationException("双因素验证码错误");
}
}
// 第三步:生成令牌(如果通过了所有验证)
Map<String, Object> map = loginService.login(username, password, loginBody.getCode(), loginBody.getUuid());
ajax.put(Constants.TOKEN, map.get("token").toString());
ajax.put("lgbDeptId", map.get("lgbDeptId").toString());
ajax.put("loginUserType", map.get("loginUserType").toString());
} catch (Exception e) {
// 处理异常
log.error("登录失败", e);
return error(e.getMessage());
}
return ajax;
}
附件:相关Maven
<dependencies>
<!-- 核心依赖:Base32编码/解码 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- 可选:Google Authenticator兼容库 -->
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.1.1</version>
</dependency>
<!-- 可选:Hutool工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- ZXing(二维码生成) -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.2</version>
</dependency>
</dependencies>
暂无评论内容