Golang 快速接入两步验证 2FA
前言
自从 GitHub 开启强制 2FA 之后,如今很多应用紧跟着接入了这个功能,两步验证,是当用户在输入密码之后,还需要输入一个一次性的验证码来进行额外的第二次验证。相比于国外环境,国内更喜欢也更普遍的是短信验证码,动不动就发一条短信。而 2FA 的强大在于是可以离线完成的,那么如何接入这个功能呢?其实了解过程和原理之后,实现是非常简单的。
快速体验
先二话不说,先体验一下最终实现的效果,利用 github.com/xlzd/gotp
我们可以非常容易的实现整个过程。
准备一个 APP
首先你需要准备一个 2FA 的 APP,如:Google Authenticator, 1Password, Authy, Microsoft Authenticator 都可以
运行并验证
1 | package main |
运行上面的代码后会生成一个 qr.png 的二维码图片,用 APP 扫码后,输入验证码,验证通过即成功 ✌️
1 | otpauth://totp/LinkinStar:user@linkinstars.com?issuer=LinkinStar&secret=ENRVL5I4WPXURPIJRC7XZAI7U4 |
实际使用
准备参数
- 用户的唯一标识 email,通常可以是邮箱
- 机构名称 appName,也就是分发这个 2FA 的应用厂商,通常是你应用的名称
- 一个随机密钥 secret,可以使用
gotp.RandomSecret(16)
生成,每个用户一个,注意存储
过程
用户初次绑定,使用上述三个参数生成一个 URI。
1 | uri := gotp.NewDefaultTOTP(secret).ProvisioningUri(email, appName) |
内容大致为:otpauth://totp/LinkinStar:user@linkinstars.com?issuer=LinkinStar&secret=ENRVL5I4WPXURPIJRC7XZAI7U4
然后用这个 URI 生成一个二维码,供用户扫码绑定并输入验证码进行验证。
注意,此处的二维码仅仅展示一次,一旦验证通过,如不必要不进行展示,再次展示也需要做验证
最后验证是否正确即可
1 | gotp.NewDefaultTOTP(randomSecret).Verify(userInput, time.Now().Unix()) |
之后用户每次登录,需要验证 2FA 时,仅仅输入验证码,然后进行验证即可
简述原理
其本质是利用了 TOTP,全称为”基于时间的一次性密码”(Time-based One-time Password),已被纳入国际标准 RFC6238 中。
- 用户开启双因素认证后,服务器生成一个密钥。
- 服务器要求用户扫描二维码或以其他方式将密钥保存到用户的手机,确保服务器和用户手机拥有相同的密钥。
- 用户登录时,手机客户端基于该密钥和当前时间戳生成一个哈希,该哈希在 30 秒内有效。用户需在有效期内将哈希提交给服务器。注意,密钥与用户手机绑定,更换手机时需要生成新密钥。
- 服务器也使用密钥和当前时间戳生成一个哈希,与用户提交的哈希进行比对。只有在两者一致时,用户才能成功登录。
RFC6238 规定了以下实现细节:
- 生成任意字节的密钥 K,并与客户端安全地共享。
- 基于 T0 协商后,Unix 时间从时间间隔(TI)开始计算时间步骤,TI 用于计算计数器 C(默认情况下,TI 的值为 T0 和 30 秒)。
- 协商加密哈希算法(默认为 SHA-1)。
- 协商密码长度(默认为 6 位)。
所以原理其实说起来也简单,就是通过时间和密钥做了 hash,并且以 30 秒 为界限。故最重要的一点就是需要保证服务器时间和客户端时间是一致(几乎)的。当然,绝大多数情况下是一致的。
最后注意点
最好需要设计一个恢复码,因为开启 2FA 之后没有手机在旁边的时候是无法使用的,万一手机因为意外丢失,则永远无法登录使用了。那么恢复码可以临时让我们使用并进入应用从而避免意外。其主要的作用是为了以防万一和将责任推给用户。