前言

自从 GitHub 开启强制 2FA 之后,如今很多应用紧跟着接入了这个功能,两步验证,是当用户在输入密码之后,还需要输入一个一次性的验证码来进行额外的第二次验证。相比于国外环境,国内更喜欢也更普遍的是短信验证码,动不动就发一条短信。而 2FA 的强大在于是可以离线完成的,那么如何接入这个功能呢?其实了解过程和原理之后,实现是非常简单的。

快速体验

先二话不说,先体验一下最终实现的效果,利用 github.com/xlzd/gotp 我们可以非常容易的实现整个过程。

准备一个 APP

首先你需要准备一个 2FA 的 APP,如:Google Authenticator, 1Password, Authy, Microsoft Authenticator 都可以

运行并验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"bufio"
"fmt"
"os"
"time"

"github.com/skip2/go-qrcode"
"github.com/xlzd/gotp"
)

func main() {
// 生成一个随机的密钥
randomSecret := gotp.RandomSecret(16)
randomSecret = "ENRVL5I4WPXURPIJRC7XZAI7U4" // 此处为了测试方便固定密钥

// 生成二维码,此处需要提供用户名称和机构名称
uri := gotp.NewDefaultTOTP(randomSecret).ProvisioningUri("user@linkinstars.com", "LinkinStar")
fmt.Println(uri)
qrcode.WriteFile(uri, qrcode.Medium, 256, "qr.png")

fmt.Print("扫描二维码后,请输入 APP 上的验证码:")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
userInput := scanner.Text()

if gotp.NewDefaultTOTP(randomSecret).Verify(userInput, time.Now().Unix()) {
fmt.Println("验证成功")
} else {
fmt.Println("验证失败")
}
}

运行上面的代码后会生成一个 qr.png 的二维码图片,用 APP 扫码后,输入验证码,验证通过即成功 ✌️

1
2
3
otpauth://totp/LinkinStar:user@linkinstars.com?issuer=LinkinStar&secret=ENRVL5I4WPXURPIJRC7XZAI7U4
扫描二维码后,请输入 APP 上的验证码:221456
验证成功

实际使用

准备参数

  • 用户的唯一标识 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 中。

  1. 用户开启双因素认证后,服务器生成一个密钥。
  2. 服务器要求用户扫描二维码或以其他方式将密钥保存到用户的手机,确保服务器和用户手机拥有相同的密钥
  3. 用户登录时,手机客户端基于该密钥和当前时间戳生成一个哈希,该哈希在 30 秒内有效。用户需在有效期内将哈希提交给服务器。注意,密钥与用户手机绑定,更换手机时需要生成新密钥
  4. 服务器也使用密钥和当前时间戳生成一个哈希,与用户提交的哈希进行比对。只有在两者一致时,用户才能成功登录。

RFC6238 规定了以下实现细节:

  • 生成任意字节的密钥 K,并与客户端安全地共享。
  • 基于 T0 协商后,Unix 时间从时间间隔(TI)开始计算时间步骤,TI 用于计算计数器 C(默认情况下,TI 的值为 T0 和 30 秒)。
  • 协商加密哈希算法(默认为 SHA-1)。
  • 协商密码长度(默认为 6 位)。

所以原理其实说起来也简单,就是通过时间和密钥做了 hash,并且以 30 秒 为界限。故最重要的一点就是需要保证服务器时间和客户端时间是一致(几乎)的。当然,绝大多数情况下是一致的。

最后注意点

最好需要设计一个恢复码,因为开启 2FA 之后没有手机在旁边的时候是无法使用的,万一手机因为意外丢失,则永远无法登录使用了。那么恢复码可以临时让我们使用并进入应用从而避免意外。其主要的作用是为了以防万一和将责任推给用户。