Google身份验证器Google Authenticator是谷歌推出的基于时间的一次性密码(Time-based One-time Password,简称TOTP),只需要在手机上安装该APP,就可以生成一个随着时间变化的一次性密码,用于帐户验证。
相对于手机短信两步验证(Two-step verification),Google Authenticator两步验证有两大好处:一是避免无法接收验证短信的痛苦,尤其是用国内手机接收国外短信,你会发现经常收不到;二是Google Authenticator不用联网也可用,手机欠费停机不影响。
国内外各大网站和应用都基本上支持Google Authenticator两步验证了。这篇文章就简单说明一下,如何在自己的网站上集成Google身份验证器
一、准备工作
需要jar包:commons-codec-1.8。
二、Java类
package controller;
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 GoogleAuthenticatorSample {
public static final int SECRET_SIZE = 10;
public static final String SEED = "g8GjEvTbW5oVSV7avLBdwIHqGlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";
public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
int window_size = 3; // default 3 - max 17 (from google docs)最多可偏移的时间
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();
GoogleAuthenticatorSample ga = new GoogleAuthenticatorSample();
ga.setWindowSize(3); // should give 5 * 30 seconds of grace...
boolean r = ga.check_code(savedSecret, code, t);
return r;
}
/**
* 获取密钥
*
* @param user
* 用户
* @param host
* 域
* @return 密钥
*/
public static String genSecret(String user, String host) {
String secret = GoogleAuthenticatorSample.generateSecretKey();
GoogleAuthenticatorSample.getQRBarcodeURL(user, host, secret);
return secret;
}
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 = "https://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s";
return String.format(format, user, host, secret);
}
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;
}
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;
}
public static void main(String[] args) {
/*
* 注意:先运行前两步,获取密钥和二维码url。 然后只运行第三步,填写需要验证的验证码,和第一步生成的密钥
*/
String user = "testUser";
String host = "testHost";
// 第一步:获取密钥
String secret = genSecret(user, host);
System.out.println("secret:" + secret);
// 第二步:根据密钥获取二维码图片url(可忽略)
String url = getQRBarcodeURL(user, host, secret);
System.out.println("url:" + url);
// 第三步:验证(第一个参数是需要验证的验证码,第二个参数是第一步生成的secret运行)
boolean result = authcode("271239", "OHXU6PLMZMDJIDY6");
System.out.println("result:" + result);
}
}
账户为:user@host。上面代码对应的账户为:testUser@testHost。
密钥是第一步生成。选择基于时间。
添加完成后,即可以动态的生成6位数字的验证码。
在项目需要做验证的地方,调用authcode()方法,传入用户输入的验证码和密钥,即可判断用户输入是否正确。