一、双因素认证的概念
双因素认证(2FA,Two Factor Authentication)又称双因子认证、两步验证,指的是是一种安全认证过程,需要用户提供两种不同类型的认证因子来表明自己的身份,包括密码、指纹、短信验证码、智能卡、生物识别等多种因素组合,从而提高用户账户的安全性和可靠性。
2FA认证流程如下:
- 用户登录应用程序。
- 用户输入登录凭证,通常是账号和密码,做初始身份验证。
- 验证成功后,提示用户提交第二个身份验证因子。
- 用户将第二个身份验证因子输入至应用程序,如果第二个身份验证因子通过,用户将通过身份验证并被授予对应的系统操作权限。
举个简单的例子,我们使用账号密码登录微博、豆瓣等应用时,命名账号密码都对了,但是还要输入手机验证码二次验证以确保安全性,这就是双因素验证。
虽然短信验证码实现简单,但是在实际场景中,一般会使用其它2FA方式替代:一则短信发送会产生费用,二则它也不是那么安全,它容易被拦截和伪造,SIM 卡也可以克隆。
一般来说,安全的双因素认证不是密码 + 短消息,而是密码+ TOTP。
二、TOTP
TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC6238。
1、TOTP步骤
第一步,用户开启双因素认证后,服务器生成一个密钥。
第二步:服务器提示用户扫描二维码(或者使用其他方式),把密钥保存到用户的手机。也就是说,服务器和用户的手机,现在都有了同一把密钥。
注意,密钥必须跟手机绑定。一旦用户更换手机,就必须生成全新的密钥。
第三步,用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为30秒。用户在有效期内,把这个哈希提交给服务器。
第四步,服务器也使用密钥和当前时间戳,生成一个哈希,跟用户提交的哈希比对。只要两者不一致,就拒绝登录。
2、TOTP原理
仔细看上面的步骤,你可能会有一个问题:手机客户端和服务器,如何保证30秒期间都得到同一个哈希呢?
答案就是下面的公式。
bash
TC = floor((unixtime(now) − unixtime(T0)) / TS)
上面的公式中,TC 表示一个时间计数器,unixtime(now)
是当前 Unix 时间戳,unixtime(T0)
是约定的起始时间点的时间戳,默认是0
,也就是1970年1月1日。TS 则是哈希有效期的时间长度,默认是30秒。因此,上面的公式就变成下面的形式。
bash
TC = floor(unixtime(now) / 30)
所以,只要在 30 秒以内,TC 的值都是一样的。前提是服务器和手机的时间必须同步。
接下来,就可以算出哈希了。
bash
TOTP = HASH(SecretKey, TC)
上面代码中,HASH
就是约定的哈希函数,默认是 SHA-1。
接下来在SpringBoot中集成TOTP实现双因素认证。
三、TOTP双因素认证实战
1、开源项目:GoogleAuth
TOTP自己手写还是稍稍有些复杂,去网上找了开源项目,发现一个比较好的实现:GoogleAuth,使用时需要引入Maven依赖:
xml
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
为了生成图片二维码,还需要引入:
xml
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<zxing.version>3.4.0</zxing.version>
</properties>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>${zxing.version}</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${zxing.version}</version>
</dependency>
GoogleAuth的API比较简单,常见API如下:
生成secret
java
/**
* 测试获取秘钥
*/
@Test
public void testGetKey() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
final GoogleAuthenticatorKey key = gAuth.createCredentials();
String key1 = key.getKey();
log.info(key1);
}
生成一次性验证码
java
/**
* 测试生成一次性验证码
*/
@Test
public void testGetTotpPassword() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
int code = gAuth.getTotpPassword("PWTBUDW6OAPV6E2EVMBHX2X7LH6MXRNE");
log.info("{}", code);
}
验证码验证
java
/**
* 测试秘钥验证
*/
@Test
public void testAuthorize() {
GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig
.GoogleAuthenticatorConfigBuilder()
//设置容忍度最小
.setWindowSize(1)
.build();
GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
int verificationCode = 448247;
String secretKey = "6VRFLPHNPQ4P2WAQWEIYPCQ43KIHVCJO";
boolean isCodeValid = gAuth.authorize(secretKey, verificationCode);
if (isCodeValid) {
log.info("匹配");
} else {
log.info("不匹配");
}
}
获取二维码(图片链接格式)
java
/**
* 获取图片二维码
*/
@Test
public void testGetOtpAuthURL() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
final GoogleAuthenticatorKey key = gAuth.createCredentials();
log.info(key.getKey());
String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthURL(
ISSUER,
userName,
key
);
log.info(otpAuthURL);
}
获取二维码(字节流)
java
@Test
public void testGetOtpAuthQrByteArrayOutputStream() throws WriterException, IOException {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
final GoogleAuthenticatorKey key = gAuth.createCredentials();
String otpAuthUri = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
ISSUER,
userName,
key);
QRCodeWriter qrWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrWriter.encode(otpAuthUri, BarcodeFormat.QR_CODE, 200, 200);
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", outputStream);
ImageIO.write(bufferedImage, "png", new File("temp.png"));
}
2、开源项目:光年Admin模板
为了能更直观的展示TOTP功能集成到SpringBoot的样子,我决定基于开源项目 Light Year Admin v5 去做前端的开发。Light Year Admin v5 是一个管理端模板,基于Bootstrap 5.1.3。线上体验地址:http://lyear.itshubao.com/v5/ ,也可以下载下来以后使用http-server
快速启动查看效果:
该项目是一个纯前端项目,为了更方便的集成到SpringBoot,我将其集成到了freemarker:
3、项目实战:2fa-demo
项目地址:https://gitee.com/kdyzm/2fa-demo
该项目依赖于MySQL,所以在运行前需要先准备好MySQL环境。
运行前准备
需要创建Mysql数据库,运行如下脚本:
sql
CREATE DATABASE `2fa_demo` ;
USE `2fa_demo`;
CREATE TABLE `sys_user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
`nick_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户昵称',
`password` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户密码',
`two_fa_secret` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '两步验证的秘钥',
`tow_fa_enabled` tinyint(1) DEFAULT '0' COMMENT '是否启用两步验证',
`create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'sys' COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除;1:已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
insert into `sys_user`(`id`,`user_name`,`nick_name`,`password`,`two_fa_secret`,`tow_fa_enabled`,`create_by`,`create_time`,`update_by`,`update_time`,`del_flag`) values
(1,'kdyzm','狂盗一枝梅','123456','H5C7U7M3FJN6DGL6EAAWHF6TVAAINAGU',0,'kdyzm','2025-06-16 13:51:03',NULL,'2025-06-17 13:37:13',0);
CREATE TABLE `sys_user_2fa` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint DEFAULT NULL COMMENT '用户id',
`secret_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '秘钥',
`scratch_codes` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '静态验证码',
`create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除;1:已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='两步验证相关临时配置表';
之后修改配置文件中的Mysql配置信息:
yaml
JDBC_MYSQL_HOST: localhost
JDBC_MYSQL_PORT: 3306
JDBC_MYSQL_DATABASE: 2fa_demo
JDBC_MYSQL_USERNAME: root
JDBC_MYSQL_PASSWORD: '123456'
项目启动
将项目导入Intelij,运行Application,出现如下即可表示运行成功
打开链接,进入登录页面
账号密码登录
在未登录的情况下打开链接http://localhost:8024,就会进入登录页面
登录账号:kdyzm
登录密码:123456
登录成功之后进入首页。
开启两步验证
登录成功之后进入首页,点击首页右上角两步验证
进入两步验证页面,由于未设置过二次验证,所以会提示去设置
点击"开启二次验证"按钮,进入两步骤验证向导
点击下一步输入电子邮件
点击下一步,进入关键的验证器配置步骤
在这一步,IOS下可以安装Authenticator
或者微信小程序搜索"MFA
",使用"腾讯身份验证器
"扫描二维码完成验证器设置,注意,如果多次扫描相同的二维码,需要删除上次扫描的记录。
点击下一步,校验配置的正确性:
提示配置开启成功即表示已配置成功。
验证两步验证
退出登录,回到登录页,输入账号密码登录,登录成功后不再跳转到首页,而是跳转到二次验证页面:
从手机上获取动态验证码,即可成功登录系统。
关闭两步验证
进入首页后,再次进入两步验证页面,即可看到关闭按钮
关闭后再次登录系统,就不会进入两步验证页面了。
4、实战总结
由于本项目案例只关注2FA相关的内容,而光年Admin模板的静态模板只实现了登录以及2FA相关的功能,而且时间匆忙代码比较糙。。。
该项目还剩下一些问题没实现
- 二次验证的记住设备功能
- 8位数的静态码未实现校验功能
- 设备丢失静态码找回功能未实现
以后有时间再补充了。
最后,欢迎关注我的博客呀:一枝梅的博客
END.