SpringBoot-集成TOTP

TOTP验证码提供了一种高效且安全的身份验证方法。它不仅减少了依赖短信或其他通信方式带来的成本和延时,还通过不断变换的密码增加了破解的难度。未来,随着技术的进步和对安全性要求的提高,TOTP及其衍生技术将继续发展并被更广泛地应用。TOTP验证码是基于时间的一次性密码算法(Time-based One-Time Password algorithm)。其核心原理是使用预共享密钥和当前时间戳生成一次性的验证码。
TOTP验证码的概念及相关分析:

定义与用途:TOTP,即基于时间的一次性密码算法,是一种利用时间同步和双方预共享的密钥来生成一次性密码的方法。这种方法主要用于双因素认证(2FA),提高账户安全性。

工作机制:在TOTP中,服务器和客户端都会预先共享一个密钥。当需要验证用户身份时,客户端会基于当前时间和该预共享密钥生成一个OTP(一次性密码)。只有知道正确的密钥和准确时间的用户才能生成正确的OTP,从而通过验证。

安全性增强:由于每次认证都使用新的密码且仅在短时间内有效,这使得TOTP比传统的静态密码更为安全。即使攻击者截获了一次密码,也因其很快就过期而无法再次使用。

性能优化:与传统的短信发送验证码相比,TOTP不需要通信费用,且响应速度更快,因为它仅依赖于时间同步而非外部通信。

广泛应用:多数现代认证系统如Google Authenticator和其他多种第三方认证应用都支持TOTP,使其成为事实上的标准之一。

TOTP验证码的原理及相关分析:

密钥预共享:服务端生成并通过安全的渠道分发一个唯一的密钥给客户端。这个密钥是后续所有操作的基础。

时间戳的使用:客户端根据当前时间和预共享的密钥计算一次性密码。这个过程通常每30秒进行一次,确保密码的新鲜性和安全性。

HMAC-SHA1算法:使用预共享密钥和当前的时间计数作为输入,通过HMAC-SHA1算法生成一串固定长度的输出值。此输出经过特定处理后被转换为较短的数位,形成最终的验证码。

服务器验证:当用户提交OTP时,服务器也会使用相同的方法和密钥计算当时的OTP应是什么,并与用户提供的值进行比较,以此判断用户的验证请求是否有效。

容错机制:考虑到客户端和服务端的时钟可能不完全同步,TOTP算法允许有一定的时间容错,通常为前后几秒的时间窗口内,这保证了合法用户的正常体验。

以下是关于TOTP验证码的普及与应用以及注意事项:

普及与应用:随着移动设备的普及和互联网安全问题的增加,TOTP作为一种安全便捷的认证方式,正在被越来越多的场景所采用,包括企业级应用、金融服务、在线教育平台等。
注意事项:虽然TOTP提高了安全性,但仍应注意保护预共享密钥的安全,避免密钥泄露导致的潜在风险。同时,应定期更新密钥,防止长时间使用同一密钥增加的风险。

二维码是,下面label和issuer随意生成,secret是服务端根据每个用户生成

otpauth://totp/{label}?secret={secret}&issuer={issuer}

写一个starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sf-framework</artifactId>
        <groupId>cn.nexteer.boot</groupId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>sf-spring-boot-starter-otp</artifactId>
    <packaging>jar</packaging>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-common</artifactId>
        </dependency>

        <!-- RPC 远程调用相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-rpc</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 业务组件 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
            <version>${revision}</version>
        </dependency>

        <!-- Spring 核心 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-web</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- Web 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-security</artifactId>
        </dependency>

        <!-- DB 相关 -->
        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-mybatis</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.nexteer.boot</groupId>
            <artifactId>sf-spring-boot-starter-redis</artifactId>
        </dependency>


        <!-- Test 测试相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 工具类相关 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>
    </dependencies>
</project>

配置类

@AutoConfiguration
public class SfOtpAutoConfiguration {

    @Bean
    public OtpAuthAspect otpAuthAspect()
    {
        return new OtpAuthAspect();
    }
}
@AutoConfiguration
@EnableFeignClients(clients = AdminUserApi.class) // 主要是引入相关的 API 服务
public class SfAdminUserRpcAutoConfiguration {
}

SPI文件

在这里插入图片描述

切面类

@Aspect
@RequiredArgsConstructor
@Slf4j
public class OtpAuthAspect {
    static final String OTP_CODE_HEADER = "X-OTP-CODE";
    static final String OPT_CODE_PARAM = "otpCode";
    @Resource
    private AdminUserApi adminUserApi;

    @Before("@annotation(otpAuth)")
    public void beforePointCut(JoinPoint joinPoint, OtpAuth otpAuth) throws Throwable {
        LoginUser loginUser = getLoginUser();
        HttpServletRequest request = getRequest();
        String otpCode = getOtpCodeByRequest(request);
        if (StrUtil.isBlank(otpCode)) {
            log.error("[around][用户({}) 请求({}) 时,未传递 OTP 验证码]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
        String secret = getKeyByLoginUserId(loginUser.getId());
        if (StrUtil.isBlank(secret)) {
            log.error("[around][用户({}) 请求({}) 时,未配置 OTP 密钥]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
        boolean result = TotpUtils.verify(secret,otpCode);
        if (!result) {
            log.error("[around][用户({}) 请求({}) 时,OTP 验证码错误]", loginUser.getId(), request.getRequestURI());
            throw new ServiceException(GlobalErrorCodeConstants.OTP_ERROR.getCode(), otpAuth.message());
        }
    }

    private String getKeyByLoginUserId(Long id) {
        CommonResult<AdminUserRespDTO> user = adminUserApi.getUser(id);
        AdminUserRespDTO checkedData = user.getCheckedData();
        return checkedData.getOtpSecret();
    }

    private String getOtpCodeByRequest(HttpServletRequest request) {
        String header = request.getHeader(OTP_CODE_HEADER);
        if (StrUtil.isNotBlank(header)) {
            return header;
        }

        String attribute = (String)request.getAttribute(OPT_CODE_PARAM);
        if (StrUtil.isNotBlank(attribute)) {
            return attribute;
        }
        String parameter = request.getParameter(OPT_CODE_PARAM);
        if (StrUtil.isNotBlank(parameter)) {
            return parameter;
        }
        return null;
    }
}

工具类

@Slf4j
public class TotpUtils {


    private static int WINDOW_SIZE = 1;

    private static long X = 30;

    private TotpUtils() {}
 
    /**
     * 该方法使用JCE提供加密算法。
     * HMAC使用加密哈希算法作为参数计算哈希消息认证码。
     * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
     *                             HmacSHA512)
     * @param keyBytes: 用于HMAC密钥的字节
     * @param text: 用于HMAC密钥的字节数
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes,
                                   byte[] text){
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey =
                    new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }
 
    /**
     * This method converts a HEX string to Byte[]
     * @param hex: the HEX string
     * @return: a byte array
     */
    private static byte[] hexStr2Bytes(String hex){
        // Adding one byte to get the right conversion Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex,16).toByteArray();
 
        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++)
            ret[i] = bArray[i+1];
        return ret;
    }
 
    private static final int[] DIGITS_POWER
            // 0 1  2   3    4     5      6       7        8
            = {1,10,100,1000,10000,100000,1000000,10000000,100000000 };
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA1");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP256(String key,
                                         String time,
                                         String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA256");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP512(String key,
                                         String time,
                                         String returnDigits){
        return generateTOTP(key, time, returnDigits, "HmacSHA512");
    }
 
    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto: the crypto function to use
     *
     * @return: a numeric String in base 10 that includes truncationDigits digits
     */
    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits,
                                      String crypto){
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;
 
        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16 )
            time = "0" + time;
 
        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
 
        byte[] hash = hmac_sha(crypto, k, msg);
 
        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;
 
        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);
 
        int otp = binary % DIGITS_POWER[codeDigits];
 
        result = Integer.toString(otp);
        while (result.length() < codeDigits) {
            result = "0" + result;
        }
        return result;
    }
 
    /**
     * 验证动态口令是否正确
     * @param secretBase32 密钥
     * @param code 待验证的动态口令
     * @return
     */
    public static boolean verify(String secretBase32, String code){
        String secretHex = HexUtil.encodeHexStr(Base32Codec.Base32Decoder.DECODER.decode(secretBase32));
        long t = System.currentTimeMillis() / 1000L / X;
        
        for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
            String steps = Long.toHexString(t).toUpperCase();
            while (steps.length() < 16) steps = "0" + steps;

            String totp = generateTOTP(secretHex, steps, "6",
                    "HmacSHA1");
            if (code.equals(totp)) {
                return true;
            }
        }
        return false;
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT安装及项目创建

一、QT安装 1、安装qt_creater 方法一&#xff1a; 镜像文件&#xff1a;在2024-6-12&#xff1a;版本已经更新到了6.7 下载地址&#xff1a;https://download.qt.io/archive/qt/ 方法二&#xff1a; 百度网盘&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1D0EmH…

SpringSecurity入门(一)

1、引入依赖 spring-boot版本2.7.3&#xff0c;如未特殊说明版本默认使用此版本 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><g…

【Linux】基础IO [万字之作]

目录 一.重谈文件 二.重谈C文件操作 1.操作 1.文件的打开和关闭 2.文件的读写操作 ​编辑 1.fgetc函数 2.fputc函数 3.fputs函数 4.fgets函数 5.fprintf函数 6.fscanf函数 7.fread函数 8.fwrite函数 三.重谈当前路径 四.系统文件操作接口 1.Open函数 2.write函数 3…

hot100 -- 栈

目录 &#x1f6a9;有效的括号 &#x1f33c;最小栈 AC 栈 AC 链表 &#x1f33c;字符串解码 &#x1f43b;每日温度 &#x1f352;柱状图中的最大矩形 解释 AC 单调栈 &#x1f6a9;有效的括号 20. 有效的括号 - 力扣&#xff08;LeetCode&#xff09; 1&#xf…

[初阶数据结构] 包装类 | 泛型

目录 一. 包装类 1.1 什么是包装类? 1.2 包装类的意义 1.3 基本数据类型与包装类 1.4 装箱 1.5 拆箱 1.6 小总结 二. 泛型 2.1 什么是泛型? 2.2 泛型的意义 2.3 泛型的语法 2.4 泛型的编译 2.4.1 下载插件 2.4.2 分析 2.5 上界 2.6 泛型方法 2.7 小总结 三. 总结 一.…

conda虚拟环境,安装pytorch cuda cudnn版本一致,最简单方式

1、pytorch版本安装&#xff08;卸载也会有问题&#xff09; &#xff08;1&#xff09;版本如何选择参考和卸载 https://zhuanlan.zhihu.com/p/401931724 &#xff08;2&#xff09;对应版本如何安装命令 https://pytorch.org/get-started/previous-versions/ 最简答安装参考…

递推算法及相关问题详解

目录 递推的概念 训练&#xff1a;斐波那契数列 解析 参考代码 训练&#xff1a;上台阶 参考代码 训练&#xff1a;信封 解析 参考代码 递推的概念 递推是一种处理问题的重要方法。 递推通过对问题的分析&#xff0c;找到问题相邻项之间的关系&#xff08;递推式&a…

实验滤膜等分切割器八等分90mm

名称:滤膜切分器 型号: RNKF-90 适用范围:切分φ90mm玻璃纤维滤膜、石英纤维滤膜 等分数:2等分、4等分、8等分 使用方法: 1、开盖:逆时针旋转防尘盖&#xff0c;与切分台分开后&#xff0c;轻放于台面。 2、放膜:持专用镊子,镊子的长尖在下,短尖在上,取待切分滤膜1片,采样…

配置响应拦截器,全局前置导航守卫

1&#xff1a;配置响应拦截器 响应拦截器&#xff0c;统一处理接口的错误 问题&#xff1a;每次请求&#xff0c;都会有可能会错误&#xff0c;就都需要错误提示 说明&#xff1a;响应拦截器是咱们拿到数据的 第一个 数据流转站&#xff0c;可以在里面统一处理错误。 // 添…

uniapp小程序计算地图计算距离

我们拿到自身和目标距离经纬度 调用此方法即可计算出自身与目标的距离 最后我所展示的页面如下 具体效果可能会有点偏差 要求严格的可以在精细的计算一下

ant组件库日期选择器汉化

ant组件库日期选择器默认英文 如何汉化 跟着官网走不能完全实现汉化。 这里提供一个解决方案&#xff0c;首先&#xff0c;通过pnpm下载moment包。 然后引入和注册文件&#xff1a; import zhCN from ant-design-vue/es/locale/zh_CN;import moment from moment;moment.loca…

vue30:v-model语法糖的本质

在Vue.js框架中&#xff0c;v-model 是一个指令&#xff0c;用于在表单输入和应用状态之间创建双向数据绑定。它本质上是语法糖&#xff0c;意味着它提供了一种更简洁的方式来编写代码&#xff0c;而不需要显式地编写额外的代码。 具体来说&#xff0c;v-model 背后实际上是由…

外汇天眼:Equals集团发布战略评估通知:MDP不再考虑收购提议

Equals Group plc (LON)今天发布了一份关于其战略评估的通知。 Equals公司不再与Madison Dearborn Partners, LLC (MDP)就公司的收购提议进行讨论。MDP因此发布了一份声明&#xff0c;确认其不打算为公司提出收购提议。 然而&#xff0c;MDP与其投资组合公司MoneyGram Interna…

台式电脑怎么连WiFi?4个宝藏方法收藏好!

“我有一部台式电脑&#xff0c;现在不知道应该怎么操作才能让电脑正确连接WiFi&#xff0c;不知道大家有什么简单的连接方法吗&#xff1f;希望可以给我出出主意。” 随着无线网络的普及和科技的飞速发展&#xff0c;越来越多人选择使用WiFi来连接互联网。对于笔记本电脑和移动…

计算机网络(3) 字节顺序:网络字节序与IPv4

一.小端与大端 小端&#xff08;Little endian&#xff09;&#xff1a;低字节保存在内存低地址&#xff0c;高字节保存在内存高地址。 大端&#xff08;Big endian&#xff09;&#xff1a;低字节保存在内存高地址&#xff0c;高字节保存在内存低地址。 例如&#xff08;14…

Android 中USB-HID协议实现

前言 所有通过USB连接android设备进行通讯的步骤都是大同小异&#xff1a;查询usb设备列表 ——>匹配对应的设备类型&#xff08;如productid , vendorId&#xff09;等——>连接usb设备&#xff0c;找到连接通讯的节点——>配置通讯信息&#xff0c;进行通讯。以上是…

Java数据结构之ArrayList(如果想知道Java中有关ArrayList的知识点,那么只看这一篇就足够了!)

前言&#xff1a;ArrayList是Java中最常用的动态数组实现之一&#xff0c;它提供了便捷的操作接口和灵活的扩展能力&#xff0c;使得在处理动态数据集合时非常方便。本文将深入探讨Java中ArrayList的实现原理、常用操作以及一些使用场景。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨…

鸿蒙开发:通过startAbilityByType拉起垂类应用

通过startAbilityByType拉起垂类应用 使用场景 开发者可通过特定的业务类型如导航、金融等&#xff0c;调用startAbilityByType接口拉起对应的垂域面板&#xff0c;该面板将展示目标方接入的垂域应用&#xff0c;由用户选择打开指定应用以实现相应的垂类意图。垂域面板为调用…

Linux网络编程(二)Socket编程

Socket编程 一、网络套接字概念&#xff1a;socket 一个文件描述符指向一个套接字&#xff08;该套接字内部由内核借助两个缓冲区实现。&#xff09;在通信过程中&#xff0c; 套接字一定是成对出现的。二、网络字节序和主机字节序的转换函数&#xff08;ip和端口&#xff09…

代码随想录算法训练营第二十一天|530.二叉搜索树的最小绝对差、501.二叉搜索树中的众数、236. 二叉树的最近公共祖先

530.二叉搜索树的最小绝对差 题目链接&#xff1a;530.二叉搜索树的最小绝对差 文档讲解&#xff1a;代码随想录 状态&#xff1a;还可以 思路&#xff1a;使用中序遍历来遍历二叉搜索树。在中序遍历过程中&#xff0c;比较当前节点和前驱节点的值&#xff0c;更新最小差值。返…