• Google身份验证器Google Authenticator的java服务端实现


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

    在本实现demo中,注释说明非常详尽,可供参考,如遇问题欢迎可以留言沟通。

    废话不多说,直接上代码,本次代码尽可能简单,最简单的结构附图

    1. package com.wuge.google;
    2. import org.apache.commons.codec.binary.Base32;
    3. import org.apache.commons.codec.binary.Hex;
    4. import org.springframework.util.StringUtils;
    5. import javax.crypto.Mac;
    6. import javax.crypto.spec.SecretKeySpec;
    7. import java.io.UnsupportedEncodingException;
    8. import java.net.URLEncoder;
    9. import java.security.InvalidKeyException;
    10. import java.security.NoSuchAlgorithmException;
    11. import java.security.SecureRandom;
    12. /**
    13. * 谷歌身份验证器工具类
    14. *
    15. * @author wuge
    16. */
    17. public class GoogleAuthenticator {
    18. /**
    19. * 时间前后偏移量
    20. * 用于防止客户端时间不精确导致生成的TOTP与服务器端的TOTP一直不一致
    21. * 如果为0,当前时间为 10:10:15
    22. * 则表明在 10:10:00-10:10:30 之间生成的TOTP 能校验通过
    23. * 如果为1,则表明在
    24. * 10:09:30-10:10:00
    25. * 10:10:00-10:10:30
    26. * 10:10:30-10:11:00 之间生成的TOTP 能校验通过
    27. * 以此类推
    28. */
    29. private static int WINDOW_SIZE = 0;
    30. /**
    31. * 加密方式,HmacSHA1、HmacSHA256、HmacSHA512
    32. */
    33. private static final String CRYPTO = "HmacSHA1";
    34. /**
    35. * 生成密钥,每个用户独享一份密钥
    36. *
    37. * @return
    38. */
    39. public static String getSecretKey() {
    40. SecureRandom random = new SecureRandom();
    41. byte[] bytes = new byte[20];
    42. random.nextBytes(bytes);
    43. Base32 base32 = new Base32();
    44. String secretKey = base32.encodeToString(bytes);
    45. // make the secret key more human-readable by lower-casing and
    46. // inserting spaces between each group of 4 characters
    47. return secretKey.toUpperCase();
    48. }
    49. /**
    50. * 生成二维码内容
    51. *
    52. * @param secretKey 密钥
    53. * @param account 账户名
    54. * @param issuer 网站地址(可不写)
    55. * @return
    56. */
    57. public static String getQrCodeText(String secretKey, String account, String issuer) {
    58. String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
    59. try {
    60. return "otpauth://totp/"
    61. + URLEncoder.encode((!StringUtils.isEmpty(issuer) ? (issuer + ":") : "") + account, "UTF-8").replace("+", "%20")
    62. + "?secret=" + URLEncoder.encode(normalizedBase32Key, "UTF-8").replace("+", "%20")
    63. + (!StringUtils.isEmpty(issuer) ? ("&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20")) : "");
    64. } catch (UnsupportedEncodingException e) {
    65. throw new IllegalStateException(e);
    66. }
    67. }
    68. /**
    69. * 获取验证码
    70. *
    71. * @param secretKey
    72. * @return
    73. */
    74. public static String getCode(String secretKey) {
    75. String normalizedBase32Key = secretKey.replace(" ", "").toUpperCase();
    76. Base32 base32 = new Base32();
    77. byte[] bytes = base32.decode(normalizedBase32Key);
    78. String hexKey = Hex.encodeHexString(bytes);
    79. long time = (System.currentTimeMillis() / 1000) / 30;
    80. String hexTime = Long.toHexString(time);
    81. return TOTP.generateTOTP(hexKey, hexTime, "6", CRYPTO);
    82. }
    83. /**
    84. * 检验 code 是否正确
    85. *
    86. * @param secret 密钥
    87. * @param code code
    88. * @param time 时间戳
    89. * @return
    90. */
    91. public static boolean checkCode(String secret, long code, long time) {
    92. Base32 codec = new Base32();
    93. byte[] decodedKey = codec.decode(secret);
    94. // convert unix msec time into a 30 second "window"
    95. // this is per the TOTP spec (see the RFC for details)
    96. long t = (time / 1000L) / 30L;
    97. // Window is used to check codes generated in the near past.
    98. // You can use this value to tune how far you're willing to go.
    99. long hash;
    100. for (int i = -WINDOW_SIZE; i <= WINDOW_SIZE; ++i) {
    101. try {
    102. hash = verifyCode(decodedKey, t + i);
    103. } catch (Exception e) {
    104. // Yes, this is bad form - but
    105. // the exceptions thrown would be rare and a static
    106. // configuration problem
    107. // e.printStackTrace();
    108. throw new RuntimeException(e.getMessage());
    109. }
    110. if (hash == code) {
    111. return true;
    112. }
    113. }
    114. return false;
    115. }
    116. /**
    117. * 根据时间偏移量计算
    118. *
    119. * @param key
    120. * @param t
    121. * @return
    122. * @throws NoSuchAlgorithmException
    123. * @throws InvalidKeyException
    124. */
    125. private static long verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
    126. byte[] data = new byte[8];
    127. long value = t;
    128. for (int i = 8; i-- > 0; value >>>= 8) {
    129. data[i] = (byte) value;
    130. }
    131. SecretKeySpec signKey = new SecretKeySpec(key, CRYPTO);
    132. Mac mac = Mac.getInstance(CRYPTO);
    133. mac.init(signKey);
    134. byte[] hash = mac.doFinal(data);
    135. int offset = hash[20 - 1] & 0xF;
    136. // We're using a long because Java hasn't got unsigned int.
    137. long truncatedHash = 0;
    138. for (int i = 0; i < 4; ++i) {
    139. truncatedHash <<= 8;
    140. // We are dealing with signed bytes:
    141. // we just keep the first byte.
    142. truncatedHash |= (hash[offset + i] & 0xFF);
    143. }
    144. truncatedHash &= 0x7FFFFFFF;
    145. truncatedHash %= 1000000;
    146. return truncatedHash;
    147. }
    148. public static void main(String[] args) {
    149. for (int i = 0; i < 100; i++) {
    150. String secretKey = getSecretKey();
    151. System.out.println("secretKey:" + secretKey);
    152. String code = getCode(secretKey);
    153. System.out.println("code:" + code);
    154. boolean b = checkCode(secretKey, Long.parseLong(code), System.currentTimeMillis());
    155. System.out.println("isSuccess:" + b);
    156. }
    157. }
    158. }
    1. package com.wuge.google;
    2. import javax.crypto.Mac;
    3. import javax.crypto.spec.SecretKeySpec;
    4. import java.lang.reflect.UndeclaredThrowableException;
    5. import java.math.BigInteger;
    6. import java.security.GeneralSecurityException;
    7. /**
    8. * 验证码生成工具类
    9. *
    10. * @author wuge
    11. */
    12. public class TOTP {
    13. private static final int[] DIGITS_POWER = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
    14. /**
    15. * This method uses the JCE to provide the crypto algorithm. HMAC computes a
    16. * Hashed Message Authentication Code with the crypto hash algorithm as a
    17. * parameter.
    18. *
    19. * @param crypto : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
    20. * @param keyBytes : the bytes to use for the HMAC key
    21. * @param text : the message or text to be authenticated
    22. */
    23. private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
    24. try {
    25. Mac hmac;
    26. hmac = Mac.getInstance(crypto);
    27. SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
    28. hmac.init(macKey);
    29. return hmac.doFinal(text);
    30. } catch (GeneralSecurityException gse) {
    31. throw new UndeclaredThrowableException(gse);
    32. }
    33. }
    34. /**
    35. * This method converts a HEX string to Byte[]
    36. *
    37. * @param hex : the HEX string
    38. * @return: a byte array
    39. */
    40. private static byte[] hexStr2Bytes(String hex) {
    41. // Adding one byte to get the right conversion
    42. // Values starting with "0" can be converted
    43. byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
    44. // Copy all the REAL bytes, not the "first"
    45. byte[] ret = new byte[bArray.length - 1];
    46. System.arraycopy(bArray, 1, ret, 0, ret.length);
    47. return ret;
    48. }
    49. /**
    50. * This method generates a TOTP value for the given set of parameters.
    51. *
    52. * @param key : the shared secret, HEX encoded
    53. * @param time : a value that reflects a time
    54. * @param returnDigits : number of digits to return
    55. * @param crypto : the crypto function to use
    56. * @return: a numeric String in base 10 that includes
    57. */
    58. public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
    59. int codeDigits = Integer.decode(returnDigits);
    60. String result = null;
    61. // Using the counter
    62. // First 8 bytes are for the movingFactor
    63. // Compliant with base RFC 4226 (HOTP)
    64. while (time.length() < 16) {
    65. time = "0" + time;
    66. }
    67. // Get the HEX in a Byte[]
    68. byte[] msg = hexStr2Bytes(time);
    69. byte[] k = hexStr2Bytes(key);
    70. byte[] hash = hmac_sha(crypto, k, msg);
    71. // put selected bytes into result int
    72. int offset = hash[hash.length - 1] & 0xf;
    73. int binary = ((hash[offset] & 0x7f) << 24)
    74. | ((hash[offset + 1] & 0xff) << 16)
    75. | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
    76. int otp = binary % DIGITS_POWER[codeDigits];
    77. result = Integer.toString(otp);
    78. while (result.length() < codeDigits) {
    79. result = "0" + result;
    80. }
    81. return result;
    82. }
    83. }
    1. package com.wuge;
    2. import com.wuge.google.GoogleAuthenticator;
    3. import org.iherus.codegen.qrcode.SimpleQrcodeGenerator;
    4. import org.springframework.boot.SpringApplication;
    5. import org.springframework.boot.autoconfigure.SpringBootApplication;
    6. import org.springframework.web.bind.annotation.GetMapping;
    7. import org.springframework.web.bind.annotation.RequestParam;
    8. import org.springframework.web.bind.annotation.RestController;
    9. import javax.servlet.http.HttpServletResponse;
    10. /**
    11. * 项目启动类
    12. *
    13. * @author wuge
    14. */
    15. @RestController
    16. @SpringBootApplication
    17. public class Application {
    18. private static String SECRET_KEY = "";
    19. public static void main(String[] args) {
    20. SpringApplication.run(Application.class, args);
    21. }
    22. /**
    23. * 生成 Google 密钥,两种方式任选一种
    24. */
    25. @GetMapping("getSecretKey")
    26. public String getSecretKey() {
    27. String secretKey = GoogleAuthenticator.getSecretKey();
    28. SECRET_KEY = secretKey;
    29. return secretKey;
    30. }
    31. /**
    32. * 生成二维码,APP直接扫描绑定,两种方式任选一种
    33. */
    34. @GetMapping("getQrcode")
    35. public void getQrcode(@RequestParam("name") String name, HttpServletResponse response) throws Exception {
    36. String secretKey = GoogleAuthenticator.getSecretKey();
    37. SECRET_KEY = secretKey;
    38. // 生成二维码内容
    39. String qrCodeText = GoogleAuthenticator.getQrCodeText(secretKey, name, "");
    40. // 生成二维码输出
    41. new SimpleQrcodeGenerator().generate(qrCodeText).toStream(response.getOutputStream());
    42. }
    43. /**
    44. * 获取code
    45. */
    46. @GetMapping("getCode")
    47. public String getCode() {
    48. return GoogleAuthenticator.getCode(SECRET_KEY);
    49. }
    50. /**
    51. * 验证 code 是否正确
    52. */
    53. @GetMapping("checkCode")
    54. public Boolean checkCode(@RequestParam("code") String code) {
    55. return GoogleAuthenticator.checkCode(SECRET_KEY, Long.parseLong(code), System.currentTimeMillis());
    56. }
    57. }
    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0modelVersion>
    5. <parent>
    6. <groupId>org.springframework.bootgroupId>
    7. <artifactId>spring-boot-starter-parentartifactId>
    8. <version>2.3.1.RELEASEversion>
    9. <relativePath/>
    10. parent>
    11. <repositories>
    12. <repository>
    13. <id>aliyunid>
    14. <name>aliyun Repositoryname>
    15. <url>http://maven.aliyun.com/nexus/content/groups/publicurl>
    16. <snapshots>
    17. <enabled>falseenabled>
    18. snapshots>
    19. repository>
    20. repositories>
    21. <groupId>com.asurplusgroupId>
    22. <artifactId>asurplusartifactId>
    23. <version>0.0.1-SNAPSHOTversion>
    24. <name>asurplusname>
    25. <description>Google身份验证器description>
    26. <properties>
    27. <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
    28. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    29. <java.version>1.8java.version>
    30. <codec.version>1.15codec.version>
    31. <qrext4j.version>1.3.1qrext4j.version>
    32. properties>
    33. <dependencies>
    34. <dependency>
    35. <groupId>org.springframework.bootgroupId>
    36. <artifactId>spring-boot-starter-webartifactId>
    37. dependency>
    38. <dependency>
    39. <groupId>commons-codecgroupId>
    40. <artifactId>commons-codecartifactId>
    41. <version>${codec.version}version>
    42. dependency>
    43. <dependency>
    44. <groupId>org.iherusgroupId>
    45. <artifactId>qrext4jartifactId>
    46. <version>${qrext4j.version}version>
    47. dependency>
    48. dependencies>
    49. <build>
    50. <finalName>google-authfinalName>
    51. <plugins>
    52. <plugin>
    53. <groupId>org.springframework.bootgroupId>
    54. <artifactId>spring-boot-maven-pluginartifactId>
    55. plugin>
    56. plugins>
    57. build>
    58. project>

  • 相关阅读:
    Swoole 4.8版本的安装
    人工智能基础 作业6
    python爬虫入门(六)BeautifulSoup使用
    天津工业大学计算机考研资料汇总
    GaussDB(分布式)实例故障处理
    《重构改善代码设计》
    MacOS怎么安装Nacos(附带:Windows系统)
    C++异步并发编程future、promise和packaged_task三者的区别和联系
    设计模式 -- 状态模式(State Pattern)
    [含lw+源码等]微信小程序家政预约系统+后台管理系统[包运行成功]
  • 原文地址:https://blog.csdn.net/wuge507639721/article/details/134514462