Java实现OTP(动态口令)服务

 更新时间:2025年03月10日 11:00:22   作者:nbsaas-boot  
OTP是一种动态生成的短时有效密码,用于身份验证,通常在登录或执行敏感操作时提供额外的安全保障,本文主要介绍了Java实现OTP(动态口令)服务,感兴趣的可以了解一下

什么是 OTP?

OTP 的全称是 One-Time Password,中文常称为“一次性密码”或“动态口令”。它是一种动态生成的短时有效密码,用于身份验证,通常在登录或执行敏感操作时提供额外的安全保障。OTP 广泛应用于 Google、微软、GitHub 等主流平台,以增强用户账户的安全性。

OTP 的特点包括:

  • 一次性使用:每个密码只能使用一次,无法重复。
  • 时效性:密码在短时间内有效,过期后无法使用。
  • 动态生成:密码基于时间或计数器动态生成。

OTP 的生成原理

常见的 OTP 实现标准有两种:

  • HOTP(HMAC-Based One-Time Password):基于计数器的 OTP。

  • TOTP(Time-Based One-Time Password):基于时间的 OTP。

TOTP 是目前使用最广泛的标准。它以共享密钥(secret key)和当前时间为输入,结合 HMAC-SHA1 算法生成短数字密码。以下是 TOTP 的主要步骤:

  • 将当前时间戳除以时间步长(例如 30 秒)得到时间索引。

  • 使用 HMAC 算法计算时间索引的哈希值。

  • 提取哈希值的动态偏移量,并生成一个 6 位或 8 位的数字密码。

用 Java 实现 OTP 服务

以下是一个基于 Java 的 OTP 服务实现,支持生成和验证 OTP。

引入依赖

在项目的 pom.xml 文件中添加以下依赖:

<dependencies>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.15</version>
    </dependency>
</dependencies>

实现 OTP 生成逻辑

以下代码实现了基于时间的 TOTP 生成和验证功能:

import java.nio.ByteBuffer;
import java.time.Instant;
import org.apache.commons.codec.binary.Base32;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class OtpService {

    private static final int TIME_STEP = 30; // 时间步长(秒)
    private static final int OTP_LENGTH = 6; // OTP 长度
    private static final int MAX_ATTEMPTS = 5; // 最大尝试次数
    private static final long BLOCK_DURATION = 300_000; // 封锁时间(毫秒)

    private final ConcurrentHashMap<String, AtomicInteger> attemptCounter = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Long> blockedUsers = new ConcurrentHashMap<>();

    // 生成 TOTP
    public String generateTOTP(String secret) throws Exception {
        long timeIndex = Instant.now().getEpochSecond() / TIME_STEP;
        return generateOtp(secret, timeIndex);
    }

    // 验证 TOTP
    public boolean validateTOTP(String secret, String otp, String userId) throws Exception {
        if (isBlocked(userId)) {
            System.out.println("User is temporarily blocked: " + userId);
            return false;
        }

        long timeIndex = Instant.now().getEpochSecond() / TIME_STEP;

        // 在验证窗口内检查 OTP
        for (int i = -1; i <= 1; i++) {
            String generatedOtp = generateOtp(secret, timeIndex + i);
            if (generatedOtp.equals(otp)) {
                resetAttempts(userId);
                return true;
            }
        }

        recordFailedAttempt(userId);
        return false;
    }

    private String generateOtp(String secret, long timeIndex) throws Exception {
        // 解码 Base32 密钥
        Base32 base32 = new Base32();
        byte[] keyBytes = base32.decode(secret);

        // 转换时间索引为字节数组
        byte[] timeBytes = ByteBuffer.allocate(8).putLong(timeIndex).array();

        // 使用 HMAC-SHA1 生成哈希
        Mac mac = Mac.getInstance("HmacSHA1");
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA1");
        mac.init(keySpec);
        byte[] hash = mac.doFinal(timeBytes);

        // 提取动态偏移量
        int offset = hash[hash.length - 1] & 0x0F;
        int binary = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16)
                | ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF);

        // 生成 OTP
        int otp = binary % (int) Math.pow(10, OTP_LENGTH);
        return String.format("%0" + OTP_LENGTH + "d", otp);
    }

    private void recordFailedAttempt(String userId) {
        attemptCounter.putIfAbsent(userId, new AtomicInteger(0));
        int attempts = attemptCounter.get(userId).incrementAndGet();

        if (attempts >= MAX_ATTEMPTS) {
            blockedUsers.put(userId, System.currentTimeMillis());
            System.out.println("User blocked due to multiple failed attempts: " + userId);
        }
    }

    private void resetAttempts(String userId) {
        attemptCounter.remove(userId);
        blockedUsers.remove(userId);
    }

    private boolean isBlocked(String userId) {
        Long blockTime = blockedUsers.get(userId);
        if (blockTime == null) {
            return false;
        }

        if (System.currentTimeMillis() - blockTime > BLOCK_DURATION) {
            blockedUsers.remove(userId);
            return false;
        }
        return true;
    }
}

测试服务

以下是使用上述 OTP 服务生成和验证 OTP 的示例:

public class OtpServiceTest {
    public static void main(String[] args) throws Exception {
        OtpService otpService = new OtpService();

        // 使用 Base32 编码密钥
        String secret = "JBSWY3DPEHPK3PXP";
        String userId = "user123";

        // 生成 OTP
        String otp = otpService.generateTOTP(secret);
        System.out.println("Generated OTP: " + otp);

        // 尝试验证 OTP
        for (int i = 0; i < 7; i++) {
            boolean isValid = otpService.validateTOTP(secret, otp, userId);
            System.out.println("Attempt " + (i + 1) + ": Is OTP valid: " + isValid);
        }
    }
}

优化建议

安全性

  • 使用安全随机数生成器生成共享密钥。

  • 通过 HTTPS 传输数据,防止中间人攻击。

时间同步

  • 客户端和服务器之间的时间必须同步,否则 OTP 验证可能失败。

防止 暴 力 破 解

  • 增加失败尝试次数限制和封锁机制。

生产环境

  • 在数据库中安全存储共享密钥,避免泄露。

  • 实现速率限制以防止暴 力 破 解攻击。

本文介绍了 OTP 的基本原理,并通过 Java 实现了一个简单的 OTP 服务,能够兼容 Google Authenticator 和 Microsoft Authenticator 等主流应用。OTP 技术通过动态密码的方式为用户提供了额外的身份验证安全保障,是目前最可靠的双因素认证技术之一。

到此这篇关于Java实现OTP(动态口令)服务的文章就介绍到这了,更多相关Java OTP动态口令内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java中字符串与日期的转换实例

    java中字符串与日期的转换实例

    java中字符串与日期的转换实例,需要的朋友可以参考一下
    2013-05-05
  • C++内存管理看这一篇就够了

    C++内存管理看这一篇就够了

    这篇文章主要介绍了C/C++中的内存管理小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-08-08
  • 深入了解MyBatis参数

    深入了解MyBatis参数

    今天小编就为大家分享一篇关于深入了解MyBatis参数,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • 详解Java字节码编程之非常好用的javassist

    详解Java字节码编程之非常好用的javassist

    这篇文章主要介绍了详解Java字节码编程之非常好用的javassist,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • 基于restTemplate遇到的编码问题及解决

    基于restTemplate遇到的编码问题及解决

    这篇文章主要介绍了restTemplate遇到的编码问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • 详解Spring Boot 目录文件结构

    详解Spring Boot 目录文件结构

    这篇文章主要介绍了Spring Boot 目录文件结构的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • 如何将默认的maven仓库改为阿里的maven仓库

    如何将默认的maven仓库改为阿里的maven仓库

    这篇文章主要介绍了如何将默认的maven仓库改为阿里的maven仓库,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • elasticsearch元数据构建metadata及routing类源码分析

    elasticsearch元数据构建metadata及routing类源码分析

    这篇文章主要为大家介绍了elasticsearch元数据构建metadata routing类内部源码分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-04-04
  • MybatisPlus使用排序查询时将null值放到最后

    MybatisPlus使用排序查询时将null值放到最后

    按照更新时间排序,但是更新时间可能为null,因此将null的数据放到最后,本文主要介绍了MybatisPlus使用排序查询时将null值放到最后,具有一定的参考价值,感兴趣的可以了解一下
    2023-08-08
  • 浅谈java Collection中的排序问题

    浅谈java Collection中的排序问题

    下面小编就为大家带来一篇浅谈java Collection中的排序问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-12-12

最新评论