Passkey 密钥

Passkey 密钥

Passkey 密钥是一种安全的无密码认证方法,使用加密密钥对,由 WebAuthn 和 FIDO2 标准支持,适用于网页浏览器。它用独特的密钥对替代密码:私钥存储在用户设备上,公钥共享给网站。用户可以通过生物识别、PIN 码或安全密钥登录,提供强大且抗钓鱼的认证,免除了传统密码的使用。

Passkey 插件的实现幕后由 SimpleWebAuthn 提供支持。

安装

安装插件

npm install @better-auth/passkey

将插件添加到身份验证配置中

要将 passkey 插件添加到您的身份验证配置中,需要导入该插件并传递给 auth 实例的 plugins 选项。

auth.ts
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey"

export const auth = betterAuth({
    plugins: [ 
        passkey(), 
    ],
})

数据库迁移

运行迁移或生成架构以向数据库添加必需的字段和表。

npx auth migrate
npx auth generate

请参阅 Schema 部分手动添加字段。

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { passkeyClient } from "@better-auth/passkey/client"

export const authClient = createAuthClient({
    plugins: [
        passkeyClient() 
    ]
})

使用方法

添加/注册 passkey 密钥

要添加或注册 passkey 密钥,确保用户已认证,然后调用客户端提供的 passkey.addPasskey 函数。

POST/passkey/add-passkey
const { data, error } = await authClient.passkey.addPasskey({    name: "example-passkey-name",    authenticatorAttachment: "cross-platform",});
Parameters
namestring

可选名称,用于标记正在注册的认证器账户。如果未提供,默认使用用户的邮件地址或用户 ID。

authenticatorAttachment"platform" | "cross-platform"

你也可以指定要注册的认证器类型。默认行为允许平台和跨平台的 passkey。

在 fetch 选项中设置 throw: true 对注册和登录的 passkey 响应无效——它们始终返回包含错误对象的数据对象。

使用 passkey 登录

使用 passkey 登录可以调用 signIn.passkey 方法。这会提示用户使用其 passkey 登录。

POST/sign-in/passkey
const { data, error } = await authClient.signIn.passkey({    autoFill: true,});
Parameters
autoFillboolean

浏览器自动填充,亦称为条件 UI。更多信息:https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui

示例用法

import { authClient } from "@/lib/auth-client";

// 登录后跳转
await authClient.signIn.passkey({
    autoFill: true,
    fetchOptions: {
        onSuccess(context) {
            // 身份验证成功后跳转到仪表盘
            window.location.href = "/dashboard";
        },
        onError(context) {
            // 处理身份验证错误
            console.error("身份验证失败:", context.error.message);
        }
    }
});

列出 passkey

您可以通过调用 passkey.listUserPasskeys 列出所有已认证用户的 passkey:

GET/passkey/list-user-passkeys
const { data: passkeys, error } = await authClient.passkey.listUserPasskeys();

删除 passkey

您可以通过调用 passkey.delete 并提供 passkey ID 来删除 passkey。

POST/passkey/delete-passkey
const { data, error } = await authClient.passkey.deletePasskey({    id: "some-passkey-id", // required});
Parameters
idstringrequired

要删除的 passkey 的 ID。

更新 passkey 名称

POST/passkey/update-passkey
const { data, error } = await authClient.passkey.updatePasskey({    id: "id of passkey", // required    name: "my-new-passkey-name", // required});
Parameters
idstringrequired

您想要更新的 passkey 的 ID。

namestringrequired

passkey 将更新为的新名称。

条件 UI

插件支持条件 UI,允许浏览器在用户已注册 passkey 时自动填充。

条件 UI 要正常工作需要两个条件:

更新输入字段

为您的输入字段添加 autocomplete 属性,值为 webauthn。您可以为多个输入字段添加此属性,但至少一个是条件 UI 生效的前提。

webauthn 值应为 autocomplete 属性中的最后一项。

<label for="name">用户名:</label>
<input type="text" name="name" autocomplete="username webauthn">
<label for="password">密码:</label>
<input type="password" name="password" autocomplete="current-password webauthn">

预加载 passkey

组件挂载时,可以通过调用 authClient.signIn.passkey 方法并设置 autoFill 选项为 true 来预加载用户的 passkey。

为避免不必要调用,我们还会检查浏览器是否支持条件 UI。

useEffect(() => {
   if (!PublicKeyCredential.isConditionalMediationAvailable ||
       !PublicKeyCredential.isConditionalMediationAvailable()) {
     return;
   }

  void authClient.signIn.passkey({ autoFill: true })
}, [])

根据浏览器不同,会出现提示以自动填充 passkey。如果用户有多个 passkey 可以选择想用的那个。

某些浏览器还需要用户先与输入框互动,提示才会弹出。

调试

测试 passkey 实现可以使用模拟认证器。这样您可以测试注册和登录流程,无需真实设备。

数据库架构

该插件需要在数据库中新建一张表来存储 passkey 数据。

表名:passkey

Table
字段
类型
描述
id
string
pk
每个 passkey 的唯一标识
name
string
?
passkey 的名称
publicKey
string
passkey 的公钥
userId
string
fk
用户的 ID
credentialID
string
注册凭据的唯一标识
counter
number
passkey 的计数器
deviceType
string
用于注册 passkey 的设备类型
backedUp
boolean
passkey 是否已备份
transports
string
?
用于注册 passkey 的传输方式
createdAt
Date
?
passkey 创建时间
aaguid
string
?
认证器的认证 GUID,指示认证器类型

选项

rpID:基于您的身份验证服务器源的唯一网站标识符。开发环境可用 'localhost'。RP ID 可通过舍弃有效顶级域名左侧的零个或多个标签而形成,例如 www.example.com 可使用 www.example.comexample.com 作为 RP ID,但不能使用顶级域名 com

rpName:您网站的人类可读名称。

origin:您的 better-auth 服务器所在的源 URL,例如 http://localhosthttp://localhost:PORT。请勿包含尾部斜杠。

authenticatorSelection:允许自定义 WebAuthn 认证器选择标准。若不指定则为默认设置。

  • authenticatorAttachment:指定认证器类型
    • platform:认证器连接于平台(例如指纹识别器)
    • cross-platform:认证器非平台附加(例如安全密钥)
    • 默认:未设置(允许平台和跨平台,平台优先)
  • residentKey:决定凭据存储行为。
    • required:用户必须将凭据存储于认证器(最高安全级别)
    • preferred:鼓励存储凭据,但非强制
    • discouraged:不要求存储凭据(最快体验)
    • 默认:preferred
  • userVerification:控制认证时的生物识别 / PIN 验证:
    • required:用户必须验证身份(最高安全)
    • preferred:建议验证,但非强制
    • discouraged:无需验证(最快体验)
    • 默认:preferred

advanced:高级选项

  • webAuthnChallengeCookie:认证流程中存储 WebAuthn 挑战 ID 的 cookie 名称(默认:better-auth-passkey

Expo 集成

使用 passkey 插件和 Expo 时,需要在 Expo 客户端配置 cookiePrefix 选项,以确保正确检测和存储 passkey cookies。

默认情况下,passkey 插件使用 "better-auth-passkey" 作为挑战 cookie 名称。该名称以 "better-auth" 开头,因此适用于 Expo 客户端默认配置。但如果自定义了 webAuthnChallengeCookie,则必须同步更新 Expo 客户端配置中的 cookiePrefix

示例配置

如果你使用了自定义 cookie 名称:

服务器端: auth.ts
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";

export const auth = betterAuth({
    plugins: [
        passkey({
            advanced: {
                webAuthnChallengeCookie: "my-app-passkey" // 自定义 cookie 名称
            }
        })
    ]
});

确保在 Expo 客户端配置中匹配该前缀:

客户端: auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import { passkeyClient } from "@better-auth/passkey/client";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            cookiePrefix: "my-app" // 必须与自定义 cookie 名称的前缀匹配
        }),
        passkeyClient()
    ]
});

如果使用多种认证系统或自定义多个 cookie 名称,也可以传递前缀数组:

客户端: auth-client.ts
expoClient({
    storage: SecureStore,
    cookiePrefix: ["better-auth", "my-app", "custom-auth"]
})

如果 cookiePrefix 与您的 webAuthnChallengeCookie 的前缀不匹配,passkey 认证流程将会失败,因为挑战 cookie 无法存储并在验证时发送回服务器。

欲了解更多 Expo 集成信息,请参见 Expo 文档