保证登录安全-08-2FA Google Authenticator java 示例
2022年2月18日大约 9 分钟
Google Authenticator 原理及 Java 实现
实现原理:
一、用户需要开启 Google Authenticator 服务时,
服务器随机生成一个类似于『DPI45HKISEXU6HG7』的密钥,并且把这个密钥保存在数据库中。
在页面上显示一个二维码,内容是一个 URI 地址(
otpauth://totp/ 账号?secret = 密钥),如『otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7』,下图:
otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7 (二维码自动识别)- 客户端扫描二维码,把密钥『DPI45HKISEXU6HG7』保存在客户端。
二、用户需要登陆时
- 客户端每 30 秒使用密钥『DPI45HKISEXU6HG7』和时间戳通过一种『算法』生成一个 6 位数字的一次性密码,如『684060』。
如下图 android 版界面:

用户登陆时输入一次性密码『684060』。
服务器端使用保存在数据库中的密钥『DPI45HKISEXU6HG7』和时间戳通过同一种『算法』生成一个 6 位数字的一次性密码。
大家都懂控制变量法,如果算法相同、密钥相同,又是同一个时间(时间戳相同),那么客户端和服务器计算出的一次性密码是一样的。
服务器验证时如果一样,就登录成功了。
Tips
- 这种『算法』是公开的,所以服务器端也有很多开源的实现,比如 php 版的:
上 github 搜索『Google Authenticator』可以找到更多语言版的 Google Authenticator。
- 所以,你在自己的项目可以轻松加入对 Google Authenticator 的支持,在一个客户端上显示多个账户的效果可以看上面 android 版界面的截图。
目前 dropbox、lastpass、wordpress,甚至 vps 等第三方应用都支持 Google Authenticator 登陆,请自行搜索。
- 现实生活中,网银、网络游戏的实体动态口令牌其实原理也差不多,大家可以自行脑补。
代码实现
GoogleAuthenticator.java
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Base64;
public class GoogleAuthenticator {
// 生成的key长度( Generate secret key length)
public static final int SECRET_SIZE = 10;
public static final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";// Java实现随机数算法
public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";// 最多可偏移的时间
int window_size = 3; // default 3 - max 17
/**
* * set the windows size. This is an integer value representing the number of *
* 30 second windows we allow The bigger the window, the more tolerant of *
* clock skew we are. * * @param s * window size - must be >=1 and = 1 && s 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;
// We're using a long because Java hasn't got unsigned int.
long truncatedHash = 0;
for (int i = 0; i
commons-codec
commons-codec
1.14
com.google.zxing
core
3.4.1
junit
junit
4.11
test编码实现
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
* 主要功能:生成密钥、生成二维码内容、校验身份
* 依赖:
*
* commons-codec
* commons-codec
* 1.14
*
*/
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 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
* com.google.zxing
* core
* 3.4.1
*
*/
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 encodeMap = new HashMap();
// 内容编码,生成二维码矩阵
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);
}
}小结
希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。
我是老马,期待与你的下次重逢。
拓展阅读
参考资料
贡献者
binbin.hou
