MENU

Java 集成 Google Authenticator

July 25, 2023 • 技术阅读设置

谷歌身份验证器 Google Authenticator 是谷歌推出的基于时间的一次性密码(Time-based One-time Password,简称TOTP),只需要在手机上安装该APP,就可以生成一个随着时间变化的一次性密码,用于帐户验证。

代码实现部分

步骤

1.pom文件引入依赖。
2.创建 TOTP 工具类。
3.创建 GoogleAuthenticatorUtil 工具类。
4.创建 QRCodeUtil 工具类。

代码

maven依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.10.0</version>
</dependency>

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.3.0</version>
</dependency>

TOTP

根据密钥生成一次性 Google Authenticator 验证码,用于校验验证码是否正确。

package com.example.practise.utils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;

/**
 * description: 生成一次性 Google Authenticator 验证码
 * date: 2023/7/25 13:10
 * author: LenonJin
 */
public class TOTP {

    private TOTP() {
    }

    /**
     * This method uses the JCE to provide the crypto algorithm.
     * HMAC computes a Hashed Message Authentication Code with the
     * crypto hash algorithm as a parameter.
     *
     * @param crypto:   the crypto algorithm (HmacSHA1, HmacSHA256,
     *                  HmacSHA512)
     * @param keyBytes: the bytes to use for the HMAC key
     * @param text:     the message or text to be authenticated
     */

    private static byte[] hmac_sha(String crypto, byte[] keyBytes,
                                   byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);
            SecretKeySpec macKey =
                    new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);
            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }


    /**
     * This method converts a HEX string to Byte[]
     *
     * @param hex: the HEX string
     * @return: a byte array
     */

    private static byte[] hexStr2Bytes(String hex) {
        // Adding one byte to get the right conversion
        // Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = bArray[i + 1];
        }
        return ret;
    }

    private static final int[] DIGITS_POWER
            // 0 1  2   3    4     5      6       7        8
            = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key:          the shared secret, HEX encoded
     * @param time:         a value that reflects a time
     * @param returnDigits: number of digits to return
     * @return: a numeric String in base 10 that includes
     */

    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA1");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key:          the shared secret, HEX encoded
     * @param time:         a value that reflects a time
     * @param returnDigits: number of digits to return
     * @return: a numeric String in base 10 that includes
     */

    public static String generateTOTP256(String key,
                                         String time,
                                         String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA256");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key:          the shared secret, HEX encoded
     * @param time:         a value that reflects a time
     * @param returnDigits: number of digits to return
     * @return: a numeric String in base 10 that includes
     */

    public static String generateTOTP512(String key,
                                         String time,
                                         String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA512");
    }


    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key:          the shared secret, HEX encoded
     * @param time:         a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto:       the crypto function to use
     * @return: a numeric String in base 10 that includes
     */

    public static String generateTOTP(String key,
                                      String time,
                                      String returnDigits,
                                      String crypto) {
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;

        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16) {
            time = "0" + time;
        }

        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
        byte[] hash = hmac_sha(crypto, k, msg);

        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;

        int binary =
                ((hash[offset] & 0x7f) << 24) |
                        ((hash[offset + 1] & 0xff) << 16) |
                        ((hash[offset + 2] & 0xff) << 8) |
                        (hash[offset + 3] & 0xff);

        int otp = binary % DIGITS_POWER[codeDigits];

        result = Integer.toString(otp);
        while (result.length() < codeDigits) {
            result = "0" + result;
        }
        return result;
    }
}

GoogleAuthenticatorUtil

提供了 生成密钥、生成 Google Authenticator Key Uri(用于生成二维码),校验验证码等核心功能。

package com.example.practise.utils;

import cn.hutool.core.codec.Base32;
import cn.hutool.core.util.HexUtil;
import com.google.common.collect.ImmutableMap;
import lombok.SneakyThrows;
import org.apache.commons.text.StringSubstitutor;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.SecureRandom;

/**
 * description: GoogleAuthenticatorUtil 密钥生成、验证码校验
 * date: 2023/7/25 13:09
 * author: LenonJin
 */
public class GoogleAuthenticatorUtils {

    /**
     * 时间前后偏移量
     * 用于防止客户端时间不精确导致生成的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 能校验通过
     * 以此类推
     */
    private static final int TIME_OFFSET = 0;

    /**
     * 创建密钥
     */
    public static String createSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        return Base32.encode(bytes).toLowerCase();
    }

    /**
     * 根据密钥获取验证码
     * 返回字符串是因为数值有可能以0开头
     *
     * @param secretKey 密钥
     * @param time      第几个30秒 System.currentTimeMillis() / 1000 / 30
     */
    public static String generateTOTP(String secretKey, long time) {
        byte[] bytes = Base32.decode(secretKey.toUpperCase());
        String hexKey = HexUtil.encodeHexStr(bytes);
        String hexTime = Long.toHexString(time);
        return TOTP.generateTOTP(hexKey, hexTime, "6");
    }

    /**
     * 生成 Google Authenticator Key Uri
     * Google Authenticator 规定的 Key Uri 格式: otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
     * https://github.com/google/google-authenticator/wiki/Key-Uri-Format
     * 参数需要进行 url 编码 +号需要替换成%20
     *
     * @param secret  密钥 使用 createSecretKey 方法生成
     * @param account 用户账户 如: example@domain.com
     * @param issuer  服务名称 如: Google,GitHub
     * @throws UnsupportedEncodingException
     */
    @SneakyThrows
    public static String createKeyUri(String secret, String account, String issuer) throws UnsupportedEncodingException {
        String qrCodeStr = "otpauth://totp/${issuer}:${account}?secret=${secret}&issuer=${issuer}";
        ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
        mapBuilder.put("account", URLEncoder.encode(account, "UTF-8").replace("+", "%20"));
        mapBuilder.put("secret", URLEncoder.encode(secret, "UTF-8").replace("+", "%20"));
        mapBuilder.put("issuer", URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
        return StringSubstitutor.replace(qrCodeStr, mapBuilder.build());
    }

    /**
     * 校验方法
     *
     * @param secretKey 密钥
     * @param totpCode  TOTP 一次性密码
     * @return 验证结果
     */
    public static boolean verification(String secretKey, String totpCode) {
        long time = System.currentTimeMillis() / 1000 / 30;
        // 优先计算当前时间,然后再计算偏移量,因为大部分情况下客户端与服务的时间一致
        if (totpCode.equals(generateTOTP(secretKey, time))) {
            return true;
        }
        for (int i = -TIME_OFFSET; i <= TIME_OFFSET; i++) {
            // i == 0 的情况已经算过
            if (i != 0) {
                if (totpCode.equals(generateTOTP(secretKey, time + i))) {
                    return true;
                }
            }
        }
        return false;
    }

}

QRCodeUtil

二维码生成工具,可以将密钥生成为二维码,方便用户进行扫码导入 Google Authenticator APP。

package com.example.practise.utils;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * description: 二维码生成工具
 * date: 2023/7/25 13:10
 * author: LenonJin
 */
public class QRCodeUtil {
    // 二维码宽度
    private static final int width = 300;
    // 二维码高度
    private static final int height = 300;
    // 二维码文件格式
    private static final String format = "png";
    // 二维码参数
    private static final Map<EncodeHintType, Object> hints = new HashMap();

    static {
        // 字符编码
        hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
        // 容错等级 L、M、Q、H 其中 L 为最低, H 为最高
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 二维码与图片边距
        hints.put(EncodeHintType.MARGIN, 2);
    }

    /**
     * 返回一个 BufferedImage 对象
     *
     * @param content 二维码内容
     * @param width   宽
     * @param height  高
     */
    public static BufferedImage toBufferedImage(String content, int width, int height) throws WriterException {
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
        return MatrixToImageWriter.toBufferedImage(bitMatrix);
    }

    /**
     * 返回一个 base64 字符串
     *
     * @param content 二维码内容
     * @param width   宽
     * @param height  高
     */
    public static String toBase64(String content, int width, int height) throws WriterException, IOException {
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
        BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, "png", outputStream);
        outputStream.flush();
        byte[] byteArray = outputStream.toByteArray();
        outputStream.close();
        return Base64.getEncoder().encodeToString(byteArray);
    }

    /**
     * 将二维码图片输出到一个流中
     *
     * @param content 二维码内容
     * @param stream  输出流
     * @param width   宽
     * @param height  高
     */
    public static void writeToStream(String content, OutputStream stream, int width, int height) throws WriterException, IOException {
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
        MatrixToImageWriter.writeToStream(bitMatrix, format, stream);
    }

    /**
     * 生成二维码图片文件
     *
     * @param content 二维码内容
     * @param path    文件保存路径
     * @param width   宽
     * @param height  高
     */
    public static void createQRCode(String content, String path, int width, int height) throws WriterException, IOException {
        BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
        MatrixToImageWriter.writeToPath(bitMatrix, format, new File(path).toPath());
    }
}

测试使用

测试接口 GoogleAuthController

这里的 demo 通过三个接口依次展示了 如何生成密钥,如何根据密钥生成二维码,以及如何进行验证码的校验。

package com.example.practise.controller;

import com.example.practise.annotation.WebLog;
import com.example.practise.utils.GoogleAuthenticatorUtils;
import com.example.practise.utils.QRCodeUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

/**
 * description: 测试 Google Authenticator 使用
 * date: 2022/03/21 15:42
 * author: LenonJin
 */
@RestController
@RequestMapping("/google")
public class GoogleAuthController {


    @WebLog(description = "生成 Google Authenticator 密钥")
    @RequestMapping(value = "/createGoogleAuth")
    public String qrcode() throws UnsupportedEncodingException {
        // account 为密钥所属的账户名.
        // issuer 为密钥的发行方信息.
        // account 及 issuer 均会在 Google Authenticator APP 中展示.
        String secretKey = GoogleAuthenticatorUtils.createSecretKey();
        String secretKeyUri = GoogleAuthenticatorUtils.createKeyUri(secretKey, "Lenon", "测试发行方");
        return secretKeyUri;
    }

    @WebLog(description = "生成二维码")
    @RequestMapping(value = "/qrcode")
    public void qrcode(String secretKeyUri, HttpServletResponse response) {
        // 此处需要注意,生成二维码时,使用的是 GoogleAuthenticatorUtils.createKeyUri 生成的 secretKeyUri.
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            QRCodeUtil.writeToStream(secretKeyUri, outputStream, 300, 300);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @WebLog(description = "校验二维码")
    @RequestMapping(value = "/verify")
    public boolean verify(String secretKey, String code) {
        // 此处需要注意,校验二维码时,使用的是 GoogleAuthenticatorUtils.createSecretKey() 生成的 secretKey.
        return GoogleAuthenticatorUtils.verification(secretKey, code);
    }
}

测试结果

2023-07-25 16:19:46.462  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : ----------------------------------------------------------- Start -----------------------------------------------------------
2023-07-25 16:19:46.504  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Uri    :/google/createGoogleAuth
2023-07-25 16:19:46.514  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Ip     :127.0.0.1
2023-07-25 16:19:46.514  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Http Method    :GET
2023-07-25 16:19:46.515  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Description    :生成 Google Authenticator 密钥
2023-07-25 16:19:46.516  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Class Method   :com.example.practise.controller.GoogleAuthController.qrcode
2023-07-25 16:19:46.517  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Request Args   :args is null.
2023-07-25 16:19:46.540  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Response Args  :otpauth://totp/%E6%B5%8B%E8%AF%95%E5%8F%91%E8%A1%8C%E6%96%B9:Lenon?secret=fxp5p56wupk5ypqhrml4vpbsugwyrxi3&issuer=%E6%B5%8B%E8%AF%95%E5%8F%91%E8%A1%8C%E6%96%B9
2023-07-25 16:19:46.540  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : Time Consuming :78 ms
2023-07-25 16:19:46.540  INFO 13812 --- [nio-8080-exec-1] c.example.practise.aspectj.WebLogAspect  : ------------------------------------------------------------ End ------------------------------------------------------------
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : ----------------------------------------------------------- Start -----------------------------------------------------------
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Request Uri    :/google/qrcode
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Request Ip     :127.0.0.1
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Http Method    :GET
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Description    :生成二维码
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Class Method   :com.example.practise.controller.GoogleAuthController.qrcode
2023-07-25 16:19:57.155  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Request Args   :{"content":["otpauth://totp/测试发行方:Lenon?secret=fxp5p56wupk5ypqhrml4vpbsugwyrxi3&issuer=测试发行方"]}
2023-07-25 16:19:57.221  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Response Args  :null
2023-07-25 16:19:57.221  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : Time Consuming :66 ms
2023-07-25 16:19:57.221  INFO 13812 --- [nio-8080-exec-6] c.example.practise.aspectj.WebLogAspect  : ------------------------------------------------------------ End ------------------------------------------------------------
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : ----------------------------------------------------------- Start -----------------------------------------------------------
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Request Uri    :/google/verify
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Request Ip     :127.0.0.1
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Http Method    :GET
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Description    :校验二维码
2023-07-25 16:21:50.821  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Class Method   :com.example.practise.controller.GoogleAuthController.verify
2023-07-25 16:21:50.822  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Request Args   :{"secretKey":["fxp5p56wupk5ypqhrml4vpbsugwyrxi3"],"code":["744150"]}
2023-07-25 16:21:51.341  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Response Args  :true
2023-07-25 16:21:51.341  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : Time Consuming :520 ms
2023-07-25 16:21:51.341  INFO 13812 --- [nio-8080-exec-4] c.example.practise.aspectj.WebLogAspect  : ------------------------------------------------------------ End ------------------------------------------------------------
Last Modified: December 6, 2023
Leave a Comment

9 Comments
  1. 博主真是太厉害了!!!

  2. 想想你的文章写的特别好

  3. 想想你的文章写的特别好https://www.237fa.com/

  4. 看的我热血沸腾啊https://www.237fa.com/

  5. 看的我热血沸腾啊https://www.ea55.com/

  6. 哈哈哈,写的太好了https://www.cscnn.com/

  7. 《尼尔杨:金子心》记录片高清在线免费观看:https://www.jgz518.com/xingkong/64238.html

  8. 《阿卡普高第二季》海外剧高清在线免费观看:https://www.jgz518.com/xingkong/98328.html

  9. 《火速救兵2》韩国剧高清在线免费观看:https://www.jgz518.com/xingkong/26668.html