系统登录,一般为用户名、密码认证,或者增加验证码与认证锁定,避免暴力破解。
为了更加安全,可以增加动态一次性口令认证。比如手机验证码、u盾。但这些对接成本比较高,Google推出了TOTP,简单安全,并能在脱机环境使用。
上验证图,验证码每30秒更新一次,很高大上:

TOTP全称为:Time-Based One-Time Password Algorithm,是基于HOTP(HMAC-Based One-Time Password Algorithm)实现的,先大致介绍下HOTP的原理。
两个算法的说明文档可参考:
HOTP:https://datatracker.ietf.org/doc/html/rfc4226
TOTP:https://datatracker.ietf.org/doc/html/rfc6238
HOTP的算法原理,参考官方文档,公式为:
HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))
其中,HMAC-SHA-1可同样换成SHA256,SHA512,MD5, 当前使用SHA25的安全性是足够的。
HMAC为标准的算法,不做过多展开,但Truncate为一种特殊的算法,目的是为了将hmac计算后的20位密文转换成方便使用的6位或8位数字,方便输入。
go有相关的开源代码实现,可参考:github.com/pquerna/otp
算法流程
取hmac后的20位密文最后一位,按16取余,得到0-15的数字,作为偏移量
从偏移量开始,取4个字符(每个字符取1个字节),拼接成一个数字(64位系统中,一个int占用4个字节),同时最高位设置成0,避免结果为负数
基于得到的数字取余。如果是需要6位,则基于1000000取余,8位同理
通过上面算法,基于密钥K,次数C,就算出来了动态验证码。而密钥通过其它方式同步,类似aksk传递方式。而次数C,每次使用后,就会增加。
TOTP的变化
由于次数C的维护比较麻烦,客户端与服务端使用后加1,而如果出现不同步,就需要人工干预,比较麻烦。
针对这个问题,TOTP直接使用unix时间作为C,通过这个网站可以进行时间互换:https://www.unixtimestamp.com/

而由于时间是一直变化的,所以不能单纯的用时间,需要将时间进行转换:C/30 默认是除以30秒,表示动态验证码有效期为30秒 而在验证算法中,服务端有个参数可以控制允许上下浮动区间,如果配置1,则可以上下浮动30秒,有效期就变成了90秒。

先实现一个服务端,用于生成密钥,服务端运行,生成密钥与二维码,等待输入验证:

客户端下载google认证app,并扫码导入,可看到验证码每30秒刷新一次。app不允许截图,拍照有点模糊:

chrome也有相关插件:

服务端验证客户端验码,认证通过。由于我服务端默认开启了偏差1,所以有效期为90秒。

服务端参考代码如下:
package main
import (
"bufio"
"bytes"
"fmt"
"image/png"
"log"
"os"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
func main() {
k, err := totp.Generate(totp.GenerateOpts{
Issuer: "test.com",
AccountName: "testUser",
Period: 30,
SecretSize: 32,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
log.Fatal("gen key failed. ", err)
}
log.Println("gen TOTP: ", k.String())
var buf bytes.Buffer
img, err := k.Image(200, 200)
if err != nil {
log.Fatal("gen image failed ", err)
}
err = png.Encode(&buf, img)
if err != nil {
log.Fatal("gen png failed ", err)
}
err = os.WriteFile("qr-code.png", buf.Bytes(), 0644)
if err != nil {
log.Fatal("write png failed ", err)
}
fmt.Println("qf code written to qf-code.png. ")
fmt.Println("Validating TOTP...")
for {
passcode := promptForPasscode()
valid := totp.Validate(passcode, k.Secret())
if valid {
println("Valid passcode!")
continue
} else {
println("Invalid passcode!", passcode)
continue
}
}
}
func promptForPasscode() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter Passcode: ")
text, _ := reader.ReadString('\n')
return text[:len(text)-1] // remove last \n
}
本期作者丨沃趣科技产品研发部
版权作品,未经许可禁止转载
往期作品快速浏览:








