​ 我以为我最初遇见他是在宝塔面板上,因为他可以方便的帮助我们进行身份验证。其实我们早就相遇在QQ安全中心手机版的口令里面(此处不确定是否是使用同一种算法,不过原理类似)。当初遇见他,我并不知道他是离线的。我以为谷歌身份验证器肯定是绑定谷歌账号的。后来找了半天,原来他只是个离线的软件。相信有很多同学和我一样的想法:离线身份验证器如何能使我们登录在线的场景?

​ 身份验证器是谷歌的产品。之前版本有开源仓库 https://github.com/google/google-authenticator。由于本人水平有限,本文使用第三方人员写的php实现方法来进行演示。https://github.com/PHPGangsta/GoogleAuthenticator

​ 首先我们可以看到仓库给出的演示代码:(以我练习两年半的水平加上了注释)

<?php
require_once 'PHPGangsta/GoogleAuthenticator.php';

$ga = new PHPGangsta_GoogleAuthenticator();
$secret = $ga->createSecret();
//生成秘钥并且输出
echo "Secret is: ".$secret."\n\n";

$qrCodeUrl = $ga->getQRCodeGoogleUrl('Blog', $secret);
echo "Google Charts URL for the QR-Code: ".$qrCodeUrl."\n\n";
//用这个秘钥生成一个二维码,方便手机录入,这里有一个自声明的schema
//otpauth://totp/infoATphpgangsta.de%3Fsecret%3DOQB6ZZGYHCPSX4AK 有info 有secret信息
$oneCode = $ga->getCode($secret);
//通过秘钥生成验证码(就是身份验证器实时显示的数字)
echo "Checking Code '$oneCode' and Secret '$secret':\n";
//通过秘钥和验证码进行身份验证。
$checkResult = $ga->verifyCode($secret, $oneCode, 2);    // 2 = 2*30sec clock tolerance
if ($checkResult) {
    echo 'OK';
} else {
    echo 'FAILED';
}

至此,我们已经有了身份验证器大致的工作流程:

服务器生成秘钥,分发给客户。客户有此秘钥就可以实时生成验证码,服务端根据此客户提供的验证码来和自己所存储的秘钥进行验证。验证通过既登陆成功。

既然如此,我们就直接从verifyCode入手,看他是如何验证的。

    public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
    {
        if ($currentTimeSlice === null) {
            $currentTimeSlice = floor(time() / 30);
        }
        // 如果时间没有指定的话,这个参数就是当前的时间/30.这就意味着我们的验证码的有效期是30S

        if (strlen($code) != 6) {
            return false;
        }
        //我们传入的验证码长度必须是6位数

        for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
            $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);//根据提供的秘钥和时间,获取验证码。此处的时间是真实时间/30后得到的。按照参数名字来看,应该叫做当前时间切片?
            if ($this->timingSafeEquals($calculatedCode, $code)) {
                return true;
            }
        }
        // 上面的循环是一个容错机制。函数入口里面的时间/30,已经指明验证码是30S的有效期,但是服务端校验时候会把当前时间段左右个两个30秒(调用verifyCode的第三个参数)都去获取code,这样用户可以更`慢`的输入验证码,更方便验证。也可以防止网络波动性问题。不过我个人觉得,2这个有些太放纵用户了。干脆设置为1,更干脆直接不设置这个循环。失效就失效,让用户重新输入。

        return false;
    }
$checkResult = $ga->verifyCode($secret, $oneCode, 2); 

当然,这一切都基于

$this->getCode($secret);

服务器端这两者的算法是相同的。并且是不可逆的。如果确实感兴趣。可以更加深一步的查看相关函数方法。如果不感兴趣的话,就只需要知道 :身份验证器是基于时间和秘钥,就可以了。