Java 接入 Google Authenticator

在网络攻击日益泛滥的今天, 用户的密码可能会因为各种原因泄漏. 而一些涉及用户重要数据的服务, 如 QQ, 邮箱, 银行, 购物等等. 一但被有心人利用, 那么除了自己隐私泄漏的风险外, 还存在自己身份被冒充的危害, 更有可能而导致极其严重的结果. 为此谷歌推出了 Google Authenticator 服务, 其原理是在登录时除了输入密码外, 还需根据 Google Authenticator APP 输入一个实时计算的验证码. 凭借此验证码, 即使在密码泄漏的情况下, 他人也无法登录你的账户

相关原理

Google Authenticator 使用了一种基于 ** 时间 ** 的 TOTP 算法, 其中时间的选取为自 1970-01-01 00:00:00 以来的毫秒数除以 30 与 客户端及服务端约定的 ** 密钥 ** 进行计算, 计算结果为一个 **6 位数的字符串 *( 首位数可能为 0, 所以为字符串 *), 所以在 Google Authenticator 中我们可以看见验证码每个 30 秒就会刷新一次. 更多详情可查看 Google 账户两步验证的工作原理 一文

实现思路

由上可知, 生成验证码有俩个重要的参数, 其一为 ** 客户端与服务端约定的密钥 **, 其二便为 **30 秒的个数 **

/**
 * 随机生成一个密钥
 */
public static String createSecretKey() {
    SecureRandom random = new SecureRandom();
    byte[] bytes = new byte[20];
    random.nextBytes(bytes);
    Base32 base32 = new Base32();
    String secretKey = base32.encodeToString(bytes);
    return secretKey.toLowerCase();
}
//1970-01-01 00:00:00 以来的毫秒数除以 30 
long time = System.currentTimeMillis() / 1000 / 30;

根据这两个参数就可以生成一个验证码

/**
 * 根据密钥获取验证码
 * 返回字符串是因为验证码有可能以 0 开头
 * @param secretKey 密钥
 * @param time      第几个 30 秒 System.currentTimeMillis() / 1000 / 30
 */
public static String getTOTP(String secretKey, long time) {
    Base32 base32 = new Base32();
    byte[] bytes = base32.decode(secretKey.toUpperCase());
    String hexKey = Hex.encodeHexString(bytes);
    String hexTime = Long.toHexString(time);
    return TOTP.generateTOTP(hexKey, hexTime, "6");
}

因为 Google Authenticator(* 以下简称 APP*) 计算验证码也需要 ** 密钥 ** 的参与, 而时间 APP 则会在本地获取, 所以我们需要将 ** 密钥保存在 APP 中 **, 同时为了与其他账户进行区分, 除了密钥外, 我们还需要录入 ** 服务名称 , 用户账户 ** 信息. 而为了方便用户信息的录入, 我们一般将所有信息生成一张二维码图片, 让用户通过扫码自动填写相关信息

/**
 * 生成 Google Authenticator 二维码所需信息
 * Google Authenticator 约定的二维码信息格式 : otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}
 * 参数需要 url 编码 + 号需要替换成 %20
 * @param secret  密钥 使用 createSecretKey 方法生成
 * @param account 用户账户 如: [email protected] 138XXXXXXXX
 * @param issuer  服务名称 如: Google Github 印象笔记
 */
public static String createGoogleAuthQRCodeData(String secret, String account, String issuer) {
    String qrCodeData = "otpauth://totp/%s?secret=%s&issuer=%s";
    try {
        return String.format(qrCodeData, URLEncoder.encode(issuer + ":" + account, "UTF-8").replace("+", "%20"), URLEncoder.encode(secret, "UTF-8")
                .replace("+", "%20"), URLEncoder.encode(issuer, "UTF-8").replace("+", "%20"));
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    return "";
}

此时再根据上述信息生成二维码, 二维码生成方式可参考以下两种方案

  • 生成二维码之 Java (Google zxing) 篇
  • 生成二维码之 JavaScript (jquery-qrcode) 篇
    如何选择可查看 关于二维码生成的一些思考

此时选择使用 Java 的方式返回一个二维码图片流

/**
* 将二维码图片输出到一个流中
* @param content 二维码内容
* @param stream  输出流
* @param width   宽
* @param height  高
*/
public static void writeToStream(String content, OutputStream stream, int width, int height) throws WriterException, IOException {
  BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
  MatrixToImageWriter.writeToStream(bitMatrix, format, stream);
}

扫描二维码

Java 接入 Google Authenticator_第1张图片
扫描二维码

扫描成功后会新增一栏验证码信息
Java 接入 Google Authenticator_第2张图片
扫描后新增一栏信息

再让用户输入验证码, 与服务端进行校验, 如果校验通过, 则表明用户可以完好使用该功能
因为验证码是使用基于时间的 TOTP 算法, 依赖于客户端与服务端时间的一致性. 如果客户端时间与服务端时间相差过大, 那在用户没有同步时间的情况下, 永远与服务端进行匹配. 同时服务端也有可能出现时间偏差的情况, 这样反而导致时间正确的用户校验无法通过
为了解决这种情况, 我们可以使用 ** 时间偏移量 ** 来解决该问题, Google Authenticator 验证码的时间参数为 1970-01-01 00:00:00 以来的毫秒数除以 30, 所以每 30 秒就会更新一次. 但是我们在后台进行校验时, 除了与当前生成的二维码进行校验外, 还会对当前时间参数 ** 前后偏移量 ** 生成的验证码进行校验, 只要其中任意一个能够校验通过, 就代表该验证码是有效的

/** 时间前后偏移量 */
private static final int timeExcursion = 3;
/**
 * 校验方法
 * @param secretKey 密钥
 * @param code      用户输入的 TOTP 验证码
 */
public static boolean verify(String secretKey, String code) {
    long time = System.currentTimeMillis() / 1000 / 30;
    for (int i = -timeExcursion; i <= timeExcursion; i++) {
        String totp = getTOTP(secretKey, time + i);
        if (code.equals(totp)) {
            return true;
        }
    }
    return false;
}

其他说明

根据以上代码我们可以简单的创建一个 Google Authenticator 的应用. 但是与此同时, 我们也发现 Google Authenticator 严重依赖手机, 又因为 Google Authenticator ** 没有同步功能 **, 所以如果用户一不小心删除了记录信息, 或者 APP 被卸载, 手机系统重装等情况. 就会导致 Google Authenticator 成为使用者的障碍. 此时我们可以使用 Authy 这款支持 ** 同步功能 ** 的 APP 以解决删除, 卸载, 重装等问题. 同时 Authy 也存在 Chrome 插件 版本, 用于解决在手机丢失的情况下获取验证码.
除了 Authy 这个选择外, 我们还可以使用 ** 备用验证码 ** 的机制用户用于解决上述问题. 即在用户绑定 Google Authenticator 成功后自动为用户生成多个 ** 备用验证码 **, 然后在前台显示. 并让用户进行保存, 再让用户使用备用验证码进行校验, 以确保用户保存成功, 可以参考 ** 印象笔记 ** 的用法 如何开启印象笔记登录两步验证?

以上所使用的代码可在 Google-Authenticator 中查看

另外本文同时参考了以下资料

  • Google Authenticator compatible 2-Factor Auth in Java
  • twofactorauth
  • Google Authenticator JAVA 实例
  • Google 账户两步验证的工作原理
  • GoogleAuth

你可能感兴趣的:(Java 接入 Google Authenticator)