两步验证Web安全工具类

import cn.hutool.core.codec.Base32;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HMac;


public class TwoFactorAuthUtils {

   private static final int VALID_TFA_WINDOW_MILLIS = 60_000;


   /**
    * 生成两步验证 Key
    *
    * @return 两步验证 Key
    */
   public static String generateTFAKey() {
      return TimeBasedOneTimePasswordUtil.generateBase32Secret(32);
   }

   /**
    * 生成两步验证码
    *
    * @param tfaKey 两步验证 Key
    * @return 两步验证码
    */
   public static String generateTFACode(String tfaKey) {
      return TimeBasedOneTimePasswordUtil.generateCurrentNumberString(tfaKey);
   }

   /**
    * 验证两步验证码
    *
    * @param tfaKey  两步验证 Key
    * @param tfaCode 两步验证码
    */
   public static boolean validateTFACode(String tfaKey, String tfaCode) {
      int validCode = Convert.toInt(tfaCode, 0);
      return TimeBasedOneTimePasswordUtil.validateCurrentNumber(tfaKey, validCode, VALID_TFA_WINDOW_MILLIS);
   }

   /**
    * 生成 Otp Auth Url
    *
    * @param userName 用户名
    * @param tfaKey   两步验证 Key
    * @return URL
    */
   public static String generateOtpAuthUrl(String userName, final String tfaKey) {
      String jpomName = "jpom-" + userName;
      return TimeBasedOneTimePasswordUtil.generateOtpAuthUrl(jpomName, tfaKey);
   }

   private static class TimeBasedOneTimePasswordUtil {
      public static final int DEFAULT_TIME_STEP_SECONDS = 30;
      private static final int NUM_DIGITS_OUTPUT = 6;

      public static String generateBase32Secret(int length) {
         return RandomUtil.randomString(RandomUtil.BASE_CHAR, length).toUpperCase();
      }

      public static boolean validateCurrentNumber(String base32Secret, int authNumber,
                                       int windowMillis) {
         return validateCurrentNumber(base32Secret, authNumber, windowMillis,
               System.currentTimeMillis(),
               DEFAULT_TIME_STEP_SECONDS);
      }

      public static boolean validateCurrentNumber(String base32Secret, int authNumber,
                                       int windowMillis, long timeMillis,
                                       int timeStepSeconds) {
         long fromTimeMillis = timeMillis;
         long toTimeMillis = timeMillis;
         if (windowMillis > 0) {
            fromTimeMillis -= windowMillis;
            toTimeMillis += windowMillis;
         }
         long timeStepMillis = timeStepSeconds * 1000L;
         for (long millis = fromTimeMillis; millis <= toTimeMillis; millis += timeStepMillis) {
            int generatedNumber = generateNumber(base32Secret, millis, timeStepSeconds);
            if (generatedNumber == authNumber) {
               return true;
            }
         }
         return false;
      }

      public static String generateCurrentNumberString(String base32Secret) {
         return generateNumberString(base32Secret, System.currentTimeMillis(), DEFAULT_TIME_STEP_SECONDS);
      }

      public static String generateNumberString(String base32Secret, long timeMillis, int timeStepSeconds) {
         int number = generateNumber(base32Secret, timeMillis, timeStepSeconds);
         String numStr = Integer.toString(number);
         return StrUtil.fillBefore(numStr, '0', TimeBasedOneTimePasswordUtil.NUM_DIGITS_OUTPUT);
      }

      public static int generateNumber(String base32Secret, long timeMillis, int timeStepSeconds) {

         byte[] key = Base32.decode(base32Secret);

         byte[] data = new byte[8];
         long value = timeMillis / 1000 / timeStepSeconds;
         for (int i = 7; value > 0; i--) {
            data[i] = (byte) (value & 0xFF);
            value >>= 8;
         }
         HMac hMac = SecureUtil.hmacSha1(key);
         byte[] hash = hMac.digest(data);

         // take the 4 least significant bits from the encrypted string as an offset
         int offset = hash[hash.length - 1] & 0xF;

         // We're using a long because Java hasn't got unsigned int.
         long truncatedHash = 0;
         for (int i = offset; i < offset + 4; ++i) {
            truncatedHash <<= 8;
            // get the 4 bytes at the offset
            truncatedHash |= hash[i] & 0xFF;
         }
         // cut off the top bit
         truncatedHash &= 0x7FFFFFFF;

         // the token is then the last 6 digits in the number
         truncatedHash %= 1000000;
         // this is only 6 digits so we can safely case it
         return (int) truncatedHash;
      }

      public static String generateOtpAuthUrl(String keyId, String secret) {
         //       addOtpAuthPart(keyId, secret, sb);
         return "otpauth://totp/" + keyId + "?secret=" + secret;
      }
   }
}

两步验证应用

【推荐】腾讯身份验证码 简单好用 Android

Authy 功能丰富 专为两步验证码 iOS/Android/Windows/Mac/Linux   Chrome 扩展

Google Authenticator 简单易用,但不支持密钥导出备份 iOS Android

Microsoft Authenticator 使用微软全家桶的推荐 iOS/Android

1Password 强大安全的密码管理付费应用iOS/Android/Windows/Mac/Linux/ChromeOS

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没事搞点事做serendipity

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值