近期管理后台有不明人士在登录,查询后台日志只能查到一个IP,其他日志因为时间的原因,已经被清除。后台其实已经做了很多防护,可以避免不法分子攻击,例如账号密码加密传输、频繁登录会被封禁、密码错误3次也会被封禁等。但为了能让后台更加安全,遂试着加上了F2A校验。
F2A(Two-Factor Authentication)其实是2FA的笔误,都表示双因素认证。它是一种安全验证机制,要求用户在登录时提供两种不同类型的证据(因素)来证明身份。
在实际应用中,F2A/2FA 最常见的表现形式是:
- 密码 + 短信验证码:最普遍但安全性相对较低(易受 SIM 卡交换攻击)。
- 密码 + 基于时间的一次性密码( the Time-Based One-time Password ,TOTP):如 Google Authenticator、Microsoft Authenticator 等应用生成的 6-8 位动态数字。
- 密码 + 硬件密钥:如 YubiKey,安全性最高,能有效防御网络钓鱼。
我们的后台将会采用第二种表现形式,后台服务端基于 Node.js 实现。核心实现原理是基于时间同步的一次性密码(TOTP,Time-based One-Time Password)算法,通过“共享密钥”和“时间”两个要素,在用户设备与服务器之间生成相同的动态验证码。即服务器和你的手机APP,各自使用同一个密钥和当前时间,通过相同的算法计算,得出一串相同的数字。
一、生成密钥
1)speakeasy.js
Node的生态提供了speakeasy.js库,可以生成F2A的密钥,还有供F2A设备扫描的地址。
npm install --save speakeasy
在项目中安装完成后,就是调用 speakeasy 的生成方法。
router.get( '/get/f2a', async (ctx) => { const secret = speakeasy.generateSecret({ length: 20 }); ctx.body = { code: 0, data: { secret: secret.base32, url: secret.otpauth_url, } }; }, );
secret 就是一串字符,而 url 是个 otpauth 协议的地址,生成二维码后便于设备扫描。
{ "secret": "M4STOUBFGM4UUXJXKZJXS5J4IIWEA3KU", "url": "otpauth://totp/SecretKey?secret=M4STOUBFGM4UUXJXKZJXS5J4IIWEA3KU" }

2)账户绑定密钥
点击上图中的重新绑定按钮,就能将当前 secret 和后台账户绑定起来,在账户表中新增两个字段:otpauthUrl 和 secret。
{ "_id": { "$oid": "5f81288d3578bb005a79cdc1" }, "status": 1, "realName": "测试", "userName": "xx@xx.me", "cellphone": "13800138000", "password": "abcd", "otpauthUrl": "otpauth://totp/SecretKey?secret=PU4WG5RDJVZVCSDHMM5UYSBFEMSEANBQ", "secret": "PU4WG5RDJVZVCSDHMM5UYSBFEMSEANBQ" }
后续在登录输入安全码后,就可进行校验了。
二、绑定应用
1)APP
在应用市场提供了很多的APP,用于显示安全码,界面基本上都是下图这样,例如 authy、2FAS 等。

2)小程序
有个叫二次验证码的小程序,也能用于绑定。

3)飞书小助手
还设计了一种更简便的查询方式。
在机器人的对话框,输入安全码,空格后面跟上自己管理后台的账号邮箱,例如“安全码 xx@xx.com”。
如此,就能得到该账户的最新安全码。

原理就是查表,然后调用 speakeasy 的 totp() 方法,再调用飞书的接口发送消息。
const account = await this.models.BackendUserAccount.findOne({ userName: email }); const token = speakeasy.totp({ secret: account.secret, encoding: 'base32', }); // 发送回复消息 await this.messageService.sendTextMessage(chat_id, token);
不过还有个小问题,就是也能收到别人的安全码。但后台都有查询记录,若出现问题,还能溯源。
三、F2A校验
1)开关
增加一步登录校验,在使用时会增加门槛,所以在通用配置中设计了一个开关。
{ "isOpen": false, "whiteList": [ "yy@yy.com" ] }
只有在 isOpen 为 true 时,才会开启校验。
whiteList 是给特殊账户开启的白名单,不需要走校验,例如给第三方用的账户。
2)登录
登录设计成了两步,两次调用的接口都是 user/login。
第一步还是原先的输入邮箱和密码。
输入完后,去请求登录接口,判断是否需要二次校验,若返回JSON包含 f2a。
{ f2a: true }
则显示弹框,安全码为必填项,点击确定进行二次校验。

下面是登录接口中的部分逻辑,从通用配置中读取开关信息,判断是否弹框,最后校验。
// 判断是否需要F2A安全校验 const configContent = await services.tool.getConfigContent({ key: '1a26f4185f66d6f94ef3897f7a475305' }); if(configContent && configContent.isOpen && configContent.whiteList.indexOf(userName) === -1) { // 未传随机码,说明是第一步校验,直接返回 if(!code) { ctx.body = { f2a: true }; return; } // 校验随机码 const verifyInfo = { secret: account.secret, encoding: 'base32', token: code, } const verify = speakeasy.totp.verify(verifyInfo); if(!verify) { ctx.status = 400; ctx.body = { error: '安全码错误' }; return; } }
3)过渡期
在开启这个功能前,会有一段时间的过渡期,在登录页面增加说明文档。
完善密钥,过渡期后,才会正式开启此功能。

posted on
浙公网安备 33010602011771号