登录安全设计:设备验证与二次验证
1. 为什么需要这套机制
密码只解决「你是谁」的问题,不解决「你在这台设备上登录是否安全」的问题。
现实中两个常见场景:
- 密码泄露:用户在网吧登录后忘记退出,或密码被钓鱼网站窃取。攻击者拿到密码就能在任何设备上登录。
- 新设备风险:用户换了新手机/浏览器,系统无法判断是本人还是攻击者。
设备信任解决场景 2——新设备需要额外验证,验证通过后标记为受信,后续免验证。 2FA 解决场景 1——即使密码泄露,攻击者没有邮箱验证码也登不进来。
新设备登录模式(全局配置)
管理员可通过系统设置 Device.NewDeviceMode 控制新设备的处理方式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
| verify(默认) | 新设备必须输入验证码才能登录 | 安全要求高 |
| notify | 新设备仅发送提醒邮件,不阻断登录 | 不想打扰用户 |
notify 模式下,邮件使用纯通知模板(无验证码),告知用户「有人在新设备登录了你的账号」,如果不是本人就去改密码。
2. 为什么不做成两套独立机制
传统做法是 2FA 和设备信任各管各的,但这带来两个问题:
用户体验差
用户开启 2FA 后,每次登录都要收邮件、输验证码,即使是常用设备。用了一周后大多数人会关掉 2FA。
管理成本高
用户需要在两个地方分别管理:2FA 开关、设备列表的信任状态。概念重叠,容易混淆。
飞书的做法
飞书将两者合并:设备信任是 2FA 的优化层。
- 2FA 关闭时:新设备验证,受信设备免验证(设备信任发挥作用)
- 2FA 开启时:每次都验证(安全优先),但设备信任状态保留
这样 2FA 不会因为「太烦」被关掉——因为受信设备在 2FA 关闭时能享受免验证。
3. 核心判定逻辑
bool needVerify = result.RequiresTwoFactor || !isTrusted;
一行代码,四个场景:
| 2FA | 设备信任 | 结果 | 为什么 |
|---|---|---|---|
| 关 | 信任 | 直接登录 | 常用设备,无需打扰 |
| 关 | 未信任 | 发验证码 | 新设备,需要确认身份 |
| 开 | 信任 | 发验证码 | 2FA 要求每次验证,安全优先 |
| 开 | 未信任 | 发验证码 | 新设备 + 2FA,双重确认 |
关键:2FA 开启时信任不跳过验证。这是和「两套独立机制」的核心区别——信任状态不覆盖 2FA 策略,只在 2FA 关闭时生效。
4. 信任设备的设计决策
为什么信任有有效期(180天)
永久信任意味着一旦设备被标记,即使设备被转卖、被盗,攻击者也能永久免验证。180 天是安全性和便利性的平衡点——用户每半年至少验证一次,足够宽松不打扰日常使用。
为什么信任不绑定 IP
指纹 = SHA256(UserAgent)[前32位]
最初设计了 SHA256(UserAgent + "|" + IP) 的方案,但 IP 太不稳定——用户切换 WiFi、4G/5G、VPN 都会变。每次 IP 变化都要求重新验证,体验很差。
只用 UserAgent 的代价是:同一台设备的不同浏览器会被视为不同设备(Chrome 和 Firefox 各自独立)。这是可接受的,因为不同浏览器确实有不同的 Cookie/登录态。
为什么信任操作只在登录时
最初设计了两种信任方式:
- 登录验证弹窗中勾选「信任此设备」
- 设备管理页面手动点击「信任」
第二种被去掉了,原因:
- 设备管理页面是「事后管理」,用户在那里信任一台设备时,这台设备可能并没有在登录时通过验证
- 信任应该和验证绑定——先证明「我能收到验证码」,再标记信任
- 减少攻击面:如果攻击者能访问设备管理页面(比如通过 XSS),他就能把自己的设备标记为信任
5. 2FA 开关的设计决策
为什么用户可以自己开关
2FA 由用户自主控制,而不是管理员强制。原因:
- 这是一个 C 端系统,用户有自主权
- 强制 2FA 会导致用户流失(注册时就要验证邮箱已经是门槛了)
- 用户决定自己需要多高的安全级别
为什么关闭 2FA 不需要验证码
开启时需要验证码(确认邮箱可达),关闭时不需要。原因:
- 关闭 2FA 会降低安全性,攻击者没有动机诱导用户关闭
- 用户已经通过了登录验证(密码 + 可能的设备验证),身份已确认
- 减少关闭流程的摩擦,避免用户因为「关都关不掉」而抱怨
信任状态在 2FA 关闭后为什么保留
用户开启 2FA → 用了三个月 → 关闭 2FA。此时设备列表中的信任状态仍然有效。
因为信任状态和 2FA 是独立维度:
- 信任 = 「这台设备是安全的」
- 2FA = 「每次登录都要额外验证」
关闭 2FA 后,「这台设备是安全的」这个事实没有改变。受信设备立刻恢复免验证,用户不需要重新信任。
6. 验证码 vs 其他方案
为什么用邮箱验证码而不是 TOTP
| 方案 | 优点 | 缺点 |
|---|---|---|
| 邮箱验证码 | 无需安装 App,所有用户都有邮箱 | 依赖邮件服务可用性 |
| TOTP(Google Authenticator) | 不依赖网络 | 需要安装 App,换手机时需要迁移 |
| 短信验证码 | 用户熟悉 | 有成本,SIM 卡劫持风险 |
邮箱验证码是最低门槛的方案,适合 C 端系统。如果未来需要更高安全级别,可以加 TOTP 作为可选项。
为什么不做成链接而是验证码
邮件中的链接(magic link)点击后直接登录,但:
- 移动端邮件客户端点击链接可能跳转到浏览器,而不是 App
- 链接被邮件安全网关预请求会导致提前失效
- 验证码让用户在当前页面输入,流程更可控
7. 登录流程中不受影响的路径
以下流程不走 LoginAsync,不受设备验证影响:
| 流程 | 原因 |
|---|---|
| 模拟登录 | 管理员操作,已有管理后台的访问控制 |
| 第三方登录 | 已经通过 OAuth 提供商验证身份 |
| 验证码登录 | 邮箱验证码本身就是第二因素 |
| 刷新 Token | Token 有效期内的续期,不涉及新设备判断 |