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类
1package controller; 2 3import java.security.InvalidKeyException; 4import java.security.NoSuchAlgorithmException; 5import java.security.SecureRandom; 6 7import javax.crypto.Mac; 8import javax.crypto.spec.SecretKeySpec; 9 10import org.apache.commons.codec.binary.Base32; 11import org.apache.commons.codec.binary.Base64; 12 13public class GoogleAuthenticatorSample { 14 15 public static final int SECRET_SIZE = 10; 16 17 public static final String SEED = "g8GjEvTbW5oVSV7avLBdwIHqGlUYNzKFI7izOF8GwLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx"; 18 19 public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG"; 20 21 int window_size = 3; // default 3 - max 17 (from google docs)最多可偏移的时间 22 23 public void setWindowSize(int s) { 24 if (s >= 1 && s <= 17) 25 window_size = s; 26 } 27 28 /** 29 * 验证身份验证码是否正确 30 * 31 * @param codes 32 * 输入的身份验证码 33 * @param savedSecret 34 * 密钥 35 * @return 36 */ 37 public static Boolean authcode(String codes, String savedSecret) { 38 long code = 0; 39 try { 40 code = Long.parseLong(codes); 41 } catch (Exception e) { 42 e.printStackTrace(); 43 } 44 long t = System.currentTimeMillis(); 45 GoogleAuthenticatorSample ga = new GoogleAuthenticatorSample(); 46 ga.setWindowSize(3); // should give 5 * 30 seconds of grace... 47 boolean r = ga.check_code(savedSecret, code, t); 48 return r; 49 } 50 51 /** 52 * 获取密钥 53 * 54 * @param user 55 * 用户 56 * @param host 57 * 域 58 * @return 密钥 59 */ 60 public static String genSecret(String user, String host) { 61 String secret = GoogleAuthenticatorSample.generateSecretKey(); 62 GoogleAuthenticatorSample.getQRBarcodeURL(user, host, secret); 63 return secret; 64 } 65 66 private static String generateSecretKey() { 67 SecureRandom sr = null; 68 try { 69 sr = SecureRandom.getInstance(RANDOM_NUMBER_ALGORITHM); 70 sr.setSeed(Base64.decodeBase64(SEED)); 71 byte[] buffer = sr.generateSeed(SECRET_SIZE); 72 Base32 codec = new Base32(); 73 byte[] bEncodedKey = codec.encode(buffer); 74 String encodedKey = new String(bEncodedKey); 75 return encodedKey; 76 } catch (NoSuchAlgorithmException e) { 77 // should never occur... configuration error 78 } 79 return null; 80 } 81 82 /** 83 * 获取二维码图片URL 84 * 85 * @param user 86 * 用户 87 * @param host 88 * 域 89 * @param secret 90 * 密钥 91 * @return 二维码URL 92 */ 93 public static String getQRBarcodeURL(String user, String host, String secret) { 94 String format = "https://www.google.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s"; 95 return String.format(format, user, host, secret); 96 } 97 98 private boolean check_code(String secret, long code, long timeMsec) { 99 Base32 codec = new Base32(); 100 byte[] decodedKey = codec.decode(secret); 101 long t = (timeMsec / 1000L) / 30L; 102 for (int i = -window_size; i <= window_size; ++i) { 103 long hash; 104 try { 105 hash = verify_code(decodedKey, t + i); 106 } catch (Exception e) { 107 e.printStackTrace(); 108 throw new RuntimeException(e.getMessage()); 109 } 110 if (hash == code) { 111 return true; 112 } 113 } 114 return false; 115 } 116 117 private static int verify_code(byte[] key, long t) 118 throws NoSuchAlgorithmException, InvalidKeyException { 119 byte[] data = new byte[8]; 120 long value = t; 121 for (int i = 8; i-- > 0; value >>>= 8) { 122 data[i] = (byte) value; 123 } 124 SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1"); 125 Mac mac = Mac.getInstance("HmacSHA1"); 126 mac.init(signKey); 127 byte[] hash = mac.doFinal(data); 128 int offset = hash[20 - 1] & 0xF; 129 long truncatedHash = 0; 130 for (int i = 0; i < 4; ++i) { 131 truncatedHash <<= 8; 132 truncatedHash |= (hash[offset + i] & 0xFF); 133 } 134 truncatedHash &= 0x7FFFFFFF; 135 truncatedHash %= 1000000; 136 return (int) truncatedHash; 137 } 138 139 public static void main(String[] args) { 140 /* 141 * 注意:先运行前两步,获取密钥和二维码url。 然后只运行第三步,填写需要验证的验证码,和第一步生成的密钥 142 */ 143 String user = "testUser"; 144 String host = "testHost"; 145 // 第一步:获取密钥 146 String secret = genSecret(user, host); 147 System.out.println("secret:" + secret); 148 // 第二步:根据密钥获取二维码图片url(可忽略) 149 String url = getQRBarcodeURL(user, host, secret); 150 System.out.println("url:" + url); 151 // 第三步:验证(第一个参数是需要验证的验证码,第二个参数是第一步生成的secret运行) 152 boolean result = authcode("271239", "OHXU6PLMZMDJIDY6"); 153 System.out.println("result:" + result); 154 } 155}

账户为:user@host。上面代码对应的账户为:testUser@testHost。
密钥是第一步生成。选择基于时间。
添加完成后,即可以动态的生成6位数字的验证码。
在项目需要做验证的地方,调用authcode()方法,传入用户输入的验证码和密钥,即可判断用户输入是否正确。