vue中使用WebAuthn

着魔的村支 2024-08-09 13:03:01 阅读 73

## WebAuthn

*   全称是 Web Authentication,是一个由万维网联盟(W3C)和 FIDO 联盟(Fast Identity Online)开发的 web 标准。它允许网站和应用程序使用公钥加密和生物识别技术(如指纹识别、面部识别)进行强身份验证,以替代传统的密码登录方式。WebAuthn 提供了更高的安全性和更好的用户体验。

*   <https://webauthn.io/>

### WebAuthn 工作原理

WebAuthn 是一个由万维网联盟(W3C)和 FIDO 联盟(Fast Identity Online)开发的 web 标准,它允许网站和应用程序使用公钥加密和生物识别技术(如指纹识别、面部识别)进行强身份验证,以替代传统的密码登录方式。以下是 WebAuthn 的工作原理,包括注册(Registration)和认证(Authentication)两个主要过程。

![WebAuthn Authentication](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/704bd11de6314dfeb579c4f515cec895~tplv-73owjymdk6-watermark.image?policy=eyJ2bSI6MywidWlkIjoiMzQ2NjExNzQzNjY3MTcwMyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1722396857&x-orig-sign=aMT4vRfxz2rCeDPjbWTF5AEjny0%3D)

### 1. 注册(Registration)

在注册过程中,用户在其设备上创建一个新的公钥-私钥对,并将公钥发送给服务器保存。具体步骤如下:

1.  **用户发起注册请求**:用户在网站上选择注册,并开始注册过程。

2.  **服务器生成挑战(challenge)**:服务器生成一个随机的挑战,并将其发送给用户的浏览器。

3.  **浏览器调用 WebAuthn API**:浏览器使用 WebAuthn API 调用用户设备的可信平台模块(TPM)或安全元件,生成公钥-私钥对。然后,设备会用私钥签署服务器的挑战,并将签名和公钥一起发送回服务器。

4.  **服务器验证并存储公钥**:服务器验证签名和挑战,以确保请求的真实性。如果验证成功,服务器存储公钥用于后续的认证。

### 2. 认证(Authentication)

在认证过程中,用户使用之前注册的设备进行登录。具体步骤如下:

1.  **用户发起登录请求**:用户在网站上选择登录,并开始认证过程。

2.  **服务器生成挑战(challenge)**:服务器生成一个随机的挑战,并将其发送给用户的浏览器。

3.  **浏览器调用 WebAuthn API**:浏览器使用 WebAuthn API 调用用户设备的 TPM 或安全元件,使用私钥签署服务器的挑战。然后,设备将签名和相关信息发送回服务器。

4.  **服务器验证签名**:服务器使用存储的公钥验证签名和挑战,以确保请求的真实性。如果验证成功,用户成功登录。

### WebAuthn 的优势

1.  **提高安全性**:WebAuthn 使用公钥加密,减少了传统密码的攻击面,避免了密码泄露、弱密码等问题。

2.  **简化用户体验**:利用设备的生物识别功能,如指纹、面部识别等,使得用户无需记住复杂的密码,登录过程更加便捷。

3.  **广泛支持**:WebAuthn 标准得到了主流浏览器和操作系统的广泛支持,包括 Chrome、Firefox、Edge、Safari,以及 Android 和 iOS 设备。

## navigator.credentials.create

- [`Navigator`](https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator) 接口的只读属性 **`credentials`** 返回与当前文档关联的 [`CredentialsContainer`](https://developer.mozilla.org/zh-CN/docs/Web/API/CredentialsContainer) 对象,该对象暴露用于请求凭据的方法。[`CredentialsContainer`](https://developer.mozilla.org/zh-CN/docs/Web/API/CredentialsContainer) 接口还会在发生感兴趣的事件时通知用户代理,例如成功登录或注销。此接口可用于特性检测。

### 开始注册

#### Web Authentication API 的 options 对象

| 字段                        | 类型    | 说明                                                                                   | 可选值 |

| --------------------------- | ------- | -------------------------------------------------------------------------------------- | ------ |

| **attestation**             | 字符串  | 指定所需的认证声明传递偏好。                                                           | `"none"`(无需认证信息)、`"indirect"`(间接认证)、`"direct"`(直接认证) |

| **authenticatorSelection**  | 对象    | 定义选择认证器的标准。                                                                 |        |

| - authenticatorAttachment   | 字符串  | 指定要使用的认证器类型。                                                               | `"platform"`(使用集成到设备中的认证器)、`"cross-platform"`(使用外部认证器) |

| - requireResidentKey        | 布尔值  | 指示认证器是否必须创建存储在设备上的常驻密钥。                                         | `true`(是)、`false`(否) |

| - residentKey               | 字符串  | 指定常驻密钥的要求。                                                                   | `"required"`(必须)、`"preferred"`(优先)、`"discouraged"`(不建议) |

| **challenge**               | 字符串  | 用于认证过程中的挑战码,应该是从服务器端生成的。                                       |        |

| **excludeCredentials**      | 数组    | 列出在注册新凭证时应排除的凭证的列表,用于确保不重复注册。                             |        |

| **extensions**              | 对象    | 用于扩展 Web Authentication API 的附加选项。                                           |        |

| - credProps                 | 布尔值  | 指示是否返回有关创建的凭证的属性信息。                                                 | `true`(返回),`false`(不返回) |

| **pubKeyCredParams**        | 数组    | 定义公钥证书应使用的加密算法。                                                         |        |

| - alg                       | 数字    | 算法标识符(如 `-7` 表示 ES256,`-257` 表示 RS256)                                    |        |

| - type                      | 字符串  | 证书类型,通常为 `"public-key"`                                                        |        |

| **rp**                      | 对象    | 定义依赖方(即服务提供者)的信息。                                                       |        |

| - id                        | 字符串  | 依赖方的域名。                                                                         |        |

| - name                      | 字符串  | 依赖方的描述名称,如 `"Passkey Example"` 本地测试可以用 `window.location.hostname`                                               |        |

| **timeout**                 | 数字    | 指定操作的超时时间(毫秒)。                                                           |        |

| **user**                    | 对象    | 定义正在注册或认证的用户的信息。                                                       |        |

| - displayName               | 字符串  | 用户的显示名称。                                                                       |        |

| - id                        | 字符串  | 用户的唯一标识符。                                                                 |        |

| - name                      | 字符串  | 用户的名称。                                                                           |        |

``` json

  // options 参考

  

  {

          attestation: "none",

          authenticatorSelection: {

            authenticatorAttachment: "platform",

            requireResidentKey: true,

            residentKey: "required",

          },

          challenge: "F16NrxGy23Ps_73Pgy_H6fh0ihgEXBuycusFLahMvmU",

          excludeCredentials: [],

          extensions: {

            credProps: true,

          },

          pubKeyCredParams: [

            {

              alg: -7, // 对应于ES256

              type: "public-key",

            },

            {

              alg: -257, // 对应于RS256

              type: "public-key",

            },

          ],

          rp: {

            id: window.location.hostname,  // 本地测试用 可以使用localhost

            name: "Passkey Example",

          },

          timeout: 60000,

          user: {

            displayName: "allen",

            id: "20222027",

            name: "allen",

          },

        };

  

```

####  需要将 challenge 和 user.id 转成`ArrayBuffer` 类型

```javascript

options.challenge = base64url.decode(options.challenge); 

options.user.id = base64url.decode(options.user.id);

const base64url = {

  encode: function (buffer) {

    const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));

    return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");

  },

  decode: function (base64url) {

    const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");

    const binStr = window.atob(base64);

    const bin = new Uint8Array(binStr.length);

    for (let i = 0; i < binStr.length; i++) {

      bin[i] = binStr.charCodeAt(i);

    }

    return bin.buffer;

  },

};

```

#### 唤起生物认证

- 未开启 windows hello

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/aec727f881714ec58801841a0ba0c62f~tplv-73owjymdk6-watermark.image?policy=eyJ2bSI6MywidWlkIjoiMzQ2NjExNzQzNjY3MTcwMyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1722396857&x-orig-sign=WXYP2zI72PEkjw2ZkdTTF9hH3Ls%3D)

- 在系统设置里面添加

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/2ee34e6b32fc4140ac0cd14711021973~tplv-73owjymdk6-watermark.image?policy=eyJ2bSI6MywidWlkIjoiMzQ2NjExNzQzNjY3MTcwMyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1722396857&x-orig-sign=bZyPHmN3VutfxNbcyElmBH%2F%2FKW4%3D)

- 添加完成  唤起成功

![image.png](https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1afaed40e72e4187810ba3fdec7cbc63~tplv-73owjymdk6-watermark.image?policy=eyJ2bSI6MywidWlkIjoiMzQ2NjExNzQzNjY3MTcwMyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1722396857&x-orig-sign=cTtPDP6hdtsaFxP48dY41CrpTJQ%3D)

#### 完成注册后的步骤

在 Web Authentication API (`navigator.credentials.create`) 成功创建公钥凭证后,你需要执行以下几个关键步骤以完成用户的注册过程:

#### 1. 发送凭证信息到服务器

成功创建凭证后,需要将其详细信息发送到服务器端进行验证和存储。以下是应发送的凭证详情:

- **ID (`credential.id`)**: 凭证的唯一标识符。

- **类型 (`credential.type`)**: 凭证的类型,通常是 `"public-key"`。

- **原始 ID (`credential.rawId`)**: 凭证的原始标识符,使用 Base64URL 编码格式。

- **响应 (`credential.response`)**: 包括认证对象 (`attestationObject`) 和客户端数据 (`clientDataJSON`) 的响应,均使用 Base64URL 编码。

#### 2. 服务器端验证

服务器接收到客户端发送的凭证信息后,需要执行以下验证步骤:

- **验证挑战** (`challenge`): 确认返回的挑战码与之前发送给客户端的挑战码相匹配。

- **验证来源** (`origin`): 确认凭证的创建请求来自预期的域名。

- **验证公钥凭证参数** (`pubKeyCredParams`): 确认凭证使用了有效的加密算法。

- **存储凭证信息**: 在服务器端安全地存储用户的公钥和其他相关凭证信息,以备将来进行身份验证使用。

#### 示例代码:将凭证信息发送到服务器

```javascript

fetch('/api/register', {

    method: 'POST',

    headers: {

        'Content-Type': 'application/json'

    },

    body: JSON.stringify({

        id: credential.id,

        type: credential.type,

        rawId: credential.rawId,

        attestationObject: credential.response.attestationObject,

        clientDataJSON: credential.response.clientDataJSON

    })

})

.then(response => response.json())

.then(data => {

    console.log('注册成功', data);

    alert('注册成功!');

})

.catch(error => {

    console.error('注册失败', error);

    alert('注册失败,请重试!');

});

```

#### 完整注册代码

```javascript 

    async register() {

      if (!("credentials" in navigator)) {

        alert(

          "此浏览器不支持 Web Authentication API。请尝试更新浏览器或使用其他支持的浏览器。"

        );

        return;

      }

      try {

        // 从服务器获取创建凭证的选项

        const {data: options} = await axios.get('/api/register-options');

        // 转成 arrayBuffer

        options.challenge = base64url.decode(options.challenge);

        options.user.id = base64url.decode(options.user.id);

        // 开始注册

        const credential = await navigator.credentials.create({

          publicKey: options,

        });

        

        await axios.post('/api/register', {

         id: credential.id,

         type: credential.type,

         rawId: credential.rawId,

         attestationObject: credential.response.attestationObject,

         clientDataJSON: credential.response.clientDataJSON

        });

        

        alert("注册成功");

      } catch (error) {

        console.error("注册失败", error);

        alert("注册失败");

      }

    },

```

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。