Google Authenticator 原理及 Java 实现
实现原理:
一、用户需要开启 Google Authenticator 服务时,
-
服务器随机生成一个类似于『DPI45HKISEXU6HG7』的密钥,并且把这个密钥保存在数据库中。
-
在页面上显示一个二维码,内容是一个 URI 地址(
otpauth://totp/ 账号?secret = 密钥
),如『otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7』,下图:
1otpauth://totp/kisexu@gmail.com?secret=DPI45HCEBCJK6HG7 (二维码自动识别)
- 客户端扫描二维码,把密钥『DPI45HKISEXU6HG7』保存在客户端。
二、用户需要登陆时
1) 客户端每 30 秒使用密钥『DPI45HKISEXU6HG7』和时间戳通过一种『算法』生成一个 6 位数字的一次性密码,如『684060』。
如下图 android 版界面:
2) 用户登陆时输入一次性密码『684060』。
3) 服务器端使用保存在数据库中的密钥『DPI45HKISEXU6HG7』和时间戳通过同一种『算法』生成一个 6 位数字的一次性密码。
大家都懂控制变量法,如果算法相同、密钥相同,又是同一个时间(时间戳相同),那么客户端和服务器计算出的一次性密码是一样的。
服务器验证时如果一样,就登录成功了。
Tips
1) 这种『算法』是公开的,所以服务器端也有很多开源的实现,比如 php 版的:
上 github 搜索『Google Authenticator』可以找到更多语言版的 Google Authenticator。
2) 所以,你在自己的项目可以轻松加入对 Google Authenticator 的支持,在一个客户端上显示多个账户的效果可以看上面 android 版界面的截图。
目前 dropbox、lastpass、wordpress,甚至 vps 等第三方应用都支持 Google Authenticator 登陆,请自行搜索。
3) 现实生活中,网银、网络游戏的实体动态口令牌其实原理也差不多,大家可以自行脑补。
代码实现
GoogleAuthenticator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135import 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 <=17. Other
* values are ignored 5 * 动态密码3分钟 4 动态密码2.5分钟 3 动态密码2分钟 2 动态密码1.5分钟 1 动态密码1分钟
*/
public void setWindowSize(int s) {
if (s >= 1 && s <= 17)
window_size = s;
}
/**
* * Generate a random secret key. This must be saved by the server and *
* associated with the users account to verify the code displayed by Google *
* Authenticator. The user must register this secret on their device. * 生成一个随机秘钥
* * * @return secret key
*/
public 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;
}
/**
* * Return a URL that generates and displays a QR barcode. The user scans *
* this bar code with the Google Authenticator application on their * smartphone
* to register the auth code. They can also manually enter the * secret if
* desired * * @param user * user id (e.g. fflinstone) * @param host * host or
* system that the code is for (e.g. myapp.com) * @param secret * the secret
* that was previously generated for this user * @return the URL for the QR code
* to scan
*/
public static String getQRBarcodeURL(String user, String host, String secret) {
String format = "http://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s?secret=%s";
return String.format(format, user, host, secret);
}
/**
* * 生成一个google身份验证器,识别的字符串,只需要把该方法返回值生成二维码扫描就可以了。 * * @param user * 账号 * @param
* secret * 密钥 * @return
*/
public static String getQRBarcode(String user, String secret) {
String format = "otpauth://totp/%s?secret=%s";
return String.format(format, user, secret);
}
/**
* * Check the code entered by the user to see if it is valid 验证code是否合法 *
* * @param secret * The users secret. * @param code * The code displayed on the
* users device * @param t * The time in msec (System.currentTimeMillis() for
* example) * @return
*/
public boolean check_code(String secret, long code, long timeMsec) {
Base32 codec = new Base32();
byte[] decodedKey = codec.decode(secret);
// convert unix msec time into a 30 second "window"
// this is per the TOTP spec (see the RFC for details)
long t = (timeMsec / 1000L) / 30L;
// Window is used to check codes generated in the near past.
// You can use this value to tune how far you're willing to go.
for (int i = -window_size; i <= window_size; ++i) {
long hash;
try {
hash = verify_code(decodedKey, t + i);
} catch (Exception e) {
// Yes, this is bad form - but
// the exceptions thrown would be rare and a static
// configuration problem
e.printStackTrace();
throw new RuntimeException(e.getMessage());
// return false;
}
if (hash == code) {
return true;
}
}
// The validation code is invalid.
return false;
}
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;
// We're using a long because Java hasn't got unsigned int.
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
// We are dealing with signed bytes:
// we just keep the first byte.
truncatedHash |= (hash[offset + i] & 0xFF);
}
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
return (int) truncatedHash;
}
}
GooGleAuthenticatorTest.java
测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50import org.junit.jupiter.api.Test;
public class GooGleAuthenticatorTest {
// 当测试authTest时候,把genSecretTest生成的secret值赋值给它
private static String secret = "R2Q3S52RNXBTFTOM";
@Test
public void genSecretTest() {// 生成密钥
// secret = GoogleAuthenticator.generateSecretKey();
// 把这个qrcode生成二维码,用google身份验证器扫描二维码就能添加成功
String qrcode = GoogleAuthenticator.getQRBarcode("983126@qq.com", secret);
System.out.println("qrcode:" + qrcode + ",key:" + secret);
}
public static void main(String[] args) {
secret = GoogleAuthenticator.generateSecretKey();
// 把这个qrcode生成二维码,用google身份验证器扫描二维码就能添加成功
String qrcode = GoogleAuthenticator.getQRBarcode("983126@qq.com", secret);
System.out.println("qrcode:" + qrcode + ",key:" + secret);
System.out.println("########################################");
long code = 251665;
long t = System.currentTimeMillis();
GoogleAuthenticator ga = new GoogleAuthenticator();
ga.setWindowSize(5);
boolean r = ga.check_code(secret, code, t);
System.out.println("检查code是否正确?" + r);
String secret1 = "R2Q3S52RNXBTFTOM";
long code1 = 867255;
long t1 = System.currentTimeMillis();
GoogleAuthenticator ga1 = new GoogleAuthenticator();
ga1.setWindowSize(5);
boolean r1 = ga1.check_code(secret1, code1, t1);
System.out.println("检查code1是否正确?" + r1);
}
/**
* 对app的随机生成的code,输入并验证 WindowSize5 动态密码3分钟 WindowSize4 动态密码2.5分钟 WindowSize3
* 动态密码2分钟 WindowSize2 动态密码1.5分钟 WindowSize1 动态密码1分钟
*/
@Test
public void verifyTest() {
long code = 300884; // 14:44:00
long t = System.currentTimeMillis();
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
googleAuthenticator.setWindowSize(1);
boolean r = googleAuthenticator.check_code(secret, code, t);
System.out.println("检查code是否正确?" + r);
}
}
内网的测试验证
maven 引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<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>
编码实现
GoogleAuthenticatorUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168import 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;
}
}
GenerateQRCodeUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113import 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
测试代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import 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://my.oschina.net/lenglingx/blog/5394928
https://blog.csdn.net/dgatiger/article/details/110196740