一、双因素认证的概念

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

2FA认证流程如下:

  1. 用户登录应用程序。
  2. 用户输入登录凭证,通常是账号和密码,做初始身份验证。
  3. 验证成功后,提示用户提交第二个身份验证因子。
  4. 用户将第二个身份验证因子输入至应用程序,如果第二个身份验证因子通过,用户将通过身份验证并被授予对应的系统操作权限。

举个简单的例子,我们使用账号密码登录微博、豆瓣等应用时,命名账号密码都对了,但是还要输入手机验证码二次验证以确保安全性,这就是双因素验证。

虽然短信验证码实现简单,但是在实际场景中,一般会使用其它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>
<artifactId>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>
<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

/**
* 测试获取秘钥
*/
@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相关的功能,而且时间匆忙代码比较糙。。。

该项目还剩下一些问题没实现

  1. 二次验证的记住设备功能
  2. 8位数的静态码未实现校验功能
  3. 设备丢失静态码找回功能未实现

以后有时间再补充了。

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

END.

SpringBoot集成TOTP双因素认证(2FA)实战的更多相关文章

  1. ASP.NET Core & 双因素验证2FA 实战经验分享

    必读 本文源码核心逻辑使用AspNetCore.Totp,为什么不使用AspNetCore.Totp而是使用源码封装后面将会说明. 为了防止不提供原网址的转载,特在这里加上原文链接: https:// ...

  2. 业余草双因素认证(2FA)教程

    所谓认证(authentication)就是确认用户的身份,是网站登录必不可少的步骤.密码是最常见的认证方法,但是不安全,容易泄露和冒充.越来越多的地方,要求启用双因素认证(Two-factor au ...

  3. 双因素认证(2FA)教程

    所谓认证(authentication)就是确认用户的身份,是网站登录必不可少的步骤. 密码是最常见的认证方法,但是不安全,容易泄露和冒充. 越来越多的地方,要求启用 双因素认证(Two-factor ...

  4. 用Abp实现双因素认证(Two-Factor Authentication, 2FA)登录(一):认证模块

    @ 目录 原理 用户验证码校验模块 双因素认证模块 改写登录 在之前的博文 用Abp实现短信验证码免密登录(一):短信校验模块 一文中,我们实现了用户验证码校验模块,今天来拓展这个模块,使Abp用户系 ...

  5. Linux 利用Google Authenticator实现ssh登录双因素认证

    1.介绍 双因素认证:双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到一起才能发挥作用的身份认证系统.双因素认证是一种采用时间同步技术的系统,采用了基于时间.事件和密钥三变量而产生的一 ...

  6. Linux 之 利用Google Authenticator实现用户双因素认证

    一.介绍:什么是双因素认证 双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到一起才能发挥作用的身份认证系统.双因素认证是一种采用时间同步技术的系统,采用了基于时间.事件和密钥三变量而产 ...

  7. springboot集成shiro实现权限认证

    github:https://github.com/peterowang/shiro 基于上一篇:springboot集成shiro实现身份认证 1.加入UserController package ...

  8. 轻松搭建CAS 5.x系列(8)-在CAS Server增加双因素认证(DUO版)

    概述说明 为了让系统更加安全,很多登录会加入双因素认证.何为双因素,如果把登陆作为开一扇门的话,那就是在原来的锁上再加一把锁,第二锁用新的钥匙,这样安全系数就更加高了. CAS是通过账号名和密码来认证 ...

  9. java 双因素认证(2FA)TOTP demo

    TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password).它是公认的可靠解决方案,已经写入国际标准 RFC6238. 很早就知道有这个 ...

  10. springboot集成shiro实现身份认证

    github地址:https://github.com/peterowang/shiro pom文件 <dependencies> <dependency> <group ...

随机推荐

  1. IvorySQL 升级指南:从 3.x 到 4.0 的平滑过渡

    日前,IvorySQL 4.0 重磅发布,全面支持 PostgreSQL 17,并且增强了对 Oracle 的兼容性.关于 IvorySQL 4.0 的介绍,各位小伙伴可以通过这篇文章回顾:Ivory ...

  2. 万字长文详解SIFT特征提取

    本文对 SIFT 算法进行了详细梳理.SIFT即尺度不变特征变换(Scale-Invariant Feature Transform),是一种用于检测和描述图像局部特征的算法.该算法对图像的尺度和旋转 ...

  3. 【Git】Git GUI的使用

    在Git命令行一文中已经对Git的操作命令进行了简单的介绍,但是理论知识过于枯燥,使得初学者在Git的使用上还是会有很大的困难.虽然我更推荐使用Git Bash方式对Git进行操作,但是对于大部分只是 ...

  4. 举个栗子之gorpc - 消息的编码和解码

    2022年的第一个rpc,比以往来的更早一些... 留杭过年...写点东西 初始化项目gorpc 借助go module我们可以轻易创建一个新的项目 mkdir gorpc go mod init g ...

  5. wpf关于设备无关性的理解

    wpf的像素单位是1/96*系统dpi.当前系统dpi是96,那么wpf的一个单位长就是1px像素.这个系统dpi的意思就是物理单位一英寸里有多少个像素点,比如windows标准的96dpi,意味着一 ...

  6. 垂直模型和AI Workflow是开AGI的历史倒车

    提供AI咨询+AI项目陪跑服务,有需要回复1 RL 之父 Rich Sutton在 2019 年的文章<苦涩的教训>中指出: 70 年的 AI 研究历史告诉我们一个最重要的道理:依靠纯粹算 ...

  7. 从excle中读取数据的方法

    倒入两个库:ExcelLibrary,Collections 首先,必须注意文件格式为xls 表格内容 open_Excel    C:\\Users\\Beckham\\Desktop\\a.xls ...

  8. CF1573B题解

    题意: 对于给定的序列 aA1,aA2,-,aAna_{A1},a_{A2},-,a_{An}aA1​,aA2​,-,aAn​.bB1,bB2,-,bBnb_{B1},b_{B2},-,b_{Bn}b ...

  9. Vue ElementUI 树表格

    树表格做懒加载-点击小箭头走接口 children为[]则使用hasChildren的true/false来判断是否有子节点,另,如果要做点击小箭头走接口必须加lazy及load <el-tab ...

  10. MySQL开启general_log

    General_log 详解 1.介绍 开启 general log 将所有到达MySQL Server的SQL语句记录下来. 一般不会开启开功能,因为log的量会非常庞大.但个别情况下可能会临时的开 ...