SpringBoot项目登录并接入MFA二次认证

news2024/11/24 4:37:24

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 17
    static 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 error
            throw 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 problem
                e.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: 从数据库里读取该用户的secretKey
        String 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/609304.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】C预处理器(宏、文件包含、条件编译...)

一、C语言编译的预处理阶段1.1 C语言的编译过程1.2 C语言编译的预处理 二、C语言 宏2.1替换常量2.2函数宏2.3 字符串化和连接&#xff1a;#和##2.4 变参宏 三、文件包含&#xff1a;#include3.1 写法3.2 头文件的作用——声明3.3 头文件和extern 、static 四、 其他指令4.1 #un…

Ansible基础4——变量、机密、事实

文章目录 一、变量二、机密2.1 创建加密文件2.2 查看加密文件2.3 编辑加密文件内容2.4 加密现有文件2.5 解密文件2.6 更改加密密码 三、事实3.1 收集展示事实3.2 展示某个结果3.3 新旧事实命令3.4 关闭事实3.5 魔法变量 一、变量 常设置的变量&#xff1a; 要创建的用户要安装的…

【C++ 基础篇:19】:类的构造函数与初始化列表:用法说明及构造函数的细节内容补充!

本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大特性等&#…

Kubernetes_容器网络_循序渐进地学习kubernetes网络

文章目录 前言一、Linux网络命名空间1.1 linux网络命名空间1.2 不同网络命名空间的通信两个网络命名空间通信多个网络命名空间通信 二、K8S Pod网络通信2.1 Pod内部容器的网络通信2.2 相同node: 不同pod间的网络通信2.3 不同node: 不同pod间的网络通信2.4 容器网络插件: Flanne…

C++STL库之map

文章目录 关于仿函数stackdeque&#xff08;双端对列&#xff09;queuepriority_queuemap(重点)set(去重) 关于仿函数 //C不能重载的运算符sizeof、 ::、 ? :、 .、 *、 class Add { public:int operator()(int a, int b)const{return a b;} }; //函数对象&#xff0c;仿函数…

EDA数字钟(三)

文章目录 前言一、设计内容二、模块结构三、代码编写1、顶层模块Digclk2、状态控制模块Ctrl3、按键消抖模块Filter4、计时模块Time5、闹钟模块Alarm6、显示模块Display7、数码管驱动模块Smg 四、测试文件五、波形仿真总结 前言 再次编写数字钟Verilog程序&#xff0c;使其符合…

数据迁移工具,用这8种就够了

前言 最近由于工作需要需要进行数据迁移&#xff0c;那么ETL数据迁移工具该用哪些呢&#xff1f; ETL(是Extract-Transform-Load的缩写&#xff0c;即数据抽取、转换、装载的过程)&#xff0c;对于企业应用来说&#xff0c;我们经常会遇到各种数据的处理、转换、迁移的场景。…

50 Projects 50 Days - Split Landing Page 学习记录

项目地址 Split Landing Page 展示效果 Split Landing Page 实现思路 当鼠标移动到左右两块区域时&#xff0c;分别给容器添加不同的class实现样式的变换。 有两种思路可以实现&#xff0c;一种是hover时改变宽度&#xff0c;一种是hover时改变flex拉伸比例&#xff0c;两…

从零手写操作系统之RVOS外设中断实现-04

从零手写操作系统之RVOS外设中断实现-04 RISC-V 中断&#xff08;Interrupt&#xff09;的分类RISC-V Trap &#xff08;中断&#xff09;处理中涉及的寄存器寄存器 mie、mip中断处理流程PLIC 介绍外部中断&#xff08;external interrupt &#xff09;PLICPLIC Interrupt Sour…

精调万分(Fine tune SAM)-万分预测器的解读和精调之一

缘起 分割万物(segment-anything model, SAM, 万分), 是图像分割领域的革命, 图像分割从此进入大模型时代. 如何自定义这个大模型以为己用? 或者说, 通过精调取长补短用于自己的项目?这是一个值得研究的问题, 在这里我试着探索一下, 万分在医学影像学里面的脊柱分割的应用. …

【sentinel】滑动时间窗口算法在Sentinel中的应用

固定窗口算法&#xff08;计数器法&#xff09; 算法介绍 计数器法是限流算法里最简单也是最容易实现的一种算法。比如我们规定&#xff0c;对于A接口来说&#xff0c;我们1秒的访问次数不能超过10次。那么我们可以这么做&#xff1a;在一开始的时候&#xff0c;我们可以设置…

ESP-BOX官方例程实践

1.下载esp-box项目代码 github仓库&#xff1a;https://github.com/espressif/esp-box gitee仓库&#xff1a;https://gitee.com/EspressifSystems/esp-box 使用git工具和如下命令进行下载&#xff1a; git clone --recursive https://github.com/espressif/esp-box.git or gi…

【C++ 基础篇:21】:friend 友元四连问:什么是友元?友元类?友元函数?什么时候用友元?

本系列 C 相关文章 仅为笔者学习笔记记录&#xff0c;用自己的理解记录学习&#xff01;C 学习系列将分为三个阶段&#xff1a;基础篇、STL 篇、高阶数据结构与算法篇&#xff0c;相关重点内容如下&#xff1a; 基础篇&#xff1a;类与对象&#xff08;涉及C的三大特性等&#…

S7-200 PLC的CPU模块介绍

更多关于西门子S7-200PLC内容查看&#xff1a;西门子200系列PLC学习课程大纲(课程筹备中) 1.什么是西门子200PLC的CPU? 如下图1-1所示&#xff0c;S7-200 PLC CUP是将一个微处理器&#xff0c;一个集成电源&#xff0c;一定的数字量或模拟量I/O&#xff0c;一定的通信接口等…

【Linux】—— git的管理以及使用

前言&#xff1a; 在上篇我们已经学习了关于调试器gdb的相关知识&#xff0c;本期我将为大家介绍的是关于版本控制工具——git的使用教程&#xff01;&#xff01;&#xff01; 目录 前言 &#xff08;一&#xff09;git的历史介绍 &#xff08;二&#xff09;github和gite…

Unity异步编程【6】——Unity中的UniTask如何取消指定的任务或所有的任务

今天儿童节&#xff0c;犬子已经9个多月了&#xff0c;今天是他的第一个儿童节。中年得子&#xff0c;其乐无穷&#xff08;音&#xff1a;ku bu kan yan&#xff09;…回头是岸啊 〇、 示例效果 一连创建5个异步任务[id 从0~4]&#xff0c;先停止其中的第id 4的任务&#x…

Flutter进阶篇-布局(Layout)原理

1、约束、尺寸、位置 overrideWidget build(BuildContext context) {return Scaffold(body: LayoutBuilder(builder: (context, constraints) {print("body约束:" constraints.toString());return Container(color: Colors.black,width: 300,height: 300,child: L…

【企业化架构部署】基于Nginx搭建LNMP架构

文章目录 一、安装 MySQL 数据库1. 安装Mysql环境依赖包2. 创建运行用户3. 编译安装4. 修改mysql 配置文件5. 更改mysql安装目录和配置文件的属主属组6. 设置路径环境变量7. 初始化数据库8. 添加mysqld系统服务9. 修改mysql 的登录密码10. 授权远程登录 二、编译安装 nginx 服务…

Maven 工具

Maven 工具 Maven简介Maven 基础概念创建 Maven项目依赖配置生命周期与插件分模块开发聚合和继承聚合继承聚合与继承的区别 属性版本管理多环境配置与应用私服 Maven简介 Maven 本质是一个项目管理工具&#xff0c;将项目开发和管理过程抽象成一个项目对象模型&#xff08;POM…

【爬虫】3.4爬取网站复杂数据

1. Web服务器网站 进一步把前面的Web网站的mysql.html, python.html, java.html丰富其中 的内容&#xff0c;并加上图形&#xff1a; mysql.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>my…