SpringBoot项目登录并接入MFA二次认证
MFA多因素认证(Multi-Factor Authentication
):
一些需要身份认证的服务(如网站),为了提升安全性,通常会在账号密码登录成功后,要求用户进行第二种身份认证,以确保是正确用户登录,避免用户密码泄露了或其它原因导致用户信息泄露。
不过,用户体验就比较差,因为要登录2次嘛。
常见使用场景:
- 企业的管理后台,尤其是涉及敏感信息的系统,比如CMS客户关系管理系统、财务系统等
- 电商后台,比如亚马逊店铺管理后台,通常都要二次认证
- Git代码仓库
- 站点上用户敏感信息的查看、修改,如手机号默认隐藏中间n位,要查看必须二次认证;修改密码要二次认证等
常见的认证方式有:
- 手机短信验证码验证:把验证码通过短信发给用户,用户在该服务里(如网站)上输入该验证码并认证
- 邮箱验证码验证:把验证码通过邮件发给用户,用户输入该验证码并认证
- MFA硬件设备:给用户分配一个硬件设施,用于生成动态口令,用户输入该口令并认证
还有一种硬件设备是插入电脑即可自动认证 - MFA软件:用户在电脑或手机上安装一个软件,软件里生成动态口令,用户输入该口令并认证
- 生物识别:通过指纹、人脸识别等生物特征进行认证
- 智能卡:给用户分配一张带身份信息的卡片,用户把卡片放在服务的读卡设备上进行认证
本文只介绍网站的MFA软件接入方案,并采用手机应用进行认证。
只要是基于时间同步算法的手机应用,都可以支持,如以下应用:
- 谷歌身份验证器(google authenticator)
- 微软身份验证器(Microsoft Authenticator)
注:这种动态口令认证,通常也称之为OTP-Code(One-time Password)、OTP令牌、两步验证、二次认证、2FA等。
前端与后端的交互流程如下:
后端提供3个接口:
- 账号密码登录接口
- 该账号是否绑定过SecureKey的接口
- 二次验证码校验接口
完整交互流程图参考如下:
SpringBoot项目接入实现
假设已经创建了SpringBoot项目。
添加依赖
打开pom.xml
文件,添加如下依赖:
<!-- 用于SecureKey生成 -->
<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId>
</dependency><!-- 用于二维码生成 -->
<dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version>
</dependency>
<dependency><groupId>com.google.zxing</groupId><artifactId>javase</artifactId><version>3.5.1</version>
</dependency>
SecureKey生成与验证码生成比对类
这个是核心实现类,主要功能:
- 生成随机的SecureKey,用于外部业务绑定到指定账号,也用于后续的验证码生成
- 根据SecureKey和系统时间,生成相应的验证码
代码参考:
package beinet.cn.googleauthenticatordemo.authenticator;import org.apache.commons.codec.binary.Base32;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;public class GoogleGenerator {// 发行者(项目名),可为空,注:不允许包含冒号public static final String ISSUER = "beinet.cn";// 生成的key长度( Generate secret key length)public static final int SECRET_SIZE = 32;// Java实现随机数算法public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";// 最多可偏移的时间, 假设为2,表示计算前面2次、当前时间、后面2次,共5个时间内的验证码static int window_size = 1; // max 17static long second_per_size = 30L;// 每次时间长度,默认30秒/*** 生成一个SecretKey,外部绑定到用户** @return SecretKey*/public static String generateSecretKey() {SecureRandom sr;try {sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);sr.setSeed(getSeed());byte[] buffer = sr.generateSeed(SECRET_SIZE);Base32 codec = new Base32();byte[] bEncodedKey = codec.encode(buffer);String ret = new String(bEncodedKey);return ret.replaceAll("=+$", "");// 移除末尾的等号} catch (NoSuchAlgorithmException e) {// should never occur... configuration errorthrow new RuntimeException(e);}}/*** 生成二维码所需的字符串,注:这个format不可修改,否则会导致身份验证器无法识别二维码** @param user 绑定到的用户名* @param secret 对应的secretKey* @return 二维码字符串*/public static String getQRBarcode(String user, String secret) {if (ISSUER != null) {if (ISSUER.contains(":")) {throw new IllegalArgumentException("Issuer cannot contain the ':' character.");}user = ISSUER + ":" + user;}String format = "otpauth://totp/%s?secret=%s";String ret = String.format(format, user, secret);if (ISSUER != null) {ret += "&issuer=" + ISSUER;}return ret;}/*** 验证用户提交的code是否匹配** @param secret 用户绑定的secretKey* @param code 用户输入的code* @return 匹配成功与否*/public static boolean checkCode(String secret, int code) {Base32 codec = new Base32();byte[] decodedKey = codec.decode(secret);// convert unix msec time into a 30 second "window"// this is per the TOTP spec (see the RFC for details)long timeMsec = System.currentTimeMillis();long t = (timeMsec / 1000L) / second_per_size;// Window is used to check codes generated in the near past.// You can use this value to tune how far you're willing to go.for (int i = -window_size; i <= window_size; ++i) {int hash;try {hash = verifyCode(decodedKey, t + i);} catch (Exception e) {// Yes, this is bad form - but// the exceptions thrown would be rare and a static// configuration probleme.printStackTrace();throw new RuntimeException(e.getMessage());// return false;}System.out.println("input code=" + code + "; count hash=" + hash);if (code == hash) { // addZero(hash)return true;}
/* if (code==hash ) {return true;}*/}// The validation code is invalid.return false;}private static int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {byte[] data = new byte[8];long value = t;for (int i = 8; i-- > 0; value >>>= 8) {data[i] = (byte) value;}SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");Mac mac = Mac.getInstance("HmacSHA1");mac.init(signKey);byte[] hash = mac.doFinal(data);int offset = hash[20 - 1] & 0xF;// We're using a long because Java hasn't got unsigned int.long truncatedHash = 0;for (int i = 0; i < 4; ++i) {truncatedHash <<= 8;// We are dealing with signed bytes:// we just keep the first byte.truncatedHash |= (hash[offset + i] & 0xFF);}truncatedHash &= 0x7FFFFFFF;truncatedHash %= 1000000;return (int) truncatedHash;}private static byte[] getSeed() {String str = ISSUER + System.currentTimeMillis() + ISSUER;return str.getBytes(StandardCharsets.UTF_8);}
}
业务服务类实现
用于封装2个方法:
- 输入账号,为该账号生成并绑定SecureKey入库,同时返回谷歌身份验证器所需的二维码URL
- 输入账号和验证码,获取该账号对应的SecureKey,并计算当前时间的验证码,与输入的验证码进行比对,返回成功与否
参考实现:
package beinet.cn.googleauthenticatordemo.authenticator;import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.HashMap;
import java.util.Map;@Service
public class AuthenticatorService {private Map<String, String> userKeys = new HashMap<>();/*** 生成一个secretKey,并关联到用户,* 然后返回二维码字符串** @param username 用户名* @return 二维码字符串*/public String generateAuthUrl(String username) {String secret = GoogleGenerator.generateSecretKey();// todo: 实际项目中,用户名与secretKey的关联关系应当存储在数据库里,否则变化了,就会无法登录userKeys.put(username, secret);return GoogleGenerator.getQRBarcode(username, secret);}/*** 根据用户名和输入的code,进行校验并返回成功失败** @param username 用户名* @param code 输入的code* @return 校验成功与否*/public boolean validateCode(String username, int code) {// todo: 从数据库里读取该用户的secretKeyString secret = userKeys.get(username);if (!StringUtils.hasLength(secret)) {throw new RuntimeException("该用户未使用Google身份验证器注册,请先注册");}return GoogleGenerator.checkCode(secret, code);}
}
登录流程嵌入
这个根据实际代码进行修改,比如前后端分离的项目:
- 前端页面在登录成功时,增加代码:
- 判断当前用户是否绑定过,未绑定时,显示二维码让用户绑定
- 弹出OTPCode输入界面,二次验证成功再跳转正常业务页
- 后端业务接口,需要判断2个Cookie都存在时才进入,少一个都要返回登录失败
完整Demo代码
- 使用上面的代码实现的,带登录+二次验证的完整代码参考:
https://github.com/youbl/study/tree/master/study-codes/google-authenticator-demo
可以查看该目录的历史提交记录,了解每个步骤做了哪些动作
手机应用下载
Google Authenticator下载
使用比较简单,手机上无需登录,可直接打开。
- 苹果版本下载地址:
https://apps.apple.com/cn/app/google-authenticator/id388497605 - 安卓版本下载地址:
https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US - 国内无法访问谷歌市场的,可以去这里下载apk安装:
https://apkpure.com/cn/google-authenticator/com.google.android.apps.authenticator2
Microsoft Authenticator下载
有一定安全性,需要进行密码或指纹验证,才能打开。
- 苹果版本下载地址:
https://apps.apple.com/us/app/microsoft-authenticator/id983156458 - 安卓版本下载地址:
https://play.google.com/store/apps/details?id=com.azure.authenticator&hl=en_US - 国内无法访问谷歌市场的,可以去这里下载apk安装:
https://apkpure.com/cn/microsoft-authenticator/com.azure.authenticator
相关文章:

SpringBoot项目登录并接入MFA二次认证
MFA多因素认证(Multi-Factor Authentication ): 一些需要身份认证的服务(如网站),为了提升安全性,通常会在账号密码登录成功后,要求用户进行第二种身份认证,以确保是正确用户登录,避…...

算法与数据结构(三)
一、堆 1,堆结构就是用数组实现的完全二叉树结构 根节点的左孩子的下标为:2i1,右孩子为2i2。两个孩子的父节点为(i-1)/2向下取整 2,完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 从下往上将孩子与父节点进行比较,如果子叶…...

亚马逊云科技出海日,让数字经济出海扩展到更多行业和领域
数字化浪潮之下,中国企业的全球化步伐明显提速。从“借帆出海”到“生而全球化”,中国企业实现了从传统制造业“中国产品”出口,向创新“中国技术”和先导“中国品牌”的逐步升级。 作为全球云计算的开创者与引领者,亚马逊云科技…...

Pb协议的接口测试
【摘要】 Protocol Buffers 是谷歌开源的序列化与反序列化框架。它与语言无关、平台无关、具有可扩展的机制。用于序列化结构化数据,此工具对标 XML ,支持自动编码,解码。比 XML 性能好,且数据易于解析。更多有关工具的介绍可参考…...

2. 分布式文件系统 HDFS
2. 分布式文件系统 HDFS 1. 引入HDFS【面试点】 问题一:如果一个文件中有 10 个数值,一行一个,并且都可以用 int 来度量。现在求 10 个数值的和 思路: 逐行读取文件的内容把读取到的内容转换成 int 类型把转换后的数据进行相加…...
借助金融科技差异化发展,不一样的“破茧”手法
撰稿 | 多客 来源 | 贝多财经 民营银行的诞生顺应了普惠金融的要求,承担着支持民营经济、服务小微的历史使命。经过近年来的发展,19家民营银行形成了特色化、差异化的发展模式,并用各自本领实践普惠金融的初心。 本文从多家民营银行在核心技…...
typescript中type、interface的区别
一、概念定义 interface:接口 在TS 中主要用于定义【对象类型】,可以对【对象】的形状进行描述。type :类型别名 为类型创建一个新名称,它并不是一个类型,只是一个别名。 二,区别 interface: …...

Ingress详解
Ingress Service对集群外暴露端口两种方式,这两种方式都有一定的缺点: NodePort :会占用集群集群端口,当集群服务变多时,缺点明显LoadBalancer:每个Service都需要一个LB,并且需要k8s之外设备支…...
【递归算法的Java实现及其应用】
文章目录 递归算法概述递归算法的实现步骤递归算法的Java实现递归算法的底层工作原理递归算法的底层代码讲解(优先级高)递归算法的实际应用场景递归算法在场景中解决的问题递归算法的优点和缺点总结 递归算法概述 递归算法是一种通过调用自身来解决问题…...

2023年度第四届全国大学生算法设计与编程挑战赛(春季赛)
目录 2023年度第四届全国大学生算法设计与编程挑战赛(春季赛)1、A2、Bx3、Cut4、Diff5、EchoN6、Farmer7、GcdGame8、HouseSub9、IMissYou!10、Jargonless 2023年度第四届全国大学生算法设计与编程挑战赛(春季赛) 1、A 题目描述…...
如何用PHP获取各大电商平台的数据
PHP获取API数据是指使用PHP语言从web服务中提取数据。API是指应用程序接口,它允许应用程序之间进行交互和通信,并且允许一个应用程序从另一个应用程序获取数据。PHP是一种网站开发语言,它可以使用多种方式来获取API数据。 在PHP中࿰…...

一站式完成车牌识别任务:从模型优化到端侧部署
交通领域的应用智能化不断往纵深发展,其中最为成熟的车牌识别早已融入人们的日常生活之中,在高速公路电子收费系统、停车场等场景中随处可见。一些企业在具体业务中倾向采用开源方案降低研发成本,但现有公开的方案中少有完成端到端的车牌应用…...

Linux4.8Nginx Rewrite
文章目录 计算机系统5G云计算第六章 LINUX Nginx Rewrite一、Nginx Rewrite 概述1.常用的Nginx 正则表达式2.rewrite和location3.location4.实际网站使用中,至少有三个匹配规则定义5.rewrite6.rewrite 示例 计算机系统 5G云计算 第六章 LINUX Nginx Rewrite 一、…...

DHT11温湿度传感器
接口定义 传感器通信 DHT11采用简化的单总线通信。单总线仅有一根数据线(SDA),通信所进行的数据交换、挂在单总线上的所有设备之间进行信号交换与传递均在一条通讯线上实现。 单总线上必须有一个上拉电阻(Rp)以实现单…...
RestTemplate超简单上手
目录 1.什么是RestTemplate? 2.RestTemplate的使用 2.1spring环境下 注意1:RestTemplate中发送请求execute()和exchange()方法的区别 execute()方式 exchange()方式 二者的区别 注意2:进阶配置——底层HTTP客户端 2.2非spring环境下 1.什么是R…...
监控系统设计原则及实现目标
1.1.1.1 1 .完善的设计理念: 包括符合国际发展潮流的特性化设计,完整的安防监控及围墙周界报警系统 的布线、设备安装、调试、测试、验收的“交钥匙”工程管理制度,以及符合标 准的质量控制体系。 1.1.1.2 设计原则…...

VulnHub项目:MONEYHEIST: CATCH US IF YOU CAN
靶机名称: MONEYHEIST: CATCH US IF YOU CAN 地址:MoneyHeist: Catch Us If You Can ~ VulnHub 这个系列是一部剧改编,还是挺好看的,大家有兴趣可以去看看! 废话不多说,直接上图开始! 渗透…...
对象存储OSS简介,一分钟了解对象存储OSS
对象存储(Object Storage)是一种新兴的数据存储方式,与传统的文件系统和块存储不同,对象存储以对象为基本单位进行数据管理和存储。在对象存储中,每个对象都有唯一的标识符,并包含了数据本身以及与之相关的…...

SpringCloud_微服务基础day2(Eureka简介与依赖导入,服务注册与发现)
p6:Eureka简介与依赖导入 前面我们了解了如何对单体应用进行拆分,并且也学习了如何进行服务之间的相互调用,但是存在一个问题,就是虽然服务拆分完成,但是没有一个比较合理的管理机制,如果单纯只是这样编写,…...
#tmux# #终端# 常用工具tmux
tmux tmux是一个终端复用工具,允许用户在一个终端会话中同时管理多个终端窗口,提高了终端使用效率,尤其在服务器上进行远程管理时更加实用。在tmux中,可以创建多个终端窗口和窗格,并在这些窗口和窗格之间自由切换&…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...

针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...
深入理解 React 样式方案
React 的样式方案较多,在应用开发初期,开发者需要根据项目业务具体情况选择对应样式方案。React 样式方案主要有: 1. 内联样式 2. module css 3. css in js 4. tailwind css 这些方案中,均有各自的优势和缺点。 1. 方案优劣势 1. 内联样式: 简单直观,适合动态样式和…...