前言
在前一段时间想和小伙伴一起玩游戏,出于一系列的原因,只能使用远程控制软件来玩。搜了一下Parsec
不错。但是需要2FA
认证。那就认证吧,按步骤一步一步来,一直到要通过这两个平台下东西。那不是完蛋了,不玩了!
过了没几天,登录GitHub
也强制需要2FA
认证,游戏可以不玩,GitHub
不能不用!去研究了一下。
原来是叫做双因素认证
(Two-Factor Authentication),简称2FA
。有几种方式来实现
- 基于时间的一次性密码(TOTP)
- 短信验证码
- 硬件令牌
Parsec
和GitHub
都是使用TOTP
来实现认证。在web
端可以通过浏览器插件--身份验证器
来帮助我们认证
TOTP
(Time-Based One-Time Password,基于时间的一次性密码)是一种基于时间的动态密码生成算法。 算法主要基于HMAC
(Hash-based Message Authentication Code)和一个时间戳来生成一次性密码。在不同的设备上,只要确保密钥正确
、时间同步或者相差不大
的情况下,生成的一次性密码是一致的
。这是TOTP
可以用于认证的关键原因OTP
(One-Time Password,一次性密码),在一定时间周期(通常为30秒)内生成一个一次性密码(通常为6位),即使密码被截获,也无法在后续的认证中再次使用。
后面有实践的完整代码地址
正文
2FA流程
在需要进行2FA
时,第一次登录时,服务器会提供一个二维码,用户使用支持TOTP
的应用程序扫描这个二维码,应用程序就会保存这个密钥,之后程序会根据时间和密钥生成一次性密码
,并且会在一定周期内刷新密码
,用户输入账号和账号密码无误后再输入一次性密码
,一次性密码
正确则登录成功。之后的登录服务器无需再提供二维码,用户直接使用账号
、账号密码
和在应用程序内一定时间刷新的一次性密码
进行验证登录即可。
GitHub
还提供了一次性密码
弄丢(程序卸载)的备用方案,在你第一次登录成功时,会叫你保存一个github-recovery-codes.txt
的文件,用这个文件里面的Code
也能登录,然后重新进行认证。
纯前端现实生成密钥二维码和解析一次性密码
密钥二维码是由后端提供的,我这边不想去搭后端的环境,就直接前端实现了
。
先来看一下效果
需要用到的几个库,
@otplib/preset-browser //生成密钥、OTP URI(后面解释)和生成一次性密码
qrcode //生成二维码
html5-qrcode //解析二维码
生成二维码
首先使用@otplib/preset-browser
的generateSecret
方法生成密钥,再将密钥通过keyuri
得到OTP URI
。最后通过qrcode
的toDataURL
方法生成二维码图片
OTP URI
是一种标准格式,遵循otpauth://
方案。它包含了所有必要的信息,如用户的账户名、服务提供商的名称和密钥。这个格式被广泛支持,可以确保各种TOTP
应用(如 Google Authenticator、Authy)能够识别和正确配置。
- Label: 通常是
账户名@服务提供商
,用于标识具体的账户。- Secret: 用户和服务器共享的密钥,用于生成
OTP
。- Issuer: 用于标识服务提供商(应用程序名称)。
- 其他可选参数: 可能包括时间步长(period)、数字位数(digits)、算法(algorithm)等
关键实现代码
import React, { useEffect, useRef, useState } from 'react';
import { authenticator } from '@otplib/preset-browser';
import QRCode from 'qrcode';
import { Button, Upload, UploadFile } from 'antd';
//配置 OTP URI
authenticator.options = {
digits: 6, //一次性密码的长度。默认值为 6。
step: 30, // 有效期限(以秒为单位)。默认值为 30。
algorithm: 'sha1' //sha1(默认) sha256 sha512
};
export default function Index() {
const [url, setUrl] = useState('');
const handleClick = () => {
//随机的字符串
const secret = authenticator.generateSecret();
//生成一个OTP URI
const otpauth = authenticator.keyuri('user@example.com', 'My App', secret);
console.log('otpauth:', otpauth);
QRCode.toDataURL(otpauth, (err: any, imageUrl: string) => {
if (err) {
console.error('Error generating QR code', err);
return;
}
setUrl(imageUrl);
console.log('QR code image URL:', imageUrl);
});
};
return (
<>
<div
style={{
height: '200px',
}}
></div>
<h2>生成二维码</h2>
<section>
{url && (
<img
style={{
height: '200px',
width: '200px',
}}
src={url}
alt="QR code"
/>
)}
<div>
<Button onClick={handleClick}>点击生成二维码</Button>
</div>
</section>
</>
);
}
解析生成的二维码
通过html5-qrcode
创建扫描器,并解析二维码得到密钥
,最后通过@otplib/preset-browser
的generate
方法得到一次性密码
关键实现代码
import React, { useEffect, useRef, useState } from 'react';
import { authenticator } from '@otplib/preset-browser';
import QRCode from 'qrcode';
import { Button, Upload, UploadFile } from 'antd';
import { Html5QrcodeScanner, Html5QrcodeScanType } from 'html5-qrcode';
authenticator.options = {
digits: 6, //一次性密码的长度。默认值为 6。
step: 30, // 有效期限(以秒为单位)。默认值为 30。
algorithm: 'sha1' //sha1(默认) sha256 sha512
};
export default function Index() {
/** 密钥*/
const secret = useRef('');
return (
<>
<h2>解析二维码</h2>
<div id="scanFile" ></div>
<section>
<div>
<Button
onClick={() => {
//创建扫描器
let reader = new Html5QrcodeScanner(
'scanFile',
{
fps: 10,
qrbox: { width: 150, height: 150 },
supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_FILE],
},
false
);
//进行解析二维码
reader.render(data => {
const urlObj = new URL(data);
const label = decodeURIComponent(urlObj.pathname.replace(/^\/+/, ''));
const params = Object.fromEntries(urlObj.searchParams.entries());
//获取密钥
secret.current = params.secret;
});
}}
>
上传图片
</Button>
<Button
onClick={() => {
//解析密钥得到一次性密码
const otp = authenticator.generate(secret.current);
// 一次性密码
console.log(otp, 'otp');
}}
>
获取OTP
</Button>
</div>
</section>
</>
);
}
结语
感兴趣的可以去试试
仓库地址: gitee.com/lin-zhiteng…