3.1 用户认证:密码、Passkey、会话与账户恢复
认证回答“当前请求背后是谁,以及这个判断有多可靠”。登录页只是入口;注册、认证器绑定、会话延续、重新认证、账户恢复和注销共同构成完整生命周期。最薄弱的一环通常不是登录算法,而是恢复流程或客服绕过。
因素数量不等于字段数量
认证因素按来源分为:
something you know:密码、PIN
something you have:安全密钥、持有私钥的设备
something you are:生物特征2
3
密码加安全问题仍是两个“知道因素”,不是 MFA。手机本地指纹通常用于解锁设备中的私钥,服务端收到的是密码学签名,不应存储用户原始指纹模板。
OTP 能抵抗简单密码重用,却可以被钓鱼站实时转发。NIST 不把手工输入 OTP 视为 phishing-resistant。高风险系统应提供基于公钥、绑定 verifier identity 的认证器,例如正确实现的 WebAuthn/passkey。
Passkey 是按 RP 隔离的公钥凭据
注册时,认证器为 relying party 创建密钥对,服务端验证注册响应后保存公钥与 credential metadata;认证时发出随机 challenge,认证器对 authenticatorData || SHA-256(clientDataJSON) 签名。RP ID hash 位于认证器数据中,challenge 与 origin 位于客户端数据中。
Registration:challenge → authenticator creates key pair → store public key
Authentication:fresh challenge → scoped signature → verify signature, challenge, origin, RP and flags2
私钥不发送给服务端,凭据按 RP ID 作用域隔离,浏览器正确执行 RP ID 范围检查、服务端严格检查 origin 时,无关的钓鱼域名不能为真实站点取得有效 assertion。Passkey 可以是设备绑定,也可以由平台端到端同步;风险模型要区分同步账户恢复、设备解锁和硬件 attestation,而不是把所有 passkey 当成同一保证等级。
服务端应使用受维护的 WebAuthn 库检查响应类型、签名、一次性 challenge、允许的 origin、RP ID hash、账户与 credential 绑定及状态。用户在场标记 UP 不等于用户验证标记 UV;要求 PIN/生物特征验证时,必须要求并检查 UV,不能只看用户点过按钮。signCount 是克隆风险信号,有些认证器始终返回 0,同步凭据也不应被假定具有跨设备单调计数;它不替代 challenge 的一次性验证。Attestation 可能带来设备可识别性与供应商依赖,只有明确需要设备来源保证时才要求。
密码仍需作为完整系统设计
使用密码时,应结合第 1.2 课的 Argon2id 存储,并实现:
- 允许长密码、密码管理器生成和粘贴;
- 对常见与已泄露密码做 blocklist;
- 按账户、来源、设备和全局风险分层限速;
- 返回统一登录错误,避免枚举账户;
- 高风险登录触发 step-up,而不是永久锁死并制造 DoS;
- 登录、恢复和认证器变更发送可追溯通知。
不要用周期强制换密鼓励 Spring2026! 一类可预测变化。检测到泄露、共享或风险事件时再强制重置,并撤销相关会话。
会话 Cookie 是浏览器中的不透明句柄
服务端会话常比把全部授权状态塞进浏览器 JWT 更容易撤销和更新:
Cookie: __Host-session=<high-entropy opaque id>
Server store: id → subject, auth_time, assurance, expiry, device, revocation2
认证成功或认证保证、权限等级改变时,服务端应签发新的 session ID 并使旧 ID 失效,防止 session fixation。敏感操作需检查近期认证,不必把“每个敏感请求都换一次 ID”作为通用规则。Cookie 通常设置:
Set-Cookie: __Host-session=...; Secure; HttpOnly; SameSite=Lax; Path=/Secure 要求 HTTPS,HttpOnly 阻止普通脚本读取,SameSite 降低部分跨站请求风险。__Host- 前缀要求 Secure、Path=/ 且不能带 Domain,可减少父域/子域 Cookie 范围混淆。HttpOnly 不阻止 XSS 以当前用户身份发请求。它们不替代输出编码、CSP 和完整 CSRF 防护;同站子域、顶级导航和业务跨站流程都可能改变威胁模型。
会话需要 idle timeout 与 absolute timeout。敏感操作还应检查最近认证时间,并要求重新认证或更强因素。注销要使服务端状态失效,而不只是删浏览器 Cookie;密码/认证器变更时要让用户查看并撤销其他设备会话。
CSRF 利用的是浏览器自动携带凭据
若浏览器自动发送 session Cookie,攻击者可以诱导用户访问恶意页面向真实站点发请求。防护通常组合:
- 不让 GET 产生状态变更;
- 使用不可预测、绑定会话的 CSRF token;
- 校验 Origin/Referer 作为纵深防御;
- 合理设置 SameSite;
- 对高风险动作重新认证或交易确认。
CORS 不是 CSRF 防护。它主要限制恶意页面读取响应,简单表单请求仍可能被发送。
恢复流程决定账户真正强度
如果客服凭姓名和生日就能移除 passkey,硬件认证的保证会被恢复流程绕过。设计恢复时应:
- 预先注册多个独立认证器和一次性恢复码;
- 恢复码只展示一次并存单向验证值:低熵码需加盐的专用密码哈希;足够高熵随机码可用合适的密码学哈希,并限制在线尝试;
- 高价值账户采用延迟、通知和多方证据;
- 恢复后撤销旧会话与高风险 token;
- 对新认证器绑定、邮箱/手机号变更保留审计和撤销窗口;
- 客服不能看到或索取用户密码、OTP 和恢复码;一次性码的“验证并消费”必须原子完成。
至少测试这些失败状态:登录后旧 session ID 被拒绝;退出后重放 Cookie 失败;另一 origin 的 assertion 被拒绝;同一个 challenge 或恢复码只有一次消费能成功;缺少所需 UV 标记时不能获得相同保证等级。用恢复码重置密码后,也要检查旧会话与绑定凭据是否按策略退出。
认证不是“登录成功=true”。应用应保留认证时间、方法、保证等级和风险上下文,供后续授权与 step-up 决策使用。
下一课讨论OAuth 2.0 与 OpenID Connect的委托授权和认证结果。