java 接入Google Authenticator 双因子验证

该Java类库用于生成GoogleAuthenticator的二维码信息和TOTP验证码。它包含了创建密钥、生成二维码、校验验证码的方法,支持时间偏移量以适应时间不一致的情况。代码使用了Base32和Hex编码,以及错误纠正等级和字符集配置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

生成 Google Authenticator 二维码所需信息, 以及验证方法

public class GeogleLoginAuthUtil {
    private static final String format = "png";// 默认二维码文件格式
    private static final Map<EncodeHintType, Object> hints = new HashMap();// 二维码参数
    /** 时间前后偏移量 */
    private static final int timeExcursion = 3;

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

    /**
     * 随机生成一个密钥
     */
    public static String createSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        String secretKey = base32.encodeToString(bytes);
        return secretKey.toLowerCase();
    }



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



    /**
     * 生成 Google Authenticator 二维码所需信息
     * Google Authenticator 约定的二维码信息格式 : otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
     * 参数需要 url 编码 + 号需要替换成 %20
     * @param secret  密钥 使用 createSecretKey 方法生成
     * @param account 用户账户 如: example@domain.com 138XXXXXXXX
     * @param issuer  服务名称 如: Google Github 印象笔记
     */
    public static String createGoogleAuthQRCodeData(String secret, String account, String issuer) {
        String qrCodeData = "otpauth://totp/%s?secret=%s&issuer=%s";
        try {
            return String.format(qrCodeData, URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20"), URLEncoder.encode(secret, "UTF-8")
                    .replace("+", "%20"), URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }


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

    /**
     * 生成二维码图片文件
     * @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);
        //toPath() 方法由 jdk1.7 及以上提供
        MatrixToImageWriter.writeToPath(bitMatrix, format, new File(path).toPath());
    }

    /**
     * 将二维码图片输出到一个流中
     * @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);
    }
 //   生成二维码输入流
//    public void qrcode(String content, @RequestParam(defaultValue = "300", required = false) int width,@RequestParam(defaultValue = "300", required = false) int height, HttpServletResponse response) {
//        ServletOutputStream outputStream = null;
//        try {
//            outputStream = response.getOutputStream();
//            writeToStream(content, outputStream, width, height);
//        } catch (Exception e) {
//            e.printStackTrace();
//        } finally {
//            if (outputStream != null) {
//                try {
//                    outputStream.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//    }


    /**
     * <dependency>
     *     <groupId>net.coobird</groupId>
     *     <artifactId>thumbnailator</artifactId>
     *     <version>0.4.8</version>
     * </dependency>
     * 图片合成
     * //Coordinate 存在一个 Coordinate(int x, int y) 构造函数, x 为水印距离底图左边的像素, y 为上边
     * BufferedImage bufferedImage = ImageIO.read(new File(logoFilePath));
     * Watermark watermark = new Watermark(new Coordinate(100, 100), bufferedImage, 1F);
     * 生成带logo二维码输入流
     * Thumbnails.of() 可以接收 BufferedImage,InputStream,URL,String,File 的可变 (** 批量处理需要 **) 参数类型
     * @param content
     * @param logoUrl
     * @param width
     * @param height
     * @param response
     */
//    public void qrcode(String content, String logoUrl, @RequestParam(defaultValue = "300") int width, @RequestParam(defaultValue = "300") int height,HttpServletResponse response) {
//        ServletOutputStream outputStream = null;
//        try {
//            outputStream = response.getOutputStream();
//            // 根据 QRCodeUtil.toBufferedImage() 返回的 BufferedImage 创建图片构件对象
//            Thumbnails.Builder<BufferedImage> builder = Thumbnails.of(toBufferedImage(content, width, height));
//            // 将 logo 的尺寸设置为二维码的 30% 大小, 可以自己根据需求调节
//            BufferedImage logoImage = Thumbnails.of(new URL(logoUrl)).forceSize((int) (width * 0.3), (int) (height * 0.3)).asBufferedImage();
//            // 设置水印位置(居中), 水印图片 BufferedImage, 不透明度(1F 代表不透明)
//            builder.watermark(Positions.CENTER, logoImage, 1F).scale(1F);
//            // 此处需指定图片格式, 否者报 Output format not specified 错误
//            builder.outputFormat("png").toOutputStream(outputStream);
//        } catch (Exception e) {
//            e.printStackTrace();
//        } finally {
//            if (outputStream != null) {
//                try {
//                    outputStream.close();
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
//    }


    /**
     * 验证码是使用基于时间的 TOTP 算法, 依赖于客户端与服务端时间的一致性. 如果客户端时间与服务端时间相差过大, 那在用户没有同步时间的情况下,
     * 永远与服务端进行匹配. 同时服务端也有可能出现时间偏差的情况, 这样反而导致时间正确的用户校验无法通过
     * 为了解决这种情况, 我们可以使用 ** 时间偏移量 ** 来解决该问题
     *
     * 校验方法
     * @param secretKey 密钥
     * @param code      用户输入的 TOTP 验证码
     */
    public static boolean verify(String secretKey, String code) {
        long time = System.currentTimeMillis() / 1000 / 30;
        for (int i = -timeExcursion; i <= timeExcursion; i++) {
            String totp = getTOTP(secretKey, time + i);
            if (code.equals(totp)) {
                return true;
            }
        }
        return false;
    }
}

TOTP算法:

public class TOTP {
    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 include truncationDigits digits
     */
    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 truncationDigits digits
     */
    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 truncationDigits digits
     */
    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 truncationDigits digits
     */
    public static String generateTOTP(String key, String time, String returnDigits, String crypto) {
        int codeDigits = Integer.decode(returnDigits);
        StringBuilder result;

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

        // 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 = new StringBuilder(Integer.toString(otp));
        while (result.length() < codeDigits) {
            result.insert(0, "0");
        }
        return result.toString();
    }

    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];
        System.arraycopy(bArray, 1, ret, 0, ret.length);
        return ret;
    }

//    public static void main(String[] args) {
//        // import org.apache.commons.lang3.RandomStringUtils
//        // Seed for HMAC-SHA1 - 20 bytes
//        String seed = RandomStringUtils.randomNumeric(40);
//        // Seed for HMAC-SHA256 - 32 bytes
//        String seed32 = RandomStringUtils.randomNumeric(64);
//        // Seed for HMAC-SHA512 - 64 bytes
//        String seed64 = RandomStringUtils.randomNumeric(128);
//        long T0 = 0;
//        long X = 30;
//        String returnDigits = "6";
//        long[] testTime = {59L, 1011217109L, 1121151171L, 1234567890L, 2008000000L, 20000700000L};
//
//        StringBuilder steps;
//        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        df.setTimeZone(TimeZone.getTimeZone("UTC"));
//
//        try {
//            System.out.println("+------------------seed:" + seed);
//            System.out.println("+------------------seed32:" + seed32);
//            System.out.println("+------------------seed64:" + seed64);
//            System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
//            System.out.println("|  Time(sec)    |   Time (UTC format)   " + "| Value of T(Hex)  |  TOTP  | Mode   |");
//            System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
//
//            for (long l : testTime) {
//                long T = (l - T0) / X;
//                steps = new StringBuilder(Long.toHexString(T).toUpperCase());
//                while (steps.length() < 16) {
//                    steps.insert(0, "0");
//                }
//                String fmtTime = String.format("%1$-11s", l);
//                String utcTime = df.format(new Date(l * 1000));
//                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " + steps + " |");
//                System.out.println(generateTOTP(seed, steps.toString(), returnDigits, "HmacSHA1") + "| SHA1   |");
//                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " + steps + " |");
//                System.out.println(generateTOTP(seed32, steps.toString(), returnDigits, "HmacSHA256") + "| SHA256 |");
//                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " + steps + " |");
//                System.out.println(generateTOTP(seed64, steps.toString(), returnDigits, "HmacSHA512") + "| SHA512 |");
//
//                System.out.println("+---------------+-----------------------+" + "------------------+--------+--------+");
//            }
//        } catch (final Exception e) {
//            System.out.println("Error : " + e);
//        }
//    }
}
1.能够熟练使用Java进行企业级项目的开发,熟悉企业开发的流程,对面向对象编程(OOP)思想一定的理解。 2.熟练使用SpringMVC、Spring Boot、Spring、Mybatis等多种框架,了解一定的IOC和AOP的原理,利用AOP实现过权限控制,如:超级管理员身份和普通员工,部门负责人的数据权限进行控制。 3.熟悉使用MySQL等关系型数据库,能进行数据库的查询进行一定程度的优化。 5.熟悉使用Redis,作为缓存数据库,加快数据的缓存与处理。 6.熟悉前后端分离服务器的部署,进行服务器上项目利用Nginx进行代理和HTTPS证书密钥的配置。 7.熟悉LInux系统,能够编写Shell脚本和进行简单的数据优化和服务器性能的简单调优。 8.熟悉掌握HTML,CSS,JavaScript,Vue,Node.js等前端技术栈,擅长使用Element-plus进行界面的编写。 9.熟练掌握Gitee、Maven、Postman、IDEA、VScode等常用开发工具 10.了解Spring Cloud微服务,了解Kafka,Docker等技术栈,能使用消息队列进行高并发的处理。 11.能完成大多数接口文档的编写和特定数据的编写。 12.了解NIO和BIO的网络编程,熟悉Netty框架,参与过物联网数据平台的开发,并进行过软件与硬件的对接和数据处理。 13.熟悉若依框架,擅长处理若依本身的开发逻辑,如:生成文件名转规定的格式,导出Excel自增编号,权限控制等问题。 帮我优化一下这份简历的内容,最好是结合项目的实际应用做出举例,不要太过千篇一律,同时可以给我补齐Spring Security的技术并举例
03-23
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值