# 2fa-demo **Repository Path**: NativeBase/2fa-demo ## Basic Information - **Project Name**: 2fa-demo - **Description**: Express + TypeScript + SQLite 实现的网站登录双因素认证(TOTP, RFC 6238) - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-01 - **Last Updated**: 2026-09-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 2FA Demo(TOTP 双因素认证) Express + TypeScript + SQLite 实现的网站登录双因素认证(TOTP, RFC 6238)演示。 ## 一、核心原理(TOTP) TOTP 的核心逻辑非常简单:**共享密钥 + 当前时间 = 动态验证码**。 1. 服务器生成一个随机的 **Secret Key(共享密钥)**,并保存下来。 2. 将这个 Secret Key 通过**二维码**分享给用户的手机 App。 3. 服务器和手机 App 都使用这个 Secret Key 和**当前的时间戳**(通常每 30 秒为一个时间窗口),通过 **HMAC-SHA1** 算法计算出一个 6 位数的验证码。 4. 因为双方的**密钥和时间相同**,所以计算出的验证码**必然一致**。 > 本项目中的实现见 `src/utils/totp.ts`:`floor(时间戳 / 30s)` 作为计数器,对其做 HMAC-SHA1 后动态截断(dynamic truncation)取 6 位数。校验时允许前后各 1 个时间窗口漂移,以容忍手机与服务器之间的时钟误差。 ## 二、完整实现流程 整个流程分为两个阶段:**用户绑定 2FA** 和 **登录时验证 2FA**。 ### 阶段 1:用户开启/绑定 2FA 1. 用户在网站设置中点击"开启 2FA"。 2. 后端生成一个随机的 `Secret Key`(Base32 编码)。 3. 后端将 `Secret Key` **加密后**与用户 ID 绑定,存入数据库。 4. 后端生成一个标准的 `otpauth://` URI(包含网站名、用户名、Secret)。 5. 前端将该 URI 生成**二维码**展示给用户,同时提供**手动输入的密钥文本**(防止用户无法扫码)。 6. 用户用手机 App 扫码后,App 会显示一个 6 位数字。用户将该数字输入网站进行**首次验证**,以确认绑定成功。 > 对应接口:`POST /api/2fa/setup`(步骤 2-5)、`POST /api/2fa/enable`(步骤 6)。 > 注意:首次验证通过前 Secret 只是"暂存"状态(`totp_enabled = 0`),未确认的密钥不会生效。 ### 阶段 2:登录时验证 2FA 1. 用户输入账号、密码(**第一因素**)验证通过。 2. 如果该用户开启了 2FA,前端跳转到 2FA 验证页面,要求输入 6 位验证码。 3. 用户打开手机 App,查看当前的 6 位数字并输入。 4. 后端从数据库取出该用户的 `Secret Key`,结合当前时间计算出**预期的验证码**。 5. 比对用户输入的验证码和预期验证码,一致则登录成功。 > 对应接口:`POST /api/login`(步骤 1-2,已开 2FA 时返回 `{need2fa:true}`,session 中暂存 `pendingUserId`)、`POST /api/login/2fa`(步骤 4-5,验证通过才写入正式登录态 `userId`)。 ## 快速开始 ```bash npm install npm run dev # 开发模式(tsx watch) # 或 npm run build && npm start ``` 打开 http://localhost:3000 ## 使用流程 1. **注册** 一个账号并登录。 2. 在用户中心点击 **开启 2FA**:后端生成随机 Secret Key(Base32),AES-256-GCM 加密后存入 SQLite,并返回二维码 + 手动输入的密钥文本。 3. 用 **Google Authenticator / Microsoft Authenticator / 1Password** 等 App 扫码(或手动输入密钥)。 4. 输入 App 显示的 6 位数字完成**首次验证**,绑定正式生效。 5. **退出后重新登录**:密码验证通过后需再输入 App 上的 6 位验证码,验证一致才登录成功。 ## 技术要点 - **TOTP 算法**(`src/utils/totp.ts`):服务器与手机 App 用同一 Secret + `floor(时间戳/30s)` 作为计数器,做 HMAC-SHA1 后动态截断取 6 位数。密钥和时间相同 ⇒ 验证码必然一致。校验时允许前后各 1 个时间窗口漂移,并用 `crypto.timingSafeEqual` 恒定时间比较。 - **Base32 编解码**(`src/utils/base32.ts`):按 RFC 4648 手写实现。 - **Secret 加密存储**(`src/utils/cryptoUtil.ts`):AES-256-GCM,密钥来自环境变量 `SECRET_ENCRYPTION_KEY`(未设置时使用仅用于演示的默认值,生产环境务必配置)。 - **两段式登录**:密码通过后 session 中标记 `pendingUserId`,2FA 验证通过才写入 `userId`;未完成第二因素前所有受保护接口返回 401。 ## 目录结构 ``` src/ ├── index.ts # 入口:装配中间件/路由,启动服务 ├── config.ts # 端口、issuer、session 等配置 ├── db.ts # SQLite 连接与建表 ├── types/session.d.ts # session 字段类型扩展 ├── middleware/auth.ts # requireAuth(登录校验)、requireCode(验证码格式校验) ├── routes/ │ ├── auth.ts # /api/register、/api/login、/api/login/2fa、/api/logout、/api/me │ └── twoFactor.ts # /api/2fa/setup、/enable、/disable ├── services/ │ ├── userService.ts # 用户表的数据访问 │ └── twoFactorService.ts # 2FA 业务逻辑(生成/加解密/校验) └── utils/ ├── base32.ts # Base32 编解码 ├── totp.ts # TOTP/HOTP 算法 └── cryptoUtil.ts # AES-256-GCM 加解密 ``` ## API | 方法 | 路径 | 说明 | |---|---|---| | POST | `/api/register` | 注册 `{username, password}` | | POST | `/api/login` | 第一因素;已开 2FA 返回 `{need2fa:true}` | | POST | `/api/login/2fa` | 第二因素 `{code}`,完成登录 | | POST | `/api/logout` | 登出 | | GET | `/api/me` | 当前用户信息 | | POST | `/api/2fa/setup` | 生成 Secret,返回 `{secret, uri, qrDataUrl}` | | POST | `/api/2fa/enable` | 首次验证 `{code}`,绑定生效 | | POST | `/api/2fa/disable` | 凭验证码关闭 2FA |