# 上次登录方式 (/docs/plugins/last-login-method)

跟踪并显示用户上次使用的身份验证方式



上次登录方式插件用于跟踪用户最近一次使用的身份验证方式（邮箱、OAuth 提供商等）。这使您能够在登录页面上显示有用的指示标志，例如“上次使用 Google 登录”，或根据用户偏好优先显示某些登录方式。

## 安装 [#安装]

<Steps>
  <Step>
    ### 将插件添加到您的身份验证配置 [#将插件添加到您的身份验证配置]

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { lastLoginMethod } from "better-auth/plugins" // [!code highlight]

    export const auth = betterAuth({
        // ... 其他配置选项
        plugins: [
            lastLoginMethod() // [!code highlight]
        ]
    })
    ```
  </Step>

  <Step>
    ### 将客户端插件添加到您的身份验证客户端 [#将客户端插件添加到您的身份验证客户端]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client"
    import { lastLoginMethodClient } from "better-auth/client/plugins" // [!code highlight]

    export const authClient = createAuthClient({
        plugins: [
            lastLoginMethodClient() // [!code highlight]
        ]
    })
    ```
  </Step>
</Steps>

## 使用方法 [#使用方法]

安装后，插件会自动跟踪用户最后一次使用的身份验证方式。您可以在应用程序中检索并显示该信息。

### 获取最后使用的方式 [#获取最后使用的方式]

客户端插件提供了多个方法来操作最后登录的方式：

```ts title="app.tsx"
import { authClient } from "@/lib/auth-client"

// 获取最后使用的登录方式
const lastMethod = authClient.getLastUsedLoginMethod()
console.log(lastMethod) // "google", "email", "github" 等等

// 检查某种方式是否为最后使用
const wasGoogle = authClient.isLastUsedLoginMethod("google")

// 清除存储的登录方式
authClient.clearLastUsedLoginMethod()
```

### UI 集成示例 [#ui-集成示例]

下面展示如何使用该插件增强登录页面：

```tsx title="sign-in.tsx"
import { authClient } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"

export function SignInPage() {
    const lastMethod = authClient.getLastUsedLoginMethod()
    
    return (
        <div className="space-y-4">
            <h1>Sign In</h1>

            {/* Email sign in */}
            <div className="relative">
                <Button 
                    onClick={() => authClient.signIn.email({...})}
                    variant={lastMethod === "email" ? "default" : "outline"}
                    className="w-full"
                >
                    使用邮箱登录
                    {lastMethod === "email" && (
                        <Badge className="ml-2">上次使用</Badge>
                    )}
                </Button>
            </div>
            
            {/* OAuth 提供商 */}
            <div className="relative">
                <Button 
                    onClick={() => authClient.signIn.social({ provider: "google" })}
                    variant={lastMethod === "google" ? "default" : "outline"}
                    className="w-full"
                >
                    使用 Google 登录
                    {lastMethod === "google" && (
                        <Badge className="ml-2">上次使用</Badge>
                    )}
                </Button>
            </div>
            
            <div className="relative">
                <Button 
                    onClick={() => authClient.signIn.social({ provider: "github" })}
                    variant={lastMethod === "github" ? "default" : "outline"}
                    className="w-full"
                >
                    使用 GitHub 登录
                    {lastMethod === "github" && (
                        <Badge className="ml-2">上次使用</Badge>
                    )}
                </Button>
            </div>
        </div>
    )
}
```

## 数据库持久化 [#数据库持久化]

默认情况下，最后登录方式仅存储在 cookie 中。若需更持久的跟踪和分析，可以启用数据库存储。

<Steps>
  <Step>
    ### 启用数据库存储 [#启用数据库存储]

    在插件配置中将 `storeInDatabase` 设置为 `true`：

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth"
    import { lastLoginMethod } from "better-auth/plugins"

    export const auth = betterAuth({
        plugins: [
            lastLoginMethod({
                storeInDatabase: true // [!code highlight]
            })
        ]
    })
    ```
  </Step>

  <Step>
    ### 运行数据库迁移 [#运行数据库迁移]

    插件会自动将 `lastLoginMethod` 字段添加到您的用户表中。运行迁移以应用更改：

    <Tabs items="[&#x22;迁移&#x22;, &#x22;生成&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth@latest migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth@latest generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>
  </Step>

  <Step>
    ### 访问数据库字段 [#访问数据库字段]

    启用数据库存储后，`lastLoginMethod` 字段将在用户对象中可用：

    ```ts title="user-profile.tsx"
    import { auth } from "@/lib/auth"

    // 服务器端访问
    const session = await auth.api.getSession({ headers })
    console.log(session?.user.lastLoginMethod) // "google", "email", 等等

    // 通过会话进行客户端访问
    const { data: session } = authClient.useSession()
    console.log(session?.user.lastLoginMethod)
    ```
  </Step>
</Steps>

### 数据库模式 [#数据库模式]

启用 `storeInDatabase` 后，插件会在 `user` 表中添加如下字段：

表：`user`



<DatabaseTable name="user" fields="lastLoginMethodUserTableFields" />

### 自定义模式配置 [#自定义模式配置]

你可以自定义数据库字段名：

```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        lastLoginMethod({
            storeInDatabase: true,
            schema: {
                user: {
                    lastLoginMethod: "last_auth_method" // 自定义字段名
                }
            }
        })
    ]
})
```

## 配置选项 [#配置选项]

上次登录方式插件接受以下选项：

### 服务端选项 [#服务端选项]

```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        lastLoginMethod({
            // Cookie 配置
            cookieName: "better-auth.last_used_login_method", // 默认: "better-auth.last_used_login_method"
            maxAge: 60 * 60 * 24 * 30, // 默认: 30 天，单位秒
            
            // 数据库存储
            storeInDatabase: false, // 默认: false
            
            // 自定义方式解析
            customResolveMethod: (ctx) => {
                // 自定义逻辑决定登录方式
                if (ctx.path === "/oauth/callback/custom-provider") {
                    return "custom-provider"
                }
                // 返回 null 使用默认解析
                return null
            },
            
            // GDPR compliance hook
	            beforeStoreCookie: async (ctx, lastUsedLoginMethod) => {
	                // Check if user has given consent for non-essential cookies
	                // Return false to prevent cookie storage
	                const hasConsent = await checkUserCookieConsent(ctx)
	                return hasConsent
	            },

	            // Schema customization (when storeInDatabase is true)
            schema: {
                user: {
                    lastLoginMethod: "custom_field_name"
                }
            }
        })
    ]
})
```

**cookieName**: `string`

* 用于存储最后登录方式的 Cookie 名称
* 默认: `"better-auth.last_used_login_method"`
* **注意**: 该 Cookie 设置为 `httpOnly: false`，以便客户端 JavaScript 访问用于 UI 功能

**maxAge**: `number`

* Cookie 过期时间（秒）
* 默认: `2592000`（30 天）

**storeInDatabase**: `boolean`

* 是否将最后登录方式存储到数据库
* 默认: `false`
* 启用后，会向用户表添加一个 `lastLoginMethod` 字段
* 如需仅将方式存储在数据库中（不使用 Cookie），可与 `beforeStoreCookie: () => false` 结合：

```ts title="auth.ts"
lastLoginMethod({
    storeInDatabase: true,
    beforeStoreCookie: () => false, // 从不设置非必要 Cookie
})
```

**customResolveMethod**: `(ctx: GenericEndpointContext) => string | null`

* 用于从请求上下文中确定登录方式的自定义函数
* 返回 `null` 使用默认解析逻辑
* 对自定义 OAuth 提供程序或身份验证流程很有用

**beforeStoreCookie**: `(ctx: GenericEndpointContext, lastUsedLoginMethod: string) => Promise<boolean> | boolean`

* 在存储上次登录方式 Cookie 之前运行的钩子函数
* 返回 `true` 允许设置 Cookie，返回 `false` 则阻止设置
* 适用于 GDPR 合规或其他要求 Cookie 存储需用户同意的法规
* 如果函数抛出错误，Cookie 将不会被设置（会记录错误，但不会中断身份验证）
* **示例**：在存储 Cookie 前检查用户同意

```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        lastLoginMethod({
            beforeStoreCookie: async (ctx, lastUsedLoginMethod) => {
                // 检查用户是否已同意非必要 Cookie
                // 这对 GDPR 合规很重要
                const hasConsent = await checkUserCookieConsent(ctx)
                return hasConsent
            }
        })
    ]
})
```

**schema**: `object`

* 启用 `storeInDatabase` 时自定义数据库字段名
* 允许将 `lastLoginMethod` 字段映射到自定义列名

### 客户端选项 [#客户端选项]

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { lastLoginMethodClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
    plugins: [
        lastLoginMethodClient({
            cookieName: "better-auth.last_used_login_method", // 默认: "better-auth.last_used_login_method"
            domain: ".example.com" // 跨子域清除 Cookie 所必需
        })
    ]
})
```

**cookieName**: `string`

* 用于读取最后登录方式的 Cookie 名称
* 必须与服务端的 `cookieName` 配置匹配
* 默认: `"better-auth.last_used_login_method"`

**domain**: `string`

* 用于清除 Cookie 的域
* 在使用 `crossSubDomainCookies` 时必需，这样客户端才能正确清除服务端设置的 Cookie
* 应与服务端 `crossSubDomainCookies` 配置中的 `domain` 值匹配

### 默认方式解析 [#默认方式解析]

插件默认跟踪以下身份验证方式：

* **电子邮件身份验证**: `"email"`
* **OAuth 提供程序**: 提供程序 ID（例如 `"google"`、`"github"`、`"discord"`）
* **OAuth 回调**: 来自 URL 路径的提供程序 ID
* **注册方式**: 与登录方式的跟踪方式相同

插件自动从以下端点检测方式：

* `/callback/:id` - 带有提供程序 ID 的 OAuth 回调
* `/sign-in/email` - 电子邮件登录
* `/sign-up/email` - 电子邮件注册
* `/one-tap/callback` - Google One Tap 登录，记录为 `"google"`
* `/siwe/verify` - 以太坊登录（Sign-In with Ethereum），记录为 `"siwe"`
* `/passkey/verify-authentication` - Passkey 登录，记录为 `"passkey"`
* `/magic-link/verify` - 魔法链接登录，记录为 `"magic-link"`
* `/sign-in/email-otp` - 电子邮件 OTP 登录，记录为 `"email-otp"`

## 跨域支持 [#跨域支持]

插件会自动继承 Better Auth 集中 Cookie 系统的 Cookie 设置，解决了最后登录方式无法在以下情况下持久化的问题：

* **跨子域设置**: `auth.example.com` → `app.example.com`
* **跨源设置**: `api.company.com` → `app.different.com`

当在 Better Auth 配置中启用 `crossSubDomainCookies` 或 `crossOriginCookies` 时，插件会自动使用与会话 Cookie 相同的域、secure 和 sameSite 设置，确保应用中的一致行为。

<Callout type="warn">
  当使用 `crossSubDomainCookies` 时，你必须向 `lastLoginMethodClient()` 传递 `domain` 选项，这样 `clearLastUsedLoginMethod()` 才能正确让 Cookie 过期。否则，浏览器会静默忽略清除请求，因为域名不匹配。
</Callout>

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth"
import { lastLoginMethodClient } from "better-auth/client/plugins"

export const authClient = createAuthClient({
    plugins: [
        lastLoginMethodClient({
            domain: ".example.com" // 必须与服务端 crossSubDomainCookies 的 domain 一致 // [!code highlight]
        })
    ]
})
```

## GDPR 合规 [#gdpr-合规]

最后登录方式 Cookie 在 GDPR 法规下被视为非必要 Cookie。
为了符合 GDPR 及类似隐私法规，只有在用户明确同意后，你才应该存储此 Cookie。

`beforeStoreCookie` 钩子允许你在存储 Cookie 之前实现同意检查：

```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        lastLoginMethod({
            beforeStoreCookie: async (ctx, lastUsedLoginMethod) => {
                // 示例 1：从会话或数据库中检查同意
                const session = await getSessionFromCtx(ctx)
                if (session?.user) {
                    // 用于访问数据库检查用户是否已同意的自定义函数
                    const userConsent = await checkUserConsent(session.user.id)
                    return userConsent?.allowsNonEssentialCookies ?? false
                }

                // 示例 2：从请求头（cookie 横幅）中检查同意
                // 当没有同意时，parseConsentCookie 应返回 false/null/undefined
                const consentCookie = ctx.request?.headers?.get("cookie")
                const hasConsent = parseConsentCookie(consentCookie)
                return !!hasConsent
            }
        })
    ]
})
```

<Callout type="warn">
  当 `beforeStoreCookie` 返回 `false` 或抛出错误时，Cookie 将不会被设置。认证仍然会正常成功 - 只会阻止 Cookie 的存储。
</Callout>

## 高级示例 [#高级示例]

### 自定义提供商跟踪 [#自定义提供商跟踪]

如果你有自定义的 OAuth 提供程序或身份验证方式，可以使用 `customResolveMethod` 选项：

```ts title="auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [
        lastLoginMethod({
            customResolveMethod: (ctx) => {
                // 跟踪自定义 SAML 提供程序
                if (ctx.path === "/saml/callback") {
                    return "saml"
                }
                
                // 跟踪 magic link 验证
                if (ctx.path === "/magic-link/verify") {
                    return "magic-link"
                }
                
                // 跟踪手机身份验证
                if (ctx.path === "/sign-in/phone") {
                    return "phone"
                }
                
                // 返回 null 使用默认逻辑
                return null
            }
        })
    ]
})
```

### 与 Expo 一起使用 [#与-expo-一起使用]

使用 Better Auth 配合 Expo 时，请确保从 `@better-auth/expo/plugins` 而非 `better-auth/plugins/client` 导入客户端插件。这确保最后登录方式能正确存储到配置的存储中。

```ts
import { createAuthClient } from "better-auth/react"
import { expoClient } from "@better-auth/expo"
import { lastLoginMethodClient } from "@better-auth/expo/plugins" // [!code highlight]
import * as SecureStore from "expo-secure-store"

export const authClient = createAuthClient({
  plugins: [
    expoClient({
      scheme: "myapp",
      storagePrefix: "myapp",
      storage: SecureStore,
    }),
    lastLoginMethodClient({
      storagePrefix: "myapp",
      storage: SecureStorage,
    })
  ]
})
```

<Callout type="info">
  仅在 Expo 应用中使用（不需要浏览器支持时），可以省略服务端插件，仅依赖客户端插件。
</Callout>

