一、双因素认证的概念
双因素认证(2FA,Two Factor Authentication)又称双因子认证、两步验证,指的是是一种安全认证过程,需要用户提供两种不同类型的认证因子来表明自己的身份,包括密码、指纹、短信验证码、智能卡、生物识别等多种因素组合,从而提高用户账户的安全性和可靠性。
2FA认证流程如下:
- 用户登录应用程序。
- 用户输入登录凭证,通常是账号和密码,做初始身份验证。
- 验证成功后,提示用户提交第二个身份验证因子。
- 用户将第二个身份验证因子输入至应用程序,如果第二个身份验证因子通过,用户将通过身份验证并被授予对应的系统操作权限。
举个简单的例子,我们使用账号密码登录微博、豆瓣等应用时,命名账号密码都对了,但是还要输入手机验证码二次验证以确保安全性,这就是双因素验证。
虽然短信验证码实现简单,但是在实际场景中,一般会使用其它2FA方式替代:一则短信发送会产生费用,二则它也不是那么安全,它容易被拦截和伪造,SIM 卡也可以克隆。
一般来说,安全的双因素认证不是密码 + 短消息,而是密码+ TOTP。
二、TOTP
TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC6238。
1、TOTP步骤
第一步,用户开启双因素认证后,服务器生成一个密钥。
第二步:服务器提示用户扫描二维码(或者使用其他方式),把密钥保存到用户的手机。也就是说,服务器和用户的手机,现在都有了同一把密钥。
注意,密钥必须跟手机绑定。一旦用户更换手机,就必须生成全新的密钥。
第三步,用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为30秒。用户在有效期内,把这个哈希提交给服务器。
第四步,服务器也使用密钥和当前时间戳,生成一个哈希,跟用户提交的哈希比对。只要两者不一致,就拒绝登录。
2、TOTP原理
仔细看上面的步骤,你可能会有一个问题:手机客户端和服务器,如何保证30秒期间都得到同一个哈希呢?
答案就是下面的公式。- TC = floor((unixtime(now) − unixtime(T0)) / TS)
复制代码 上面的公式中,TC 表示一个时间计数器,unixtime(now)是当前 Unix 时间戳,unixtime(T0)是约定的起始时间点的时间戳,默认是0,也就是1970年1月1日。TS 则是哈希有效期的时间长度,默认是30秒。因此,上面的公式就变成下面的形式。- TC = floor(unixtime(now) / 30)
复制代码 所以,只要在 30 秒以内,TC 的值都是一样的。前提是服务器和手机的时间必须同步。
接下来,就可以算出哈希了。- TOTP = HASH(SecretKey, TC)
复制代码 上面代码中,HASH就是约定的哈希函数,默认是 SHA-1。
接下来在SpringBoot中集成TOTP实现双因素认证。
三、TOTP双因素认证实战
1、开源项目:GoogleAuth
TOTP自己手写还是稍稍有些复杂,去网上找了开源项目,发现一个比较好的实现:GoogleAuth,使用时需要引入Maven依赖:- <dependency>
- <groupId>com.warrenstrange</groupId>
- googleauth</artifactId>
- <version>1.5.0</version>
- </dependency>
复制代码 为了生成图片二维码,还需要引入:- <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>
- core</artifactId>
- <version>${zxing.version}</version>
- </dependency>
- <dependency>
- <groupId>com.google.zxing</groupId>
- javase</artifactId>
- <version>${zxing.version}</version>
- </dependency>
复制代码 GoogleAuth的API比较简单,常见API如下:
生成secret
- /**
- * 测试获取秘钥
- */
- @Test
- public void testGetKey() {
- GoogleAuthenticator gAuth = new GoogleAuthenticator();
- final GoogleAuthenticatorKey key = gAuth.createCredentials();
- String key1 = key.getKey();
- log.info(key1);
- }
复制代码 生成一次性验证码
- /**
- * 测试生成一次性验证码
- */
- @Test
- public void testGetTotpPassword() {
- GoogleAuthenticator gAuth = new GoogleAuthenticator();
- int code = gAuth.getTotpPassword("PWTBUDW6OAPV6E2EVMBHX2X7LH6MXRNE");
- log.info("{}", code);
- }
复制代码 验证码验证
- /**
- * 测试秘钥验证
- */
- @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("不匹配");
- }
- }
复制代码 获取二维码(图片链接格式)
- /**
- * 获取图片二维码
- */
- @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);
- }
复制代码 获取二维码(字节流)
- @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数据库,运行如下脚本:- 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配置信息:- 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.
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |