谷歌身份验证器

Google身份验证器Google Authenticator是谷歌推出的一款基于时间与哈希的一次性密码算法的两步验证软件令牌,此软件用于Google的认证服务。此项服务所使用的算法已列于RFC 6238和RFC 4226中。谷歌验证器上的动态密码按照时间或使用次数不断动态变化(默认30秒变更一次)。

一、实现原理及步骤


Google身份验证系统是通过基于时间的一次性密码算法实现的双因子(2FA)验证,即TOTP(Time-Based One-Time Password)算法。该算法由3部分组成:

  • 一个共享密钥
  • 一个基于当前时间的输入
  • 一个签名函数

1. Shared Secret(共享密钥)
用户在手机端进行身份验证的时候,需要获取共享密钥。拿Authy举例,获取的方式包括识别程序扫描给定的二维码(QR)或者直接手动输入,密钥是32位加密的字符串。

  • 扫描QR用户,QR识别的URL链接为:
    otpauth://totp/Google%3Ayourname@gmail.com?secret=xxxx&issuer=Google
  • 对于手动输入的用户,Google身份验证系统给出的共享密钥格式为:
    xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx

步骤如下:

  • 用户开启双因子(2FA)验证后,服务器会生成一个密钥(secret);
  • 服务器提示用户扫描二维码或者手动输入的方式,将密钥保存在用户的手机上。此时,用户和服务器现在都拥有同一把钥匙。(需要注意的是密钥必须跟手机绑定,一旦用户更换手机,需要重新生成全新的密钥);
  • 用户登录时,手机客户端Authy使用这个密钥和当前的Unix时间戳,生成一个hash value(h1),有效期默认为30s。用户在有效期内,将这个哈希值提交给服务器;
  • 服务器也使用密钥和当前时间戳,生成一个hash value(h2),将h2和用户提交的h1进行比较,如果两者一致,就能够正常登陆,否则,拒绝登陆。

2. Input(Current Time)
简单来说,输入是基于用户手机产生的当前时间,一旦你获取到secret密钥,就不用再与服务器进行通信了。但是这里比较重要的是用户手机时间要准确,因为从算法原理来讲,身份验证服务器会基于同样的时间来重复进行用户手机的运算。进一步来说,服务器会计算当前时间前后几分钟内的令牌,跟用户提交的令牌比较。所以如果时间上相差太多,身份验证过程就会失败。

3. Signing Function(签名函数)
签名函数使用的是HMAC-SHA1。HMAC是基于哈希的消息验证码,能够用安全的单向哈希函数(SHA1)来生成签名。

验证算法的原理:只有共享密钥拥有者和服务器才能根据同样的输入(基于时间的)得到同样的输出签名。

  hmac = SHA1(secret + SHA1(secret + input))

二、算法


1. 首先,使用base32的解码密钥
为了更方便用户输入,谷歌采用了空格和小写的方式表示密钥。但是base32不能有空格而且必须大写,伪代码如下:

  original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
  secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))

2. 接下来,从当前时间获取输入,即Unix时间戳

  input = CURRENT_UNIX_TIME()

一般验证码会有个失效时间,大概30s。这种设计主要是出于方便用户输入考虑,为了实现这种时效性,可以通过整除30的方式来实现。

  input = CURRENT_UNIX_TIME() / 30

3. 最后一步,签名函数(HMAC-SHA1)

  original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
  secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
  input = CURRENT_UNIX_TIME() / 30
  hmac = SHA1(secret + SHA1(secret + input))

SHA1中的input转换为byte[8]的时候一定要是大端转换。
由于HMAC是个标准长度的SHA1数值,有20-byte,40 hex characters的长度,对于用户来说太长,所以google会根据规则截取6位数字。(也就是我们在Authy中看到的6个数字)

4. 最终的伪代码

  original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
  secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
  input = CURRENT_UNIX_TIME() / 30
  hmac = SHA1(secret + SHA1(secret + input))
  ​
  four_bytes = hmac[LAST_BYTE(hmac):LAST_BYTE(hmac) + 4]
  large_integer = INT(four_bytes)
  small_integer = large_integer % 1,000,000 //生成的6位数字身份验证码

三、具体实现(Java)

使用思路

  1. 第一次请求,判断未绑定谷歌验证码,那就生成随机base32的秘钥展示到页面,供用户创建账号,或者生成二维码,供用户在手机端使用谷歌验证器扫码创建账号;
  2. 绑定验证,将手机app谷歌验证器生成的验证码输入到需要登录的平台验证,成功后将秘钥存入数据库;
  3. 绑定过后每次请求查询数据库的秘钥生成二维码传出;
  4. 取消绑定,清除数据库的数据。

使用步骤

1. 手机上下载谷歌身份验证器,便于生成验证码

  • iPhone手机:在App Store中搜索 Google Authenticator
  • 安卓手机:点击链接下载

2. 整合java

2.1. 导入依赖
点击查看代码
  <!--胡图工具包-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.2</version>
    </dependency>
		<!--二维码的生成工具类-->
    <dependency>
        <groupId>com.google.zxing</groupId>
        <artifactId>core</artifactId>
        <version>3.3.3</version>
    </dependency>
		<!--谷歌身份验证器-->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.12</version>
    </dependency>
2.2. 编写谷歌身份验证器工具类
点击查看代码
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  import java.security.SecureRandom;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;
  import org.apache.commons.codec.binary.Base32;
  import org.apache.commons.codec.binary.Base64;


  /**
   * @Description: 谷歌身份验证器工具类
   */
  public class GoogleAuthenticator {

      /**
       * 生成秘钥的长度
       */
      public static final int SECRET_SIZE = 10;

      public static final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
      /**
       * 实现随机数算法
       */
      public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

      /**
       * 时间前后偏移量
       * 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致
       * 如果为0,当前时间为 10:10:15
       * 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过
       * 如果为1,则表明在
       * 10:09:30-10:10:00
       * 10:10:00-10:10:30
       * 10:10:30-10:11:00 之间生成的TOTP 能校验通过
       * 以此类推
       */
        int window_size = 10; // default 3 - max 17

      /**
       * set the windows size. This is an integer value representing the number of
       * 30 second windows we allow The bigger the window, the more tolerant of
       * clock skew we are.
       *
       * @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 sr = null;
          try {
              sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
              sr.setSeed(Base64.decodeBase64(SEED.getBytes()));
              byte[] buffer = sr.generateSeed(SECRET_SIZE);
              Base32 codec = new Base32();
              byte[] bEncodedKey = codec.encode(buffer);
              return new String(bEncodedKey);
          } catch (NoSuchAlgorithmException e) {
              // should never occur... configuration error
          }
          return null;
      }


      /**
       * 生成一个google身份验证器,识别的字符串,只需要把该方法返回值生成二维码扫描就可以了。
       * 最后展示的账户名称将会是 label:user
       * @param label 标签
       * @param user 账号
       * @param secret 密钥
       * @return
       */
      public static String getQRBarcode(String label, String user, String secret) {
          String format = "otpauth://totp/%s:%s?secret=%s";
          return String.format(format, label, user, secret);
      }

      /**
       * 生成一个google身份验证器,识别的字符串,只需要把该方法返回值生成二维码扫描就可以了。
       *最后展示的账户名称将会是 user
       * @param user 账号
       * @param secret 密钥
       * @return
       */
      public static String getQRBarcode(String user, String secret) {
          String format = "otpauth://totp/%s?secret=%s";
          return String.format(format, user, secret);
      }

      /**
       * 验证code是否合法
       * @param secret 秘钥
       * @param code 验证码
       * @param timeMses 时间戳
       * @return true表示正确 false 表示错误
       */
      public  boolean check_code(String secret, long code, long timeMses) {
          if(secret == null || "".equals(secret))
          {
              return false;
          }
          Base32 codec = new Base32();
          byte[] decodedKey = codec.decode(secret);
          // convert unix msec time into a 30 second "window"
          // this is per the TOTP spec (see the RFC for details)
          long t = (timeMses / 1000L) / 30L;
          // Window is used to check codes generated in the near past.
          // You can use this value to tune how far you're willing to go.
          for (int i = -window_size; i <= window_size; ++i) {
              long hash;
              try {
                  hash = verify_code(decodedKey, t + i);
              } catch (Exception e) {
                  // Yes, this is bad form - but
                  // the exceptions thrown would be rare and a static
                  // configuration problem
                  e.printStackTrace();
                  throw new RuntimeException(e.getMessage());
                  // return false;
              }
              if (hash == code) {
                  return true;
              }
          }
          // The validation code is invalid.
          return false;
      }

      /**
       * 根据时间偏移量计算
       *
       * @param key
       * @param t
       * @return
       * @throws NoSuchAlgorithmException
       * @throws InvalidKeyException
       */
      private static int verify_code(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;
          // We're using a long because Java hasn't got unsigned int.
          long truncatedHash = 0;
          for (int i = 0; i < 4; ++i) {
              truncatedHash <<= 8;
              // We are dealing with signed bytes:
              // we just keep the first byte.
              truncatedHash |= (hash[offset + i] & 0xFF);
          }
          truncatedHash &= 0x7FFFFFFF;
          truncatedHash %= 1000000;
          return (int) truncatedHash;
      }
  }
2.3. 测试生成秘钥
点击查看代码
  import cn.hutool.core.io.FileUtil;
  import cn.hutool.extra.qrcode.QrCodeUtil;
  import com.example.vehicleinformationsystem.util.GoogleAuthenticator;
  import org.junit.jupiter.api.Test;
  import org.springframework.boot.test.context.SpringBootTest;

  @SpringBootTest
  class VehicleInformationSystemApplicationTests {
      private static String secretKey = "";

      /**
       * 生成秘钥
       */
      @Test
      void contextLoads() {
          //生成秘钥
          secretKey = GoogleAuthenticator.generateSecretKey();
  	    System.out.println("秘钥:"+secretKey);
      }
  }
2.4. 结果
  秘钥:DPSKD2HAZVANIXX7

3. 秘钥使用方式:

  • 在手机上下载谷歌身份验证器(下载方式参考上文)
  • 点击右下角加号
  • 点击输入设置秘钥
  • 输入需要登录的平台用户名,以及刚才的秘钥,选择基于时间,点击添加
  • 将账号的二维码填写到需要登录的平台进行验证

4. 测试生成二维码

  • 生成二维码代码

    点击查看代码
      /**
       * 生成二维码
       */
      @Test
      void contextLoads() {
          //生成秘钥
          secretKey = GoogleAuthenticator.generateSecretKey();
          System.out.println("秘钥:"+secretKey);
          //生成二维码信息
          String QRBarcode = GoogleAuthenticator.getQRBarcode("system_admin",secretKey);
          System.out.println("二维码所需要的信息:"+QRBarcode);
          //生成二维码  300 表示二维码的大小  D:\img\aa\qrcode.jpg 表示为二维码的生成路径
          QrCodeUtil.generate(QRBarcode, 300, 300, FileUtil.file("D:\\img\\aa\\qrcode.jpg"));
      }
    
  • 在手机上下载谷歌身份验证器(下载方式参考上文)

  • 点击右下角加号

  • 点击扫描二维码

  • 扫描生成的二维码,即可

  • 将账号的二维码填写到需要登录的平台进行验证

5. 测试验证码

点击查看代码
  //刚才生成的秘钥
  private static String secretKey = "DPSKD2HAZVANIXX7";


  /**
   * 测试验证码
   */
  @Test
  void testData() {
      String code = "126128";
      long time = System.currentTimeMillis ();
      GoogleAuthenticator g = new GoogleAuthenticator ();
      boolean result = g.check_code (secretKey,Long.valueOf(code),time );
      System.out.println ( "验证码是否正确--》"+result );
  }

Google验证器是如何实现的
谷歌身份验证器的使用超详细步骤

posted @ 2022-12-13 15:23  Fogram  阅读(2355)  评论(0编辑  收藏  举报