内网实现Google Authenticator二步验证

2022-07-27,,,

0.概述

相关背景参考https://blog.csdn.net/lizhengjava/article/details/76947962,本Demo将调用google api生成二维码改为了com.google.zxing包本地生成。

1. pom.xml dependencies部分

  <dependencies>
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.14</version>
    </dependency>
    <dependency>
      <groupId>com.google.zxing</groupId>
      <artifactId>core</artifactId>
      <version>3.4.1</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

2.GoogleAuthenticatorUtils.java

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;


/**
 * @author alexhu
 *     主要功能:生成密钥、生成二维码内容、校验身份
 *     依赖:
 *     <dependency>
 *       <groupId>commons-codec</groupId>
 *       <artifactId>commons-codec</artifactId>
 *       <version>1.14</version>
 *     </dependency>
 */
public class GoogleAuthenticatorUtils {

    public static final int SECRET_SIZE = 10;

    public static final String SEED = "g8GjEvTbW5oVSV7avLBdwIHqGlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";

    public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";

    /**
     * default 3 - max 17 (from google docs)最多可偏移的时间
     */
    int window_size = 3;

    public void setWindowSize(int s) {
        if (s >= 1 && s <= 17) {
            window_size = s;
        }
    }

    /**
     * 验证身份验证码是否正确
     *
     * @param codes       输入的身份验证码
     * @param savedSecret 密钥
     * @return
     */
    public static Boolean authcode(String codes, String savedSecret) {
        long code = 0;
        try {
            code = Long.parseLong(codes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        long t = System.currentTimeMillis();
        GoogleAuthenticatorUtils ga = new GoogleAuthenticatorUtils();

        // should give 5 * 30 seconds of grace...
        ga.setWindowSize(ga.window_size);

        return ga.check_code(savedSecret, code, t);
    }

    /**
     * 获取密钥
     *
     * @param user 用户
     * @param host 域
     * @return 密钥
     */
    public static String genSecret(String user, String host) {
        String secret = GoogleAuthenticatorUtils.generateSecretKey();
        GoogleAuthenticatorUtils.getQRBarcodeURL(user, host, secret);
        return secret;
    }

    /**
     * 生成密钥
     *
     * @return
     */
    private static String generateSecretKey() {
        SecureRandom sr = null;
        try {
            sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM);
            sr.setSeed(Base64.decodeBase64(SEED));
            byte[] buffer = sr.generateSeed(SECRET_SIZE);
            Base32 codec = new Base32();
            byte[] bEncodedKey = codec.encode(buffer);
            String encodedKey = new String(bEncodedKey);
            return encodedKey;
        } catch (NoSuchAlgorithmException e) {
            // should never occur... configuration error
        }
        return null;
    }

    /**
     * 获取二维码内容URL
     *
     * @param user   用户
     * @param host   域
     * @param secret 密钥
     * @return 二维码URL
     */
    public static String getQRBarcodeURL(String user, String host, String secret) {
        String format = "otpauth://totp/%s@%s?secret=%s";
        return String.format(format, user, host, secret);
    }

    /**
     * 校验code是否正确
     *
     * @param secret 密钥
     * @param code   动态code
     * @param timeMsec 时间
     * @return
     */
    private boolean check_code(String secret, long code, long timeMsec) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        long t = (timeMsec / 1000L) / 30L;
        for (int i = -window_size; i <= window_size; ++i) {
            long hash;
            try {
                hash = verify_code(decodedKey, t + i);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
            if (hash == code) {
                return true;
            }
        }
        return false;
    }

    /**
     * 时间校验密钥与code是否匹配
     *
     * @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;
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
    }


}

3.GenerateQRCodeUtils.java

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;

/**
 * @author alexhu
 *
 * 主要功能:根据二维码内容生成二维码,并保存在指定位置
 *
 * 依赖:
 *     <dependency>
 *       <groupId>com.google.zxing</groupId>
 *       <artifactId>core</artifactId>
 *       <version>3.4.1</version>
 *     </dependency>
 */
public class GenerateQRCodeUtils {
    /**
     * 二维码颜色
     */
    private static final int BLACK = 0xFF000000;
    private static final int WHITE = 0xFFFFFFFF;

    /**
     * 图片的宽度
     */
    private static int WIDTH = 200;
    /**
     * 图片的高度
     */
    private static int HEIGHT = 200;
    /**
     * 图片的格式
     */
    private static String FORMAT = "png";


    /**
     * 生成二维码
     *
     * @param basePath 配置文件定义的生成二维码存放文件夹
     * @param content 二维码内容
     * @return 文件路径
     */
    public static String generateQRCodeImg(String basePath, String content){
        try {
            Map<EncodeHintType, String> encodeMap = new HashMap<EncodeHintType, String>();
            // 内容编码,生成二维码矩阵
            encodeMap.put(EncodeHintType.CHARACTER_SET, "utf-8");
            BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, WIDTH, HEIGHT, encodeMap);

            File file = new File(basePath);
            if (!file.exists() && !file.isDirectory()){
                file.mkdirs();
            }

            //文件名,默认为时间为名
            String filePath = basePath + System.currentTimeMillis() + "." + FORMAT;

            File outputFile = new File(filePath);
            if (!outputFile.exists()){
                // 生成二维码文件
                writeToFile(bitMatrix, FORMAT, outputFile);
            }
            return filePath;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 把二维码矩阵保存为文件
     *
     * @param matrix 二维码矩阵
     * @param format 文件类型,这里为png
     * @param file  文件句柄
     * @throws IOException
     */
    public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
        BufferedImage image = toBufferedImage(matrix);
        if (!ImageIO.write(image, format, file)) {
            throw new IOException("Could not write an image of format " + format + " to " + file);
        }
    }

    /**
     * 生成二维码矩阵(内存)
     *
     * @param matrix 二维码矩阵
     * @return
     */
    public static BufferedImage toBufferedImage(BitMatrix matrix) {
        int width = matrix.getWidth();
        int height = matrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
            }
        }
        return image;
    }
}

4.GoogleAuthenticatorTest.java

import org.junit.Test;

import static org.example.GenerateQRCodeUtils.generateQRCodeImg;
import static org.example.GoogleAuthenticatorUtils.*;

/**
 * Unit test for Google Authenticator.
 */
public class GoogleAuthenticatorTest {
    /**
     * Rigorous Test :-)
     */
    @Test
    public void genTest() {
        /*
         * 注意:先运行前两步,获取密钥和二维码url。 然后只运行第三步,填写需要验证的验证码,和第一步生成的密钥
         */
        String user = "testUser";
        String host = "test.com";
        // 第一步:获取密钥
        String secret = genSecret(user, host);
        System.out.println("secret:" + secret);
        // 第二步:根据密钥获取二维码图片url(可忽略)
        String url = getQRBarcodeURL(user, host, secret);
        System.out.println("url:" + url);
        // 第三步 生成二维码
        generateQRCodeImg("", url);
    }

    @Test
    public void verifyTest() {
        // 第四步:验证(第一个参数是需要验证的验证码,第二个参数是第一步生成的secret运行)
        boolean result = authcode("105938", "WUH2RO3Q4D53AF5Z");
        System.out.println("result:" + result);
    }
}

本文地址:https://blog.csdn.net/dgatiger/article/details/110196740

《内网实现Google Authenticator二步验证.doc》

下载本文的Word格式文档,以方便收藏与打印。