用户与账户

了解如何管理用户和账户,包括更新用户信息、更改邮箱和密码、验证删除用户、令牌加密以及账户关联与解除关联。

除了认证用户外,Better Auth 还提供了一组管理用户的方法。这包括更新用户信息、更改密码等。

用户表存储用户的认证数据 点击此处查看模式

用户表可以通过附加字段或插件进行扩展,以存储额外的数据。

更新用户

更新用户信息

要更新用户信息,可以使用客户端提供的 updateUser 函数。updateUser 函数接收一个具有以下属性的对象:

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

await authClient.updateUser({
    image: "https://example.com/image.jpg",
    name: "John Doe",
})

更改电子邮件

更改电子邮件

要允许用户更改他们的邮箱,首先需要启用默认关闭的 changeEmail 功能。将 changeEmail.enabled 设置为 true

auth.ts
import { betterAuth } from "better-auth";
import { sendEmail } from './email'; // 你的发送邮件函数

export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
        }
    },
    emailVerification: {
        // 发送验证邮件所需
        sendVerificationEmail: async ({ user, url, token }) => {
            void sendEmail({
                to: user.email,
            })
        }
    }
})

避免等待邮件发送,以防止 定时攻击。在无服务器平台上,使用 waitUntil 或类似机制确保邮件已发送。

默认情况下,当用户请求更改邮箱时,验证邮件会发送到新的邮箱地址。 用户验证新邮箱后,邮箱才会被更新。

使用当前邮箱确认

为了增加安全性,你可以在验证邮件发送到新地址之前,要求用户通过当前邮箱确认更改。为此,请提供 sendChangeEmailConfirmation 函数。

auth.ts
import { betterAuth } from "better-auth";
import { sendEmail } from './email'; // 你的发送邮件函数

export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
            sendChangeEmailConfirmation: async ({ user, newEmail, url, token }, request) => { 
                void sendEmail({
                    to: user.email, // 发送到当前邮箱
                    subject: '批准邮箱更改',
                    text: `点击链接批准将邮箱更改为 ${newEmail}: ${url}`
                })
            }
        }
    },
    // ...
})

无需验证即可更新

如果你允许用户立即更新邮箱(仅当当前邮箱未验证时),可以启用 updateEmailWithoutVerification

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        changeEmail: {
            enabled: true,
            updateEmailWithoutVerification: true
        }
    }
})

如果 updateEmailWithoutVerification 为 false(默认值),则在新邮箱验证之前不会更新邮箱,即使当前邮箱未验证。

客户端用法

在客户端使用 changeEmail 函数启动流程。

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

await authClient.changeEmail({
    newEmail: "[email protected]",
    callbackURL: "/dashboard", // 验证后跳转
});

更改密码

用户的密码不会存储在用户表中。相反,它存储在账户表中。要更改用户密码,可以使用以下方法之一:

POST/change-password
const { data, error } = await authClient.changePassword({    newPassword: "newpassword1234", // required    currentPassword: "oldpassword1234", // required    revokeOtherSessions: true,});
Parameters
newPasswordstringrequired

要设置的新密码

currentPasswordstringrequired

当前用户密码

revokeOtherSessionsboolean

设置为 true 时,此用户的所有其他活动会话将被失效

设置密码

如果用户通过 OAuth 或其他提供者注册,则不会有密码或凭证账户。在这种情况下,可以使用 setPassword 操作为用户设置密码。出于安全考虑,此函数只能在服务器端调用。建议用户通过“忘记密码”流程来设置密码。

set-password.ts
import { auth } from "@/lib/auth"

await auth.api.setPassword({
    body: {
        newPassword: "new-password",
    },
    headers: await headers() // 包含用户会话令牌的请求头
});

验证密码

验证密码

verifyPassword 函数允许验证用户当前密码。适用于在执行敏感操作(如更新安全设置)前确认用户身份。此函数只能在服务器端调用。

verify-password.ts
import { auth } from "@/lib/auth"

await auth.api.verifyPassword({
    body: {
        password: "user-password" // 必填
    },
    headers: await headers() // 包含用户会话令牌的请求头
});

对于没有密码的 OAuth 用户,建议使用电子邮件验证或新鲜会话检查来替代敏感操作。

删除用户

Better Auth 提供了一个工具,可以从数据库硬删除用户。默认禁用,但可以通过传递 enabled: true 轻松启用。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    //...其他配置
    user: {
        deleteUser: { 
            enabled: true
        } 
    }
})

启用后,可以调用 authClient.deleteUser 永久删除数据库中的用户数据。

删除前添加验证

为了增加安全性,你可能希望在删除账户之前确认用户的意图。一种常见方法是发送验证邮件。Better Auth 提供了 sendDeleteAccountVerification 工具来实现此目的。 这在你已设置 OAuth 并且希望用户无需重新登录即可删除账户时尤其需要。

配置示例如下:

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            sendDeleteAccountVerification: async (
                {
                    user,   // 用户对象
                    url,    // 自动生成的删除链接
                    token   // 验证令牌(可用于生成自定义链接)
                },
                request  // 原始请求对象(可选)
            ) => {
                // 你的邮件发送逻辑
                // 例如:sendEmail(user.email, "验证删除", url);
            },
        },
    },
});

回调验证工作原理:

  • 回调 URL: sendDeleteAccountVerification 中提供的 URL 是一个预生成的链接,访问后会删除用户数据。
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser({
    callbackURL: "/goodbye" // 可提供删除后跳转的 URL
});
  • 身份验证检查: 用户必须登录到其尝试删除的账户。 如果未登录,删除过程将失败。

如果已发送自定义 URL,则可以通过带有 token 的 deleteUser 方法删除用户。

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

await authClient.deleteUser({
    token
});

认证要求

删除用户需满足以下条件之一:

  1. 有效密码

如果用户有密码,提供密码即可删除账户。

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

await authClient.deleteUser({
    password: "password"
});
  1. 新鲜会话

用户需要拥有一个“新鲜”会话令牌,即最近登录过。如果未提供密码,则会检查此条件。

默认情况下 session.freshAge 设置为 60 * 60 * 24(1 天)。你可以通过向 auth 配置传递 session 对象来更改此值。如果设置为 0,新鲜度检查将被禁用。如果你未使用电子邮件验证来删除账户,建议不要禁用此检查。

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

await authClient.deleteUser();
  1. 启用邮箱验证(用于 OAuth 用户)

OAuth 用户没有密码,需要发送验证邮件来确认删除意愿。如果已添加 sendDeleteAccountVerification 回调,调用 deleteUser 无需其他信息即可完成。

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

await authClient.deleteUser();
  1. 如果你有一个自定义删除账户页面,并通过 sendDeleteAccountVerification 回调发送了该 URL。 那么你需要使用 token 调用 deleteUser 方法来完成删除。
import { authClient } from "@/lib/auth-client"

await authClient.deleteUser({
    token
});

回调函数

validateUserInfo:用于决定 Better Auth 接受哪些身份的关卡。它会在创建用户(create-user)或关联新提供者账户(link-account)之前立即触发,适用于每种认证方式(OAuth、OIDC SSO、SAML SSO、电子邮件/密码、magic link、email OTP、匿名、SIWE、电话号码、管理员创建的用户和 SCIM),也适用于没有持久化数据库的无状态设置,因此策略可以集中在一个地方,而不必按提供者分别配置。

当已有的 OAuth 或 SSO 用户再次登录(sign-in)时,它也会触发;此时接收的是提供者最新的电子邮件和资料,而不是存储的记录。这使得你可以拒绝其提供者身份已超出范围的用户,例如电子邮件地址不再属于允许的域名。对于非提供者的回访登录不会重新验证,因为其存储记录自创建用户时通过检查后并未改变;如需阻止这些登录,请使用 admin 插件的封禁控制,或使用 databaseHooks.session.create.before 钩子。

source.action 的值为 "create-user""link-account""sign-in",而 source.method 是认证方式。对于 OAuth,source.oauth 包含提供者 id 和原始提供者资料。对于 OIDC 和 SAML SSO,source.sso 包含 SSO 提供者 id 以及原始提供者声明或断言属性。

不返回任何内容即可允许创建。返回包含 error 的对象即可拒绝:浏览器/重定向流程会将拒绝信息发送到配置的错误 URL,而编程式流程会返回 403 API 错误。避免在 errorDescription 中放入敏感详情,因为它会返回给客户端。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        validateUserInfo: ({ user, source }) => {
            if (!user.email?.endsWith("@example.com")) {
                return {
                    error: "email_not_allowed",
                    errorDescription: "Use your example.com email to sign in",
                };
            }

            if (
                source.oauth?.providerId === "company-oauth" &&
                source.oauth?.profile?.hd !== "example.com"
            ) {
                return {
                    error: "invalid_organization",
                    errorDescription: "Use your company OAuth account",
                };
            }
        },
    },
});

validateUserInfo 是高级策略关卡。较底层的 databaseHooks.user.create.before 仍会在之后运行,用于调整数据,也可以中止写入;当你需要修改记录,而不是接受或拒绝身份时,应使用它。

beforeDelete:此回调会在删除用户之前调用。你可以使用此回调在删除用户前执行清理或额外检查。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user) => {
                // 在这里执行清理或额外检查
            },
        },
    },
});

你也可以抛出 APIError 来中断删除过程。

auth.ts
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user, request) => {
                if (user.email.includes("admin")) {
                    throw new APIError("BAD_REQUEST", {
                        message: "管理员账户无法删除",
                    });
                }
            },
        },
    },
});

afterDelete:此回调在删除用户后调用。可用于执行清理或其他操作。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            afterDelete: async (user, request) => {
                // 在这里执行清理或其他操作
            },
        },
    },
});

账户

Better Auth 通过电子邮件和密码、Google 或企业身份提供者等提供者支持多种认证方式。与用户关联的每种方式都会作为一个账户存储。

账户包含本地记录 ID 和提供者身份。id 用于标识 Better Auth 账户记录,也是向账户管理 API 传递的 accountId 值。issueraccountId 的组合用于标识外部账户:issuer 表示受信任的机构,而 accountId 是该机构分配的稳定标识符。providerId 用于标识 Better Auth 在协议操作中使用的提供者配置。

没有受信任发行者的 OAuth 提供者使用 local:oauth:<encoded providerId> 作为其账户命名空间,其中提供者 ID 部分经过百分号编码。凭证账户使用 local:credential;不要将此凭证命名空间用于 OAuth 提供者。

这种分离使同一发行者的多个提供者配置能够对同一外部身份进行去重,同时不会将来自另一发行者的标识符视为同一个人。提供者别名共享同一个账户记录和令牌集;它们没有独立的授权或提供者生命周期记录。完整字段列表请参阅账户模式

列出用户账户

使用 listAccounts 获取与当前用户关联的所有认证方式。当需要解除账户关联或调用其他特定于账户的 API 时,请保留返回结果中的 id

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

const { data: accounts, error } = await authClient.listAccounts();

if (error) {
    throw new Error(error.message);
}

const googleAccount = accounts?.find((account) => account.providerId === "google");

令牌加密

Better Auth 默认不加密令牌,这是有意而为。我们希望你对加密和解密的方式拥有完全控制权,而不是内置可能混淆或限制的行为。如果你需要存储加密的令牌(例如 accessToken 或 refreshToken),可以使用数据库钩子(databaseHooks)在保存前进行加密。

import { betterAuth } from "better-auth";

export const auth = betterAuth({
    databaseHooks: {
        account: {
            create: {
                before(account, context) {
                    const withEncryptedTokens = { ...account };
                    if (account.accessToken) {
                        const encryptedAccessToken = encrypt(account.accessToken)  
                        withEncryptedTokens.accessToken = encryptedAccessToken;
                    }
                    if (account.refreshToken) {
                        const encryptedRefreshToken = encrypt(account.refreshToken); 
                        withEncryptedTokens.refreshToken = encryptedRefreshToken;
                    }
                    return {
                        data: withEncryptedTokens
                    }
                },
            }
        }
    }
})

然后每次取回账户时,记得先解密令牌再使用。

账户关联

账户关联在默认启用,允许用户将多种认证方式关联到同一账户。使用 Better Auth,用户可为现有账户连接额外的社交登录或 OAuth 提供者(前提是提供者确认用户邮箱已验证)。

如果禁用账户关联,则不论提供者或邮箱验证状态如何,均无法关联账户。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: false, 
        }
    },
});

强制关联

你可以指定一组“受信任提供者”。当用户通过该类提供者登录时,即使提供者未确认邮箱验证状态,其账户也会自动关联。请慎用此功能,因可能增加账户被接管风险。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            enabled: true,
            trustedProviders: ["google", "github"]
        }
    },
});

禁用隐式关联

默认情况下,当用户使用邮箱与现有用户匹配的 OAuth 提供者登录时(且该提供者已验证邮箱或位于 trustedProviders 中),Better Auth 会自动将该 OAuth 账户关联到该用户。设置 disableImplicitLinking: true 可关闭此行为。启用后:

  • 对于已有用户,同邮箱的 OAuth 登录会被拒绝,并返回 account_not_linked 错误,而不是被静默关联,即使提供者位于 trustedProviders 中或邮箱已验证也是如此。
  • 新用户(没有该邮箱对应的现有用户)仍然可以通过 OAuth 注册。
  • 已认证的用户仍可通过 linkSocial() 显式关联提供者。

当你希望用户在设置页面中确认关联,而不是在登录时自动完成关联时,可使用此选项。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            disableImplicitLinking: true,
        }
    },
});

手动关联账户

已登录用户可手动将账户关联到额外的社交提供者或凭证账户。

  • 关联社交账户: 使用客户端的 linkSocial 方法将一个社交提供者关联到用户的账户。

    import { authClient } from "@/lib/auth-client"
    
    await authClient.linkSocial({
        provider: "google", // 要关联的提供者
        callbackURL: "/callback" // 关联完成后的回调 URL
    });

    你也可以在关联社交账户时请求特定权限范围,与初次认证时的权限不同:

    import { authClient } from "@/lib/auth-client"
    
    await authClient.linkSocial({
        provider: "google",
        callbackURL: "/callback",
        scopes: ["https://www.googleapis.com/auth/drive.readonly"] // 请求额外权限
    });

    新授予的权限范围会合并到 account.scope 中,因此之前授予的权限仍会保留。重新进行登录认证和刷新令牌响应不会修改 account.scope

    你也可以直接使用 ID 令牌关联账户,而无需重定向到提供者的 OAuth 流程:

    import { authClient } from "@/lib/auth-client"
    
    await authClient.linkSocial({
        provider: "google",
        idToken: {
            token: "id_token_from_provider",
            nonce: "nonce_used_for_token", // 可选
            accessToken: "access_token", // 可选,部分提供者需要
            refreshToken: "refresh_token" // 可选
        }
    });

    这在以下情况下非常有用:

    • 在使用原生 SDK 登录后
    • 当使用处理身份验证的移动应用程序
    • 当实现自定义 OAuth 流程时

    ID 令牌必须有效且提供者支持验证。

    如果希望用户可以用与账户邮箱不同的邮箱关联社交账户,或使用不返回邮箱的提供者,需要在账户关联设置中启用此功能。

    如果希望新关联的账户更新用户信息,需要在账户关联设置中启用此功能。

    auth.ts
    import { betterAuth } from "better-auth";
    
    export const auth = betterAuth({
        account: {
            accountLinking: {
                allowDifferentEmails: true
            }
        },
    });

    默认情况下,关联账户不会影响现有用户资料。启用 updateUserInfoOnLink 后,每次关联账户时都会将提供者的资料复制到用户。同步的字段与注册时持久化的字段相同(nameimage,以及 mapProfileToUser 添加的任何允许输入字段)。用户的 emailemailVerified 在关联时永远不会被更改,因此关联提供者不会重新绑定该账户的身份。

    auth.ts
    import { betterAuth } from "better-auth";
    
    export const auth = betterAuth({
        account: {
            accountLinking: {
                updateUserInfoOnLink: true
            }
        },
    });
  • 关联基于凭证的账户: 要关联基于凭证的账户(例如邮箱和密码),用户可以发起“忘记密码”流程,或者你可以在服务器端调用 setPassword 方法。

    import { auth } from "@/lib/auth"
    
    await auth.api.setPassword({
      body: {
          newPassword: "new-password", // 必填
      },
      headers: await headers() // 包含用户会话令牌的请求头
    });

setPassword 不能从客户端调用,出于安全考虑。

账户解除关联

通过传递 Better Auth 账户记录的 id 来解除账户关联,该值可以从 listAccounts 获取。

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

const { data: accounts, error } = await authClient.listAccounts();

if (error) {
    throw new Error(error.message);
}

const account = accounts?.find((account) => account.providerId === "google");

if (account) {
    await authClient.unlinkAccount({
        accountId: account.id,
    });
}

如果账户不存在或不属于当前用户,Better Auth 会返回错误。除非 allowUnlinkingAlltrue,否则 Better Auth 还会阻止用户解除其唯一账户的关联。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
    account: {
        accountLinking: {
            allowUnlinkingAll: true
        }
    },
});