---
description: "三种主流登录形态放在同一形状下：邮箱+密码是默认底座，社交 OAuth 是「一键继续」，Magic Link 是无密码替补。蓝本的策略是把它们共栈到 better-auth 同一份 plugins 数组，前端各出一段小组件（SocialButtons / magic-link 表单）"
---

# 登录方式（邮箱密码 / 社交 OAuth / Magic Link）

三种主流登录形态放在同一形状下：邮箱+密码是默认底座，社交 OAuth 是「一键继续」，Magic Link 是无密码替补。蓝本的策略是把它们**共栈**到 `better-auth` 同一份 `plugins` 数组，前端各出一段小组件（`SocialButtons` / magic-link 表单），共用一个 `authClient` 客户端句柄。

<iframe src="/skills/dashboard/assets/demo-auth-method.html"
        style="width:100%;height:640px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：登录方式"></iframe>

试这几下：

- 顶部分段控制切换**邮箱密码 / 社交 / Magic Link**；若当前面板已经输入，切换会先弹确认
- 邮箱密码面板：邮箱格式与密码 ≥6 位在 blur 后就地校验；密码填 `wrong` 提交，顶部横幅出**「邮箱或密码错误」**
- 提交与「跳转」都有 loading 态 —— 按钮禁用、转圈、文本切**「正在…」**；社交面板同时最多一个 provider pending，其它按钮禁用
- Magic Link 提交后进入**「检查邮箱」**终态，只显示邮箱地址与「换个邮箱」；换邮箱前弹确认，避免误清

## 规矩

- **错误分两层**：格式类客户端错误（邮箱格式 / 密码长度）在字段 touched 后就近显示；服务端拒绝（凭据错、账号锁）走表单顶部横幅。demo 用 `pwTop` 挂横幅，真实版把 `authClient.signIn.email()` 返回的 `error.message` 塞进去
- **提交按钮无效或提交中即禁用，文本切「正在…」**：单一按钮承载全部状态，杜绝「提交中还能再点一次」的双提；社交面板同时只允许一个 provider pending，避免叠发 OAuth 跳转
- **共栈的插件顺序不能乱**：蓝本约定 `tanstackStartCookies()` 必须是 `plugins` 数组最后一项（它负责刷 `Set-Cookie`），`magicLink()` 与其它插件都插在 `// auth-plugins:anchor` 之上；对应地每个插件需要的 in-memory model（如 `verification`）必须在 `memoryStore` 里存在
- **demo 三种形态一个页面切换，真实版每种走独立 flow**：密码字段永远走 https + 服务端 hash（never plain）；magic link 走一次性 token + 服务端节流防滥用；社交登录跳去 provider 授权，回跳落到 `<BETTER_AUTH_URL>/api/auth/callback/<provider>`
- **浏览器只能通过 `authClient` 说话**：`@/lib/auth` 是服务端句柄，任何客户端可达模块里 `import` 它，等于把服务端密钥打进 bundle。demo 全内存态不涉及这条界，真实版这条铁

> 蓝本：`add-auth-method.md`[^add-auth-method]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-auth-method.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-auth-method.html"
    ```

[^add-auth-method]: [`add-auth-method.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-auth-method.md)，2026-07-28 读。
