WebAuthn 无密码认证标准

FreeGuideOnline 最新 2026-07-11

mermaid sequenceDiagram participant 用户 participant 浏览器 participant 网站服务器 participant 认证器 (如指纹)

用户 ->> 网站服务器: 注册请求 (用户名)
网站服务器 ->> 浏览器: 挑战值 + RP 信息
浏览器 ->> 认证器: 请求创建凭证
认证器 ->> 用户: 要求验证 (指纹/面部)
用户 ->> 认证器: 提供生物识别
认证器 ->> 浏览器: 公钥 + 签名挑战 + 凭证 ID
浏览器 ->> 网站服务器: 发送公钥凭证
网站服务器 ->> 网站服务器: 验证签名,存储公钥

### 第二阶段:验证流程(登录)

1. 用户发起登录,输入用户名或直接点击“通过安全密钥登录”。
2. 服务器根据用户信息,生成新挑战值,并返回**之前注册时存储的凭证 ID 列表**。
3. 浏览器调用 `navigator.credentials.get()`,将挑战值和凭证 ID 传给认证器。
4. 认证器根据凭证 ID 找到对应的私钥,要求用户进行同样的验证(生物识别/PIN)。
5. 认证器用私钥对挑战值进行**签名**,将签名结果返回浏览器。
6. 浏览器将签名结果发送给服务器。
7. 服务器使用该用户之前保存的**公钥**验证签名。验证成功即登录成功。

这个过程中,用户无需输入密码,服务器也从未接触过任何“秘密”,完全依赖公钥加密。

## 关键角色与术语

- **依赖方(Relying Party,RP)**:即你的网站或应用,想要验证用户身份的实体。
- **客户端(Client)**:用户的浏览器或操作系统,提供 WebAuthn API。
- **认证器(Authenticator)**:执行用户验证和生成密钥对的硬件或软件。常见有:
  - **平台认证器**:内置在设备中的生物识别模块,如 Touch ID、Face ID、Windows Hello、Android 生物识别。
  - **漫游认证器**:可跨设备使用的物理安全密钥,如 USB/NFC 的 YubiKey、Feitian 密钥。
- **凭证 ID(Credential ID)**:认证器生成的一个随机标识符,用来在后续登录时定位到正确的私钥(因为一个认证器可存储多个网站的密钥)。
- **挑战值(Challenge)**:服务器生成的随机字符串,用于防止重放攻击。

## WebAuthn 的突出优势

| 传统密码登录 | WebAuthn |
| :--- | :--- |
| 依赖共享秘密(密码) | 使用非对称加密(公钥/私钥) |
| 密码可从服务器泄露 | 服务器只存公钥,泄露无法仿冒 |
| 用户必须记住/管理密码 | 只需生物识别或硬件令牌 |
| 极易被钓鱼 | 绑定域名,无法被中间人攻击 |
| 跨设备同步困难 | 平台认证器可在 Apple/Google 生态内同步 |

## 实际使用体验

对于最终用户,流程极其简单:

- **注册时**:网站弹出“使用 Touch ID / Windows Hello 注册?”,轻按手指或看一眼摄像头,完成。
- **登录时**:点击“登录”,再次验证指纹或面部,瞬间登入。

整个过程完全不涉及密码输入,连“创建密码”的框都没有。

## 浏览器与平台支持

WebAuthn 已经获得**所有主流浏览器和操作系统**的广泛支持:

- **桌面端**:Windows 10+(Hello)、macOS(Touch ID)、Chrome OS、Linux。
- **移动端**:iOS 14+(Face ID / Touch ID)、Android 7+(指纹/面部)。
- **浏览器**:Chrome、Firefox、Safari、Edge 均完美实现 WebAuthn API。
- **跨浏览器复用的平台密钥**:通过 FIDO 联盟的 **多设备凭证(Passkeys)**,用户可以将 WebAuthn 凭证同步到 Google 密码管理器或 iCloud 钥匙串,实现跨设备无缝登录。

## 开发者快速实现要点

### 1. 后端要求

- 生成并验证挑战值,使用 `crypto` 库进行签名验证。
- 存储用户凭证:至少需要保存 `credentialID`、`publicKey`(以 PEM 或 COSE 格式)、`signCount`(签名计数器,用于防克隆)。
- 推荐使用成熟的开源库,如 **SimpleWebAuthn**、**Yubico java-webauthn-server**、**Duo WebAuthn4J** 等。

### 2. 前端 JavaScript 调用

**注册时**:
```javascript
const publicKeyCredentialCreationOptions = {
    challenge: Uint8Array.from(randomString, c => c.charCodeAt(0)),
    rp: { name: "示例网站", id: "example.com" },
    user: {
        id: Uint8Array.from(userId, c => c.charCodeAt(0)),
        name: "user@example.com",
        displayName: "用户显示名",
    },
    pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
    authenticatorSelection: {
        authenticatorAttachment: "platform", // 仅使用平台认证器
        userVerification: "required"
    },
    timeout: 60000
};

const credential = await navigator.credentials.create({
    publicKey: publicKeyCredentialCreationOptions
});
// 将 credential.id, credential.response 等发往后端

验证时

const publicKeyCredentialRequestOptions = {
    challenge: Uint8Array.from(randomString, c => c.charCodeAt(0)),
    allowCredentials: [{
        id: credentialIdBuffer, // 用户已注册的凭证 ID
        type: 'public-key',
    }],
    userVerification: 'required',
    timeout: 60000,
};

const assertion = await navigator.credentials.get({
    publicKey: publicKeyCredentialRequestOptions
});
// 将 assertion 发送至后端验证签名