接入Google认证Google Authenticator

  • 介绍

既然来看该文章就应该知道Google的两步认证是干什么的,如果需要APP(Google Authenticator)的可以私信我。

添加图片注释,不超过 140 字(可选)

验证原理讲解:

为每个用户在注册之前生成32位随机码(该码一般会存入数据库)。

调用API传入32位随机码,生成正确的6位验证码,每隔1分种会变化。

根据用户输入的6位验证码和正确的6位验证码做匹配,相同则登陆成功,不同则验证码时间失效或错误。

用户绑定讲解:

调用API生成32位随机码。。

调用API生成二维码QR字符串,需要传入用户信息(比如邮箱,昵称等),标题,以及生成的32位随机码。

调用API将二维码QR字符串转化为图片后以Base64的方式展现到前端页面上。

用户使用APP(Google Authenticator)扫码添加后,点击确认绑定,即可看到6位验证码。

后端根据API生成的32位随机码,用户信息(用来确定数据库中用户记录),以及输入6位验证码,通过API传入32位随机码验证,当其与输入的验证码相同时,则绑定成功,把32位随机码持久化与用户绑定。

添加图片注释,不超过 140 字(可选)

  • 准备工作

导入一下Maven依赖

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>
  • 导入工具类GoogleAuthenticatorUtils

    import org.apache.commons.codec.binary.Base32;
    import org.apache.commons.codec.binary.Base64;
    
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.security.SecureRandom;
    
    /**
     * @ProjectName: cdkj-framework
     * @Package: com.cdkjframework.util.tool
     * @ClassName: GoogleAuthenticatorUtils
     * @Description: 身份验证
     * @Author: xiaLin
     * @Date: 2023/4/3 15:16
     * @Version: 1.0
     */
    public class GoogleAuthenticatorUtils {
    
      /**
       * 生成的key长度( Generate secret key length)
       */
      public static final int SECRET_SIZE = 10;
    
      /**
       * SEED 值
       */
      public static final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
    
      /**
       * Java实现随机数算法
       */
      public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
    
      /**
       * 最多可偏移的时间
       * // default 3 - max 17
       */
      private int window_size = 3;
    
      /**
       * 设置窗口大小。这是一个整数值,表示
       * 我们允许30秒的窗口窗口越大
       * 时钟歪斜。
       *
       * @param s window size - must be >=1 and <=17. Other values are ignored
       */
      public void setWindowSize(int s) {
        if (s >= 1 && s <= 17)
          window_size = s;
      }
    
      /**
       * 生成一个随机密钥。这必须由服务器保存,并且
       * 与用户帐户关联,以验证谷歌显示的代码
       * 身份验证人。用户必须在其设备上注册此机密。
       * 生成一个随机秘钥
       *
       * @return 返回 secret key
       */
      public static String generateSecretKey() {
        SecureRandom random = null;
        try {
          random = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
          random.setSeed(Base64.decodeBase64(SEED));
          byte[] buffer = random.generateSeed(SECRET_SIZE);
          Base32 codec = new Base32();
          byte[] bEncodedKey = codec.encode(buffer);
          String encodedKey = new String(bEncodedKey);
          return encodedKey;
        } catch (NoSuchAlgorithmException e) {
          // 不应该发生。。。配置错误
        }
        return null;
      }
    
      /**
       * 生成一个google身份验证器,识别的字符串,只需要把该方法返回值生成二维码扫描就可以了。
       *
       * @param user   账号
       * @param secret 密钥
       * @return 返回结果
       */
      public static String getQRBarcode(String user, String secret, String issuer) {
        String format = "otpauth://totp/%s?secret=%s&issuer=%s";
        return String.format(format, user, secret, issuer);
      }
    
      /**
       * 验证code是否合法
       *
       * @param secret   用户的秘密
       * @param code     用户设备上显示的代码
       * @param timeMsec 时间(毫秒)
       * @return 返回结果
       */
      public boolean checkCode(String secret, long code, long timeMsec) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        // 将unix毫秒时间转换为30秒的“窗口”
        // 这是根据TOTP规范(有关详细信息,请参阅RFC)
        long t = (timeMsec / 1000L) / 30L;
        // 窗口用于检查最近生成的代码。
        // 您可以使用此值来调整您愿意走多远。
        for (int i = -window_size; i <= window_size; ++i) {
          long hash;
          try {
            hash = verifyCode(decodedKey, t + i);
          } catch (Exception e) {
            // 抛出的异常将是罕见的,并且是静态的
            // 配置问题
            throw new RuntimeException(e.getMessage());
          }
          if (hash == code) {
            return true;
          }
        }
        // 验证代码无效。
        return false;
      }
    
      /**
       * 验证随机码
       *
       * @param key 值
       * @param t   时间
       * @return 返回结果
       * @throws NoSuchAlgorithmException
       * @throws InvalidKeyException
       */
      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;
        // 我们使用long是因为Java没有无符号int。
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
          truncatedHash <<= 8;
          // 我们正在处理签名字节: 我们只保留第一个字节。
          truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
      }
    }
  • 需要注意

该方式生成的码由于偏移量的问题可能出现验证码过期却依然能通过验证的情况,可根据情况调整便宜量;具体生成二维码、验证接口这里不在贴代码了自行实现。

猜你喜欢

转载自blog.csdn.net/JPST228/article/details/132892411