Java实现TOTP动态口令验证

动态口令使用场景

  • 服务器登录动态口令验证
  • WEB应用密码登录二次验证
  • 银行转账动态口令
package org.totp.commons.util;

import org.apache.commons.codec.binary.Base32;
import org.apache.commons.lang3.RandomStringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.UUID;

public class TotpUtils {

    /** 时间步长,动态口令变化时间周期(单位秒) */
    private static final int TIME_STEP = 30;
    /** 动态口令默认长度 */
    private static final int CODE_DIGITS = 6;

    /**
     * 生成唯一密钥
     *
     * @return
     */
    public static String generateSecretKey() {
        // UUID + 4位随机字符生成唯一标识
        String uniqueId = UUID.randomUUID() + RandomStringUtils.randomAlphabetic(4);
        return new String(new Base32().encode(uniqueId.getBytes()));
    }

    /**
     * 生成一个基于TOTP标准身份验证器识别的字符串
     * 将该字符串生成二维码可供通用动态密码工具识别,例如:iOS应用(Authy)、微信小程序(二次验证码)
     *
     * @param user
     * @param secret
     * @return
     */
    public static String getQRCodeStr(String user, String secret) {
        String format = "otpauth://totp/%s?secret=%s";
        return String.format(format, user, secret);
    }

    /**
     * 生成动态口令
     *
     * @param secret
     * @return
     */
    public static String generateTOTP(String secret) {
        return TotpUtils.generateTOTP(secret, TotpUtils.getCurrentInterval(), CODE_DIGITS);
    }

    /**
     * 生成指定位数的动态口令
     *
     * @param secret
     * @param codeDigits
     * @return
     */
    public static String generateTOTP(String secret, int codeDigits) {
        return TotpUtils.generateTOTP(secret, TotpUtils.getCurrentInterval(), codeDigits);
    }

    /**
     * 验证动态口令
     *
     * @param secret
     * @param code
     * @return
     */
    public static boolean verify(String secret, String code) {
        return TotpUtils.verify(secret, code, CODE_DIGITS);
    }

    /**
     * 验证动态口令
     *
     * @param secret
     * @param code
     * @param codeDigits
     * @return
     */
    public static boolean verify(String secret, String code, int codeDigits) {
        long currentInterval = TotpUtils.getCurrentInterval();
        // 考虑到时间延时,需考虑前一个步长的动态密码是否匹配
        for (int i = 0; i <= 1; i++) {
            String tmpCode = TotpUtils.generateTOTP(secret, currentInterval - i, codeDigits);
            if (tmpCode.equals(code)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取动态口令剩余秒数
     * <p>
     * 所有口令是基于时间戳计算,因此任何动态口令的剩余有效时间都是一致的
     *
     * @return
     */
    public static int getRemainingSeconds() {
        return TIME_STEP - (int) (System.currentTimeMillis() / 1000 % TIME_STEP);
    }

    /**
     * 生成动态口令
     *
     * @param secret
     * @param currentInterval
     * @param codeDigits
     * @return
     */
    private static String generateTOTP(String secret, long currentInterval, int codeDigits) {
        if (codeDigits < 1 || codeDigits > 18) {
            throw new UnsupportedOperationException("不支持" + codeDigits + "位数的动态口令");
        }
        byte[] content = ByteBuffer.allocate(8).putLong(currentInterval).array();
        byte[] hash = TotpUtils.hmacsha(content, secret);
        // 获取hash最后一个字节的低4位,作为选择结果的开始下标偏移
        int offset = hash[hash.length - 1] & 0xf;
        // 获取4个字节组成一个整数,其中第一个字节最高位为符号位,不获取,使用0x7f
        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);
        // 如果所需位数为6,则该值为1000000
        long digitsPower = Long.parseLong(TotpUtils.rightPadding("1", codeDigits + 1));
        // 获取当前数值后的指定位数
        long code = binary % digitsPower;
        // 将数字转成字符串,不够指定位前面补0
        return TotpUtils.leftPadding(Long.toString(code), codeDigits);
    }

    /**
     * 获取当前时间戳
     *
     * @return
     */
    private static long getCurrentInterval() {
        return System.currentTimeMillis() / 1000 / TIME_STEP;
    }

    /**
     * 向左补足0
     *
     * @param value
     * @param length
     * @return
     */
    private static String leftPadding(String value, int length) {
        while (value.length() < length) {
            value = "0" + value;
        }
        return value;
    }

    /**
     * 向右补足0
     *
     * @param value
     * @param length
     * @return
     */
    private static String rightPadding(String value, int length) {
        while (value.length() < length) {
            value = value + "0";
        }
        return value;
    }

    /**
     * 使用HmacSHA1加密
     *
     * @param content
     * @param key
     * @return
     */
    private static byte[] hmacsha(byte[] content, String key) {
        try {
            byte[] byteKey = new Base32().decode(key);
            Mac hmac = Mac.getInstance("HmacSHA1");
            SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA1");
            hmac.init(keySpec);
            return hmac.doFinal(content);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
动态口令认证是一种基于时间同步技术的认证方式,常用于网络身份认证、支付等场景。在Java中,我们可以通过以下步骤实现动态口令认证: 1. 获取当前时间 使用Java中的System.currentTimeMillis()方法获取当前时间戳,单位为毫秒。 2. 生成随机密钥 使用Java中的SecureRandom类生成一个随机密钥,长度为6-8位。 3. 计算动态口令 将当前时间戳除以一个固定的时间间隔(通常为30秒或60秒),得到的结果称为时间步长。使用Java中的HMAC-SHA1算法,将时间步长与随机密钥进行哈希运算,得到一个20字节的哈希值。根据RFC4226标准,从哈希值中取出4个字节作为动态口令。 4. 验证动态口令 在服务端,使用相同的算法和密钥生成动态口令,与用户输入的口令进行比较。如果相同,则认证通过。 下面是一个简单的Java代码示例: ```java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class TOTP { // 时间步长,单位为秒 private static final int TIME_STEP = 30; // 动态口令长度 private static final int TOKEN_LENGTH = 6; // 哈希算法 private static final String HASH_ALGORITHM = "HmacSHA1"; // 随机密钥长度 private static final int SECRET_LENGTH = 8; public static void main(String[] args) { // 生成随机密钥 byte[] secret = new byte[SECRET_LENGTH]; SecureRandom random = new SecureRandom(); random.nextBytes(secret); // 获取当前时间戳 long timestamp = System.currentTimeMillis() / 1000; // 计算时间步长 long timeStep = timestamp / TIME_STEP; // 计算哈希值 byte[] hash = hmacSha1(secret, longToBytes(timeStep)); // 取出动态口令 int offset = hash[hash.length - 1] & 0xf; int token = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff); token %= Math.pow(10, TOKEN_LENGTH); System.out.println("动态口令:" + String.format("%06d", token)); } /** * HMAC-SHA1算法 */ private static byte[] hmacSha1(byte[] key, byte[] data) { try { Mac mac = Mac.getInstance(HASH_ALGORITHM); SecretKeySpec secret = new SecretKeySpec(key, HASH_ALGORITHM); mac.init(secret); return mac.doFinal(data); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("HMAC-SHA1算法失败", e); } } /** * 将long类型转换为byte数组 */ private static byte[] longToBytes(long value) { byte[] bytes = new byte[8]; for (int i = 7; i >= 0; i--) { bytes[i] = (byte) (value & 0xff); value >>= 8; } return bytes; } } ``` 在实际应用中,我们需要将随机密钥保存在服务端,每次认证时从数据库中获取,并与用户输入的口令进行比较。同时,由于时间同步存在延迟和误差,我们需要允许一定的时间差来允许用户的动态口令通过认证。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值