---
title: "不用密码也能登陆？React中的通行密钥实现"
url: "https://lerry.me/post/2024/10/passwordless-login-react-passkey-integration"
date: 2024-10-11T12:05:23.974Z
updated: 2026-09-04T03:50:15.399Z
tags:
  - "Next.js"
  - "React"
  - "Passkey"
  - "通行密钥"
language: zh-CN
---

# 不用密码也能登陆？React中的通行密钥实现

你可能已经注意到了，近两年有些网站提供了一种名为 通行密钥/Passkey 的东西作为用户身份认证方式，或者辅助验证。今天我们来了解下什么是通行密钥。![screenshot-20241012-012826](https://r2.lerry.me/next-blog/uploads/202410/11e7fdf21757bcb96f1c0b73ba2502bf.png)

## 什么是通行密钥?

2022 年 6 月，苹果公司宣布将在 iOS 和 macOS 中加入对通行密钥标准的支持，“通行密钥”这个术语首次被广泛提及。除了 Apple，目前 Google 和 Microsoft 也提供了对通行密钥的支持。

简单的说，通行密钥是一种用于网站或者 App 身份验证的方式，它使用非对称加密方式，使用存储在个人设备（手机或者电脑）中的私钥进行签名，由服务器端使用对应的公钥进行验证，确认用户身份完成登录过程。

要实现这个过程需要浏览器和操作系统的共同支持。这个技术在现代的设备上已经有了比较好的支持，如果你使用 iOS/MacOS 系统，那么更妙，iCloud 会自动同步存储的信息，方便你跨设备使用。

## 为什么使用通行密钥?

1. 安全。传统的用户名/密码登陆方式早已不安全，容易受到撞库、密码泄漏影响。
2. 良好的体验。两步验证虽然安全，但是使用上比较麻烦。而使用通行密钥用户无需记住复杂的密码，还可以使用生物识别等方式快速登录。
3. 跨平台兼容。通行密钥可在不同设备和平台间无缝使用。

## 如何在 React 应用中实现通行密钥?

要在代码中实现通行密钥的支持，需要客户端和服务端的共同配合。我试着实现了一下，这个流程其实并不复杂，客户端的核心 API 只有`navigator.credentials.create`和`navigator.credentials.get`，但是因为涉及数据格式转换、签名验证等，还是有些繁琐的。所以我推荐使用[simplewebauthn](https://simplewebauthn.dev/)这个库来帮助实现，它提供了客户端和服务端实用函数的封装，十分方便。

### 流程

我用 Claude 生成了两个图方便理解，

![passkey-registration-flow](https://r2.lerry.me/next-blog/uploads/202410/a8ae303d427028dd13afc10e51546191.png)

![passkey-login-flow](https://r2.lerry.me/next-blog/uploads/202410/33580c5989137b468e9f6c6e0499d975.png)

两个流程很类似，都是先向服务器请求包含 challenge 的配置信息，然后和`navigator.credentials` API 交互，最后把结果传给服务器用于注册或者登陆。

### 示例代码

因为我使用 Next.js 的 Server Action，所以避免了手动调用接口，直接调用后端方法即可。代码比较长，我省略了函数的实现，完整代码以文件形式提供。

**完整代码**：[passkey.tsx](https://r2.lerry.me/next-blog/uploads/202410/passkey.tsx) [passkey.ts](https://r2.lerry.me/next-blog/uploads/202410/passkey.ts)

首先在数据库 User 表中添加了三个字段用于存储通行密钥信息，以下是 prisma 的 schema 文件修改：

```prisma
credentialId String? @unique @map("credential_id")
publicKey    String? @map("public_key")
counter      Int     @default(0) @map("counter")
```

```ts
// passkey.ts 文件，用于服务器端

// 生成通行密钥创建选项
// 返回包含challenge等信息的选项对象,用于客户端调用navigator.credentials.create
export async function generateCreateOptions() {
  ...
}

// 验证通行密钥创建响应
// 参数attResponse: 客户端创建通行密钥后的响应数据
// 返回验证结果,成功则返回true
export async function verifyCreateResponse(
  attResponse: RegistrationResponseJSON,
) {
   ...
}

// 生成通行密钥登录选项
// 返回包含challenge等信息的选项对象,用于客户端调用navigator.credentials.get
export async function generateLoginOptions() {
  ...
}

// 验证通行密钥登录响应
// 参数attResponse: 客户端使用通行密钥登录后的响应数据
// 返回验证结果,成功则返回true
export async function verifyLoginResponse(
  attResponse: AuthenticationResponseJSON,
) {
  ...
}

```

```tsx
// passkey.tsx React组件，用于客户端

export function PasskeyBindButton() {
  const handleBindPasskey = async () => {
    const options = await generateCreateOptions();
    const credential = await startRegistration(options);
    const verified = await verifyCreateResponse(credential as any);
    if (verified) {
      toast.success("绑定通行密钥成功");
    } else {
      toast.error("绑定通行密钥失败");
    }
  };

  return (
    <div>
      <Button onClick={handleBindPasskey}>绑定通行密钥</Button>
    </div>
  );
}

export function PasskeyLoginButton() {
  const router = useRouter();
  const handlePasskeyLogin = async () => {
    const options = await generateLoginOptions();
    const credential = await startAuthentication(options);

    try {
      const verified = await verifyLoginResponse(credential as any);
      if (verified) {
        toast.success("登录成功");
        router.push(siteConfig.adminUrl);
      } else {
        toast.error("登录失败");
      }
    } catch (error) {
      console.error(error);
      toast.error((error as Error).message);
      return;
    }
  };

  return (
    <Button className="w-full" onClick={handlePasskeyLogin}>
      使用通行密钥登录
    </Button>
  );
}
```

如果客户端和服务端都正确实现的话，点击页面上的绑定或者登陆按钮，就会唤起浏览器的注册/登陆界面。

### 一些注意事项

1. 我这里是在用户登陆以后，提供了绑定通行密钥的按钮，所以"注册流程"走完以后会绑定到当前用户。你也可以在其他流程中调用，只需要修改验证流程即可。
2. 实际上可以允许用户绑定多个通行密钥，这样在登陆的时候，可以选择使用哪个通行密钥登录。我这里只实现了一个，你可以在代码中修改。
3. 创建 options 和验证响应数据时，都使用到了 challenge，这个值应该是一个随机数，并且只能使用一次，避免重放攻击，这一点 `simplewebauthn` 已经帮我们做到了，我们需要将其存储在 session 中用于后续验证。
4. 通行密钥的API提供了很多选项用于详细的设置，可以查看相关文档。

## 结论

通行密钥提供了一种现代、安全、便捷的身份验证方法。通过利用 WebAuthn API，开发者可以轻松地将这一技术整合到他们的应用中，从而提高安全性并改善用户体验。随着更多浏览器和设备支持这一技术，通行密钥有望成为未来身份验证的标准方式。

## 参考资料

- [Web Authentication API - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
- [Passkey Form Autofill - Google Codelabs](https://developers.google.com/codelabs/passkey-form-autofill?hl=zh-cn#0)
- [通行密钥开发指南 - 廖雪峰](https://liaoxuefeng.com/blogs/all/2023-08-16-passkey-dev/index.html)
- [SimpleWebAuthn Documentation](https://simplewebauthn.dev/docs/)
