谷歌身份验证器 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 ------------------------------------------------------------
博主真是太厉害了!!!
想想你的文章写的特别好
想想你的文章写的特别好https://www.237fa.com/
看的我热血沸腾啊https://www.237fa.com/
看的我热血沸腾啊https://www.ea55.com/
哈哈哈,写的太好了https://www.cscnn.com/
《尼尔杨:金子心》记录片高清在线免费观看:https://www.jgz518.com/xingkong/64238.html
《阿卡普高第二季》海外剧高清在线免费观看:https://www.jgz518.com/xingkong/98328.html
《火速救兵2》韩国剧高清在线免费观看:https://www.jgz518.com/xingkong/26668.html