最近,鸭哥在不少交易所的安全中心,发现了一个Google身份验证器,而且需要安装一个“GoogleAuthenticator”的应用,才能正常使用,再比如咱们登录堡垒机,Github,也可以用它来验证,具体见下图:

太赞了!Google推出的抵御密码泄露的“杀手锏”已开源,该如何接入呢?_java


我们如果安装好应用GoogleAuthenticator,便会每隔一段时间便会生成一个新的验证码,用于登录校验,给我的感觉,跟短信验证码类似~

太赞了!Google推出的抵御密码泄露的“杀手锏”已开源,该如何接入呢?_java_02


# GoogleAuthenticator是什么鬼?


Google身份验证器是一款TOTP与HOTP的两步验证软件令牌,此软件用于Google的认证服务。给予用户一个六位到八位的一次性密码用于进行登录Google或其他站点时的附加验证。维基百科

说得直白点,就是一个二次验证服务,用户需要下载该软件令牌,这样做的好处就不言而喻了,就算别人获取到你的账号密码,也进入不了你的账户。


这个鸭哥当年是身有体会的,当年搞了一批邮箱的账密,想突破二次验证,获取重要邮件,搞了一个月,都突破不了~

太赞了!Google推出的抵御密码泄露的“杀手锏”已开源,该如何接入呢?_java_03

还是太菜了~


该项目是开源的,安卓和iOS都支持,可以在Github上面查看源码。


官网:https://github.com/google/google-authenticator



# 工作原理


它是采用的TOTP 算法(Time-based One-time Password,即基于时间的一次性密码),主要包括这3个核心部分。


1、密钥:客户端和服务器事先协商好一个密钥K,用于一次性密码的生成过程,此密钥不被任何第三方所知道。


2、验证时间:Google选择了30秒作为时间片,T的数值为从Unix epoch(1970年1月1日 00:00:00)来经历的30秒的个数。所以,你便会发现验证码30秒便会刷新一次。


3、算法:客户端对密钥和计数器的组合(K,C)使用HMAC(Hash-based Message Authentication Code)算法计算一次性密码,公式:HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))。


# 如何接入?


下面的接入代码部分参考网友分享,经过鸭哥亲自测试,可以使用。


核心代码实例代码:

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;/** * 微信公众号:小黄鸭编程社区 */public class GoogleAuthenticator {// 生产 key 的长度public static final int SECRET_SIZE = 10;// SHA1PRNG 算法种子public static final String SEED = "g8GjEvTbW5oVSV7avL47357438reyhreyuryetredLDVKs2m0QN7vxRs2im5MDaNCWGmcD2rvcZx";// Java实现随机数算法public static final String RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";int window_size = 3; // min 3  ---  max 17public void setWindowSize(int s) {if (s >= 1 && s <= 17)            window_size = s;    }// 生成随机秘钥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) {            e.printStackTrace();        }return null;    }/**     * 请求一个 QR 链接     *     * @param user     * @param host     * @param secret     * @return     */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);    }/**     * 验证6位验证码     *     * @param secret     * @param code     * @param timeMsec     * @return     */public boolean check_code(String secret, String 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) {            String 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.equals(code)) {return true;            }        }// The validation code is invalid.return false;    }/**     * 生成验证码     *     * @param key secret 编码后的字节数组     * @param t 时间     * @return 验证码     */private static String 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 String.valueOf(truncatedHash);    }}

测试代码:

/** * 微信公众号:小黄鸭编程社区 */public class GoogleAuthTest {// Change this to the saved secret from the running the above test.static String savedSecret = "3BEK7222DFODDJXO";    @Testpublic void genSecretTest() {        String secret = GoogleAuthenticator.generateSecretKey();        String url = GoogleAuthenticator.getQRBarcodeURL("小黄鸭编程社区", "studycoder.com", secret);        System.out.println("二维码地址:" + url);        System.out.println("密钥:" + secret);    }
   @Testpublic void authTest() {// enter the code shown on device. Edit this and run it fast before the code expires!        String code = "287875";        long t = System.currentTimeMillis();        GoogleAuthenticator ga = new GoogleAuthenticator();        boolean r = ga.check_code(savedSecret, code, t);        System.out.println("验证结果:" + r);    }}

说明:将会生成一个二维码地址,打开便是一张二维码图片,进行GoogleAuthenticator配置;可替换code值进行测试~


如果有所收获,欢迎点赞转发,要是有疑问,欢迎留言讨论~