双因素认证(2FA)教程

作者: 阮一峰

日期: 2017年11月 2日

所谓认证(authentication)就是确认用户的身份,是网站登录必不可少的步骤。

密码是最常见的认证方法,但是不安全,容易泄露和冒充。

越来越多的地方,要求启用双因素认证(Two-factor authentication,简称 2FA)。本文介绍它的概念和实现方法。

文章结尾有一则活动消息优达学城(Udacity)的"双十一优惠",课程最高减免1111元。

一、双因素认证的概念

一般来说,三种不同类型的证据,可以证明一个人的身份。

  • 秘密信息:只有该用户知道、其他人不知道的某种信息,比如密码。
  • 个人物品:该用户的私人物品,比如身份证、钥匙。
  • 生理特征:该用户的遗传特征,比如指纹、相貌、虹膜等等。

这些证据就称为三种"因素"(factor)。因素越多,证明力就越强,身份就越可靠。

双因素认证就是指,通过认证同时需要两个因素的证据。

银行卡就是最常见的双因素认证。用户必须同时提供银行卡和密码,才能取到现金。

二、双因素认证方案

常用的双因素组合是密码 + 某种个人物品,比如网上银行的 U 盾。用户插上 U 盾,再输入密码,才能登录网上银行。

但是,用户不可能随时携带 U 盾,手机才是最好的替代品。密码 + 手机就成了最佳的双因素认证方案。

国内的很多网站要求,用户输入密码时,还要提供短消息发送的验证码,以证明用户确实拥有该手机。

但是,短消息是不安全的,容易被拦截和伪造,SIM 卡也可以克隆。已经有案例,先伪造身份证,再申请一模一样的手机号码,把钱转走。

因此,安全的双因素认证不是密码 + 短消息,而是下面要介绍的 TOTP

三、TOTP 的概念

TOTP 的全称是"基于时间的一次性密码"(Time-based One-time Password)。它是公认的可靠解决方案,已经写入国际标准 RFC6238

它的步骤如下。

第一步,用户开启双因素认证后,服务器生成一个密钥。

第二步:服务器提示用户扫描二维码(或者使用其他方式),把密钥保存到用户的手机。也就是说,服务器和用户的手机,现在都有了同一把密钥。

注意,密钥必须跟手机绑定。一旦用户更换手机,就必须生成全新的密钥。

第三步,用户登录时,手机客户端使用这个密钥和当前时间戳,生成一个哈希,有效期默认为30秒。用户在有效期内,把这个哈希提交给服务器。

第四步,服务器也使用密钥和当前时间戳,生成一个哈希,跟用户提交的哈希比对。只要两者不一致,就拒绝登录。

五、TOTP 的算法

仔细看上面的步骤,你可能会有一个问题:手机客户端和服务器,如何保证30秒期间都得到同一个哈希呢?

答案就是下面的公式。


TC = floor((unixtime(now) − unixtime(T0)) / TS)

上面的公式中,TC 表示一个时间计数器,unixtime(now)是当前 Unix 时间戳,unixtime(T0)是约定的起始时间点的时间戳,默认是0,也就是1970年1月1日。TS 则是哈希有效期的时间长度,默认是30秒。因此,上面的公式就变成下面的形式。


TC = floor(unixtime(now) / 30)

所以,只要在 30 秒以内,TC 的值都是一样的。前提是服务器和手机的时间必须同步。

接下来,就可以算出哈希了。


TOTP = HASH(SecretKey, TC)

上面代码中,HASH就是约定的哈希函数,默认是 SHA-1。

TOTP 有硬件生成器和软件生成器之分,都是采用上面的算法。

(说明:TOTP 硬件生成器)

(说明:Google Authenticator 是一个生成 TOTP 的手机 App)

五、TOTP 的实现

TOTP 很容易写,各个语言都有实现。下面我用 JavaScript 实现2fa来演示一下真实代码。

首先,安装这个模块。


$ npm install --save 2fa

然后,生成一个32位字符的密钥。


var tfa = require('2fa');

tfa.generateKey(32, function(err, key) {
  console.log(key);
});
// b5jjo0cz87d66mhwa9azplhxiao18zlx

现在就可以生成哈希了。


var tc = Math.floor(Date.now() / 1000 / 30);
var totp = tfa.generateCode(key, tc);
console.log(totp); // 683464

六、总结

双因素认证的优点在于,比单纯的密码登录安全得多。就算密码泄露,只要手机还在,账户就是安全的。各种密码破解方法,都对双因素认证无效。

缺点在于,登录多了一步,费时且麻烦,用户会感到不耐烦。而且,它也不意味着账户的绝对安全,入侵者依然可以通过盗取 cookie 或 token,劫持整个对话(session)。

双因素认证还有一个最大的问题,那就是帐户的恢复。

一旦忘记密码或者遗失手机,想要恢复登录,势必就要绕过双因素认证,这就形成了一个安全漏洞。除非准备两套双因素认证,一套用来登录,另一套用来恢复账户。

七、参考链接

(正文完)

====================================

从业两三年后,程序员往往遇到职业瓶颈,80%的人都把时间耗费在熬夜加班、修 Bug,只有少数人选择业余时间精进技术,提升自己的潜力,突破薪资天花板。

优达学城(Udacity)作为来自硅谷的前沿技术学习平台,帮你掌握前沿技术。

它的课程和项目,来自Google、Facebook等硅谷名企,并提供人工审阅、一对一在线答疑等服务,拒绝浪费时间走弯路。

今年双十一,与其囤积一年都用不完的便宜货,不如来优达学城投资未来提升自我。11月1日~11月11日,课程全场最高减¥1111,让你轻松享有硅谷学习资源!

优惠席位有限,先到先得,点击这里了解详情。

(完)

留言(44条)

浅显易懂,Thumbs up!

每次使用都比较好奇居然没有被墙。。。。

赞,之前twitter看提到英文文章,一直没来得及看

看您的博客之后 发现一个真理哈 就是从一个小白到一个小白的过渡 这个小白不是说自己看了没效果 只是会发现自己真的是很渺小 对知识永远保持一颗谦卑的心

这个博客比王垠的强多了

有一种小黄车就是基于 TOTP 算法的吧。4个按钮的那种小黄车。

入侵者依然可以通过盗取 cookie 或 token,劫持整个对话(session)。我最担心浏览器出卖个人信息了!

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

引用baixiangcpp的发言:

有一种小黄车就是基于 TOTP 算法的吧。4个按钮的那种小黄车。


那个是固定的吧,出厂就不会变了。

引用潞潞的发言:


那个是固定的吧,出厂就不会变了。

不是机械锁,有一种“半智能”锁,深圳是有的

引用刘世理的发言:

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

你没看懂。
举个例子,客户端算tc的时候使用的当前时间是0点0分0秒,算出一个tc值,服务器端在0点0分到0点0分30秒内算出来的tc都会和客户端一样。超出30s后算出来就不一样了,也就是过期了,验证就失败了。
你那29,31是个啥,那儿是时间戳,远大于30的值

@但丁:

如果客户端请求的时间为0点0分29秒呢。服务器在0点0分59秒算出了来的和客户端是不一致的。但是也在30秒以内。你说呢。

引用王韸的发言:

@但丁:

如果客户端请求的时间为0点0分29秒呢。服务器在0点0分59秒算出了来的和客户端是不一致的。但是也在30秒以内。你说呢。


这是基于时间的。 你手机不联网 都可以的。 只要时间是同步的。

硬件生成器那种也没联网, 也能保证 某一个时间段内密码一致。
一般的都是30秒,或者60秒内 的 算法保证 密码一致.

引用刘世理的发言:

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

是的,其实你用一下这个2FA 相关的应用,就知道了!
每隔30s 会重新计算一次TOTP 。比如在0:0:29的时候当前密钥的有效时间就只剩下1秒了。

SIM 短信息 也是可以用TOTP 2FA的,这不是短信不安全的原因吧

时间的严格同步其实并不容易做到,但是现实中的算法应该是平衡了时差的因素,即使错过一个时间窗仍然可以认证成功。

实际是服务器会计算当前的和之前几个时间窗口里的验证码,只要你输入的符合其中任何一个就认为你通过了.
支持这种协议的应用不只有google的验证器,(如微软的,或者authy) 而且计算验证码不需要网络,所以也不存在被墙的概念.
小黄车四位电子密码锁, 银行的电子密令牌(自动生成6位验证码的那种)应该都是类似的方法.

本质上还是通过一个密码和当前时间来生成动态验证码,所以那个密码丢失也就没有安全性可言.不过使用过程中,并不会直接键入这个密码,通过生成的验证码和时间也不能反推原密码,所以还是很安全的.
两步验证的安全性并不是绝对的,只是多一步验证,假设该步验证丢失的概率1/n,那么可以认为安全性提高了n倍.

网易游戏的将军令,应该也是这个原理吧,零几年就有了。

引用王韸的发言:

@但丁:

如果客户端请求的时间为0点0分29秒呢。服务器在0点0分59秒算出了来的和客户端是不一致的。但是也在30秒以内。你说呢。


很简单,过期啊。。重新来吧

宁盾令牌就是这个了。

将军令

引用但丁的发言:

你没看懂。
举个例子,客户端算tc的时候使用的当前时间是0点0分0秒,算出一个tc值,服务器端在0点0分到0点0分30秒内算出来的tc都会和客户端一样。超出30s后算出来就不一样了,也就是过期了,验证就失败了。
你那29,31是个啥,那儿是时间戳,远大于30的值



你是看懂了,但你没动手验证过,这个算法没法保证在30s内是相同的
举个例子:
1510279844
1510279845

这2个时间戳只差了1s,但两者除以30四舍五入后并不相等,望修正

峰哥 威武

引用刘世理的发言:

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

你理解错了,30秒内是指0~29之间,不包括30秒,30秒已经过时。

这个让我想起了某个游戏常用的一个叫做将军令的东西,

垃圾暴雪,毁我青春。

明白了。上次记得在你微博里预告了这个内容。

手机的时间和服务端可能并不一致,这样算出来的值就不会相等了

默认的算法是HMAC-SHA-1而不是SHA-1,https://tools.ietf.org/html/rfc6238#section-1.2

引用Unreal的发言:

手机的时间和服务端可能并不一致,这样算出来的值就不会相等了

是的,所以需要其他渠道校对时间,一般上现在的授时精度都比较高了。而且也可以允许一部分的时间偏差 https://tools.ietf.org/html/rfc6238#section-6

最反感三家基础电信运营商的网厅强制用户短信双因素验证了。打着安全的幌子强迫用户,不然用户就用不了网厅。

服务器将将秘钥发给用户,这样处理没有问题么?

引用百年目标第一季的发言:

每次使用都比较好奇居然没有被墙。。。。

沒網都能用你說爲什麼沒有被牆 :-)

密钥必须跟手机绑定. 怎么绑定的?

我也想问:
1.怎么确保密钥和手机绑定?用手机UUID?iOS设备目前是拿不到真正的UUID的
2.怎么确保保存在手机上的密钥是安全的?用另一套加密?

引用chenzw的发言:


你是看懂了,但你没动手验证过,这个算法没法保证在30s内是相同的
举个例子:
1510279844
1510279845

这2个时间戳只差了1s,但两者除以30四舍五入后并不相等,望修正

floor是向下取整

引用aaaa的发言:

密钥必须跟手机绑定. 怎么绑定的?

每个移动设备都有个IMEI

引用六个核桃的发言:

我也想问:
1.怎么确保密钥和手机绑定?用手机UUID?iOS设备目前是拿不到真正的UUID的
2.怎么确保保存在手机上的密钥是安全的?用另一套加密?

其实大可不必纠结于“怎么用手机的唯一特征码实现与密钥绑定”,作者本意是想说明密钥重要性,防范用户不妥善保管导致密钥泄露。其实在每一次发行密钥(二维码)时均更新密钥,同时将旧密钥作废,即可杜绝用户更换手机时,从旧手机泄露密钥的引患,当然更换新密钥前须先行验证TOTP一次性密码,以确保形成安全闭环。

引用六个核桃的发言:

我也想问:
1.怎么确保密钥和手机绑定?用手机UUID?iOS设备目前是拿不到真正的UUID的
2.怎么确保保存在手机上的密钥是安全的?用另一套加密?

关于第二个问题,只能”假定手机是用户私密设备不会被第三方窥见“,银行硬件U盾同样也是这个假定,如果无此假定可能性就多了去了,黑客打车到用户家偷走手机等。其实2FA原理上讲单单泄露密钥,在无账户与密码时对黑客也无任何利用价值,即便如此有比较注重安全的TOTP客户端APP厂商也会有加密、脱敏、匿名等安全措施。所以在这个问题上不必太过焦虑!

没明白,为什么不能由服务器发送一个时间或者客户端发送请求的时候绑定时间戳呢?这样不就可以保证时间统一了么?是因为麻烦一些更消耗内存和带宽么?但只有在登陆以及改密码的时候需要发送请求,而且可以免去服务端容错的计算比较多次且还是可能出错呀

引用an的发言:

没明白,为什么不能由服务器发送一个时间或者客户端发送请求的时候绑定时间戳呢?这样不就可以保证时间统一了么?是因为麻烦一些更消耗内存和带宽么?但只有在登陆以及改密码的时候需要发送请求,而且可以免去服务端容错的计算比较多次且还是可能出错呀

这个问题问得好。
JWT 登录验证就需要发送时间到服务端进行验证。
2FA为什么一定要用自己的时间?

其实没那么严格,上一个token也能用,很多手机也不是完全时间准确的

引用chenzw的发言:


你是看懂了,但你没动手验证过,这个算法没法保证在30s内是相同的
举个例子:
1510279844
1510279845

这2个时间戳只差了1s,但两者除以30四舍五入后并不相等,望修正

我实际应用的时候是牺牲了一定的的精度,来解决的:
Math.abs(服务器端生成的时间 - 前端回传的时间) 就算是匹配成功 :)

引用刘世理的发言:

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

每30秒,totp客户端会刷新一次密钥。

引用刘世理的发言:

TC = floor(unixtime(now) / 30)
并不能保证30秒内TC相等,比如floor(29/30)和floor(31/30)

你理解错了,并非是说 (serverTime-30, serverTime+30)这个区间内算出的密钥都一致。
这个30是一个模值,可以理解为 时间戳%30 一致的话,算出来的密钥都是一致的。
totp客户端一般都会有一个刷新时间,每30秒会刷新一次密钥,这个刷新的时刻恰好是 时间戳%30 == 0 的时刻。
当然,30作为步长,也是可以配置的。

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接