找回密码
 立即注册
首页 业界区 业界 SpringBoot集成TOTP双因素认证(2FA)实战

SpringBoot集成TOTP双因素认证(2FA)实战

谧怏弦 昨天 16:16
一、双因素认证的概念

双因素认证(2FA,Two Factor Authentication)又称双因子认证、两步验证,指的是是一种安全认证过程,需要用户提供两种不同类型的认证因子来表明自己的身份,包括密码、指纹、短信验证码、智能卡、生物识别等多种因素组合,从而提高用户账户的安全性和可靠性。
2FA认证流程如下:

  • 用户登录应用程序。
  • 用户输入登录凭证,通常是账号和密码,做初始身份验证。
  • 验证成功后,提示用户提交第二个身份验证因子。
  • 用户将第二个身份验证因子输入至应用程序,如果第二个身份验证因子通过,用户将通过身份验证并被授予对应的系统操作权限。
举个简单的例子,我们使用账号密码登录微博、豆瓣等应用时,命名账号密码都对了,但是还要输入手机验证码二次验证以确保安全性,这就是双因素验证。
虽然短信验证码实现简单,但是在实际场景中,一般会使用其它2FA方式替代:一则短信发送会产生费用,二则它也不是那么安全,它容易被拦截和伪造,SIM 卡也可以克隆。
一般来说,安全的双因素认证不是密码 + 短消息,而是密码+ TOTP。
二、TOTP

TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC6238。
1、TOTP步骤

第一步,用户开启双因素认证后,服务器生成一个密钥。
第二步:服务器提示用户扫描二维码(或者使用其他方式),把密钥保存到用户的手机。也就是说,服务器和用户的手机,现在都有了同一把密钥。
1.png
注意,密钥必须跟手机绑定。一旦用户更换手机,就必须生成全新的密钥。
第三步,用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为30秒。用户在有效期内,把这个哈希提交给服务器。
2.jpeg
第四步,服务器也使用密钥和当前时间戳,生成一个哈希,跟用户提交的哈希比对。只要两者不一致,就拒绝登录。
2、TOTP原理

仔细看上面的步骤,你可能会有一个问题:手机客户端和服务器,如何保证30秒期间都得到同一个哈希呢?
答案就是下面的公式。
  1. TC = floor((unixtime(now) − unixtime(T0)) / TS)
复制代码
上面的公式中,TC 表示一个时间计数器,unixtime(now)是当前 Unix 时间戳,unixtime(T0)是约定的起始时间点的时间戳,默认是0,也就是1970年1月1日。TS 则是哈希有效期的时间长度,默认是30秒。因此,上面的公式就变成下面的形式。
  1. TC = floor(unixtime(now) / 30)
复制代码
所以,只要在 30 秒以内,TC 的值都是一样的。前提是服务器和手机的时间必须同步。
接下来,就可以算出哈希了。
  1. TOTP = HASH(SecretKey, TC)
复制代码
上面代码中,HASH就是约定的哈希函数,默认是 SHA-1。
接下来在SpringBoot中集成TOTP实现双因素认证。
三、TOTP双因素认证实战

1、开源项目:GoogleAuth

TOTP自己手写还是稍稍有些复杂,去网上找了开源项目,发现一个比较好的实现:GoogleAuth,使用时需要引入Maven依赖:
  1. <dependency>
  2.     <groupId>com.warrenstrange</groupId>
  3.     googleauth</artifactId>
  4.     <version>1.5.0</version>
  5. </dependency>
复制代码
为了生成图片二维码,还需要引入:
  1. <properties>
  2.     <maven.compiler.source>8</maven.compiler.source>
  3.     <maven.compiler.target>8</maven.compiler.target>
  4.     <zxing.version>3.4.0</zxing.version>
  5. </properties>
  6. <dependency>
  7.     <groupId>com.google.zxing</groupId>
  8.     core</artifactId>
  9.     <version>${zxing.version}</version>
  10. </dependency>
  11. <dependency>
  12.     <groupId>com.google.zxing</groupId>
  13.     javase</artifactId>
  14.     <version>${zxing.version}</version>
  15. </dependency>
复制代码
GoogleAuth的API比较简单,常见API如下:
生成secret
  1. /**
  2. * 测试获取秘钥
  3. */
  4. @Test
  5. public void testGetKey() {
  6.     GoogleAuthenticator gAuth = new GoogleAuthenticator();
  7.     final GoogleAuthenticatorKey key = gAuth.createCredentials();
  8.     String key1 = key.getKey();
  9.     log.info(key1);
  10. }
复制代码
生成一次性验证码
  1. /**
  2. * 测试生成一次性验证码
  3. */
  4. @Test
  5. public void testGetTotpPassword() {
  6.     GoogleAuthenticator gAuth = new GoogleAuthenticator();
  7.     int code = gAuth.getTotpPassword("PWTBUDW6OAPV6E2EVMBHX2X7LH6MXRNE");
  8.     log.info("{}", code);
  9. }
复制代码
验证码验证
  1. /**
  2. * 测试秘钥验证
  3. */
  4. @Test
  5. public void testAuthorize() {
  6.     GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig
  7.             .GoogleAuthenticatorConfigBuilder()
  8.             //设置容忍度最小
  9.             .setWindowSize(1)
  10.             .build();
  11.     GoogleAuthenticator gAuth = new GoogleAuthenticator(config);
  12.     int verificationCode = 448247;
  13.     String secretKey = "6VRFLPHNPQ4P2WAQWEIYPCQ43KIHVCJO";
  14.     boolean isCodeValid = gAuth.authorize(secretKey, verificationCode);
  15.     if (isCodeValid) {
  16.         log.info("匹配");
  17.     } else {
  18.         log.info("不匹配");
  19.     }
  20. }
复制代码
获取二维码(图片链接格式)
  1. /**
  2. * 获取图片二维码
  3. */
  4. @Test
  5. public void testGetOtpAuthURL() {
  6.     GoogleAuthenticator gAuth = new GoogleAuthenticator();
  7.     final GoogleAuthenticatorKey key = gAuth.createCredentials();
  8.     log.info(key.getKey());
  9.     String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthURL(
  10.             ISSUER,
  11.             userName,
  12.             key
  13.     );
  14.     log.info(otpAuthURL);
  15. }
复制代码
获取二维码(字节流)
  1. @Test
  2. public void testGetOtpAuthQrByteArrayOutputStream() throws WriterException, IOException {
  3.     GoogleAuthenticator gAuth = new GoogleAuthenticator();
  4.     final GoogleAuthenticatorKey key = gAuth.createCredentials();
  5.     String otpAuthUri = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL(
  6.             ISSUER,
  7.             userName,
  8.             key);
  9.     QRCodeWriter qrWriter = new QRCodeWriter();
  10.     BitMatrix bitMatrix = qrWriter.encode(otpAuthUri, BarcodeFormat.QR_CODE, 200, 200);
  11.     BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
  12.     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  13.     ImageIO.write(bufferedImage, "png", outputStream);
  14.     ImageIO.write(bufferedImage, "png", new File("temp.png"));
  15. }
复制代码
2、开源项目:光年Admin模板

为了能更直观的展示TOTP功能集成到SpringBoot的样子,我决定基于开源项目 Light Year Admin v5 去做前端的开发。Light Year Admin v5 是一个管理端模板,基于Bootstrap 5.1.3。线上体验地址:http://lyear.itshubao.com/v5/ ,也可以下载下来以后使用http-server快速启动查看效果:
3.jpeg
该项目是一个纯前端项目,为了更方便的集成到SpringBoot,我将其集成到了freemarker:
4.png
3、项目实战:2fa-demo

项目地址:https://gitee.com/kdyzm/2fa-demo
该项目依赖于MySQL,所以在运行前需要先准备好MySQL环境。
运行前准备

需要创建Mysql数据库,运行如下脚本:
  1. CREATE DATABASE `2fa_demo` ;
  2. USE `2fa_demo`;
  3. CREATE TABLE `sys_user` (
  4.   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  5.   `user_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
  6.   `nick_name` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户昵称',
  7.   `password` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户密码',
  8.   `two_fa_secret` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '两步验证的秘钥',
  9.   `tow_fa_enabled` tinyint(1) DEFAULT '0' COMMENT '是否启用两步验证',
  10.   `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'sys' COMMENT '创建人',
  11.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  12.   `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
  13.   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  14.   `del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除;1:已删除',
  15.   PRIMARY KEY (`id`)
  16. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
  17. 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
  18. (1,'kdyzm','狂盗一枝梅','123456','H5C7U7M3FJN6DGL6EAAWHF6TVAAINAGU',0,'kdyzm','2025-06-16 13:51:03',NULL,'2025-06-17 13:37:13',0);
  19. CREATE TABLE `sys_user_2fa` (
  20.   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  21.   `user_id` bigint DEFAULT NULL COMMENT '用户id',
  22.   `secret_key` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '秘钥',
  23.   `scratch_codes` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '静态验证码',
  24.   `create_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建人',
  25.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  26.   `update_by` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
  27.   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  28.   `del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标志,0:未删除;1:已删除',
  29.   PRIMARY KEY (`id`),
  30.   UNIQUE KEY `unique_user_id` (`user_id`)
  31. ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='两步验证相关临时配置表';
复制代码
之后修改配置文件中的Mysql配置信息:
  1. JDBC_MYSQL_HOST: localhost
  2. JDBC_MYSQL_PORT: 3306
  3. JDBC_MYSQL_DATABASE: 2fa_demo
  4. JDBC_MYSQL_USERNAME: root
  5. JDBC_MYSQL_PASSWORD: '123456'
复制代码
项目启动

将项目导入Intelij,运行Application,出现如下即可表示运行成功
5.png
打开链接,进入登录页面
账号密码登录

在未登录的情况下打开链接http://localhost:8024,就会进入登录页面
6.jpeg
登录账号:kdyzm
登录密码:123456
登录成功之后进入首页。
开启两步验证

登录成功之后进入首页,点击首页右上角两步验证
7.jpeg
进入两步验证页面,由于未设置过二次验证,所以会提示去设置
8.png
点击“开启二次验证”按钮,进入两步骤验证向导
9.jpeg
点击下一步输入电子邮件
10.jpeg
点击下一步,进入关键的验证器配置步骤
11.jpeg
在这一步,IOS下可以安装Authenticator或者微信小程序搜索“MFA”,使用“腾讯身份验证器”扫描二维码完成验证器设置,注意,如果多次扫描相同的二维码,需要删除上次扫描的记录
12.jpeg
点击下一步,校验配置的正确性:
13.jpeg
提示配置开启成功即表示已配置成功。
验证两步验证

退出登录,回到登录页,输入账号密码登录,登录成功后不再跳转到首页,而是跳转到二次验证页面:
14.jpeg
从手机上获取动态验证码,即可成功登录系统。
15.jpeg
关闭两步验证

进入首页后,再次进入两步验证页面,即可看到关闭按钮
16.png
关闭后再次登录系统,就不会进入两步验证页面了。
4、实战总结

由于本项目案例只关注2FA相关的内容,而光年Admin模板的静态模板只实现了登录以及2FA相关的功能,而且时间匆忙代码比较糙。。。
该项目还剩下一些问题没实现

  • 二次验证的记住设备功能
  • 8位数的静态码未实现校验功能
  • 设备丢失静态码找回功能未实现
以后有时间再补充了。

最后,欢迎关注我的博客呀:一枝梅的博客


END.

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册