# 会话管理 (/docs/concepts/session-management)

了解 Better Auth 中的会话管理，包括会话过期、新鲜度、Cookie 缓存策略、次级存储、无状态会话以及自定义会话响应。



Better Auth 使用传统的基于 Cookie 的会话管理机制。会话数据存储在 Cookie 中，并在每个请求时发送到服务器。服务器随后验证会话，若会话有效则返回用户数据。

主 `session_token` Cookie 仍然是服务器端的会话标识符。如果启用 `session.cookieCache`，Better Auth 还会写入一个单独的 `session_data` Cookie，用于短时缓存会话数据。此缓存 Cookie 与 JWT 插件的 `/token` 端点和 `set-auth-jwt` 标头输出相互独立。

## 会话表 [#会话表]

会话表用于存储会话数据，包含以下字段：

* `id`: 会话的唯一标识符。
* `token`: 会话令牌，也用作会话 Cookie。
* `userId`: 用户 ID。
* `expiresAt`: 会话过期时间。
* `ipAddress`: 用户的 IP 地址。
* `userAgent`: 用户的用户代理信息，存储请求中的用户代理头部。

## 会话过期 [#会话过期]

默认情况下，会话在 7 天后过期。但每当会话被使用且达到 `updateAge` 时间时，会话过期时间会更新为当前时间加上 `expiresIn` 值。

你可以通过在 `auth` 配置中传入 `session` 对象来修改 `expiresIn` 和 `updateAge` 的值：

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

export const auth = betterAuth({
    //... 其他配置选项
    session: {
        expiresIn: 60 * 60 * 24 * 7, // 7 天
        updateAge: 60 * 60 * 24 // 1 天（每隔 1 天更新一次会话过期时间）
    }
})
```

### 禁用会话刷新 [#禁用会话刷新]

你可以禁用会话刷新，使得无论 `updateAge` 怎样，会话都不会被更新。

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

export const auth = betterAuth({
    //... 其他配置选项
    session: {
        disableSessionRefresh: true
    }
})
```

### 延迟会话刷新 [#延迟会话刷新]

默认情况下，`GET /get-session` 会执行数据库写操作以刷新会话。这在读副本数据库环境中可能产生问题，因为 GET 请求会路由到只读副本。

启用延迟刷新后，GET 请求变为只读，需刷新时返回 `needsRefresh: true`，客户端会自动调用 POST 进行刷新。

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

export const auth = betterAuth({
    session: {
        deferSessionRefresh: true
    }
})
```

## 会话新鲜度 [#会话新鲜度]

部分 Better Auth 接口要求会话必须是 **新鲜的**。如果会话的 `createdAt` 在 `freshAge` 限制范围内，则该会话被视为新鲜。默认情况下，`freshAge` 设置为 **1 天**（60 \* 60 \* 24）。

你可以通过在 `auth` 配置中传入 `session` 对象来自定义 `freshAge`：

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

export const auth = betterAuth({
    //... 其他配置选项
    session: {
        freshAge: 60 * 5 // 5 分钟（会话在最近 5 分钟内创建即视为新鲜）
    }
})
```

要 **禁用新鲜度检查**，将 `freshAge` 设置为 `0`：

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

export const auth = betterAuth({
    //... 其他配置选项
    session: {
        freshAge: 0 // 禁用新鲜度检查
    }
})
```

## 会话管理 [#会话管理]

Better Auth 提供了一组函数来管理会话。

### 获取会话 [#获取会话]

`getSession` 函数用于获取当前活跃的会话。

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

const { data: session } = await authClient.getSession()
```

有关自定义会话响应的说明，请参阅[自定义会话响应](#customizing-session-response)部分。

### 使用会话 [#使用会话]

`useSession` 操作提供了一种响应式访问当前会话的方式。

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

const { data: session } = authClient.useSession()
```

### 列出会话 [#列出会话]

`listSessions` 函数返回用户的所有活跃会话列表。

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

const sessions = await authClient.listSessions()
```

### 撤销会话 [#撤销会话]

用户在一台设备上登出时，会话会自动结束。你也可以在任何已登录设备上手动结束指定会话。

调用 `revokeSession` 函数并传入会话令牌即可结束该会话。

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

await authClient.revokeSession({
    token: "session-token"
})
```

### 撤销其他会话 [#撤销其他会话]

要撤销除当前会话之外的所有其他会话，可以使用 `revokeOtherSessions` 函数。

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

await authClient.revokeOtherSessions()
```

### 撤销所有会话 [#撤销所有会话]

要撤销所有会话，可以使用 `revokeSessions` 函数。

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

await authClient.revokeSessions()
```

### 更新会话 [#更新会话]

如果你为会话配置了[额外字段](/docs/concepts/database#扩展核心架构)，可以使用 `updateSession` 函数来更新它们。

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

await authClient.updateSession({
    theme: "dark",
    language: "en",
})
```

核心会话字段（`token`、`userId`、`expiresAt`、`createdAt`、`updatedAt`、`ipAddress`、`userAgent`）不能通过此接口更新，仅允许修改自定义附加字段。

服务器端示例：

```ts title="server.ts"
await auth.api.updateSession({
    body: {
        theme: "dark",
    },
    headers: await headers() // 包含用户会话令牌的请求头
});
```

### 修改密码时撤销会话 [#修改密码时撤销会话]

可以在用户修改密码时撤销所有会话，只需在 `changePassword` 函数中传入 `revokeOtherSessions: true`。

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

await authClient.changePassword({
    newPassword: newPassword,
    currentPassword: currentPassword,
    revokeOtherSessions: true,
})
```

对于存储在数据库中的会话，如果适配器启用了事务，密码修改与会话撤销会在一个事务中完成，并先撤销会话。撤销失败时，请求会返回错误，当前密码保持不变。未启用事务时，如果保存新密码失败，当前密码仍然有效，但会话已经撤销。使用[二级存储](/docs/concepts/database#secondary-storage)时，只有事务提交后才会从二级存储中删除会话。删除失败会记录日志，新密码仍会生效，而这些会话可能会一直有效到过期。

## 会话缓存 [#会话缓存]

### Cookie 缓存 [#cookie-缓存]

每次调用 `useSession` 或 `getSession` 时访问数据库并不理想，尤其是会话不常变化时。Cookie 缓存通过将会话数据存储在短时效、已签名的 Cookie 中来解决这个问题——类似使用 JWT 访问令牌和刷新令牌的模式。

启用 Cookie 缓存后，服务器可以直接从 Cookie 验证会话，而不必每次访问数据库。Cookie 是经过签名的以防止篡改，且较短的 `maxAge` 使会话数据定期刷新。如果会话被撤销或过期，Cookie 会自动失效。

要启用 Cookie 缓存，只需在身份验证配置中设置 `session.cookieCache`：

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

export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60 // 缓存时长，单位秒（5 分钟）
        }
    }
});
```

<Callout type="info">
  **注意事项**

  当启用 `cookieCache` 时，已撤销的会话可能在其他设备上保持活跃状态，直到 Cookie 缓存过期（`maxAge`）。原因如下：

  * Cookie 缓存在客户端浏览器中存储会话数据
  * 服务器无法直接删除其他设备上的 Cookie
  * 会话只在缓存过期或使用 `disableCookieCache: true` 时重新验证

  **如果需要立即撤销会话：**

  * 完全禁用 `cookieCache`，或
  * 设置较短的 `maxAge`（例如 60 秒），或
  * 对敏感操作使用 `disableCookieCache: true`
</Callout>

#### Cookie 缓存策略 [#cookie-缓存策略]

Better Auth 支持三种不同的 Cookie 缓存编码策略：

* **`compact`**（默认）：使用 base64url 编码和 HMAC-SHA256 签名。不包含 JWT 规范开销，格式最紧凑。最适合关注性能和体积的场景。
* **`jwt`**：标准 JWT。默认使用 HMAC-SHA256（HS256）和你的 Better Auth 密钥。令牌经过签名但未加密——任何人都可以读取，但无法篡改。如果你需要能够通过 JWT 插件的 JWKS 验证的会话缓存 Cookie，请安装 `jwt()` 插件并设置 `sessionCookieCache: true`，以使用其非对称签名密钥。
* **`jwe`**：使用 JWE（JSON Web Encryption）以及 A256CBC-HS512 和 HKDF 密钥派生。令牌经过完全加密——既无法读取，也无法篡改。安全性最高，但体积最大。

**比较：**

| 策略        | 体积 | 安全性     | 可读性 | 互操作性 | 适用场景               |
| --------- | -- | ------- | --- | ---- | ------------------ |
| `compact` | 最小 | 良好（已签名） | 是   | 否    | 性能敏感、内部使用          |
| `jwt`     | 中等 | 良好（已签名） | 是   | 是    | 需要 JWT 兼容或与第三方系统集成 |
| `jwe`     | 最大 | 最佳（加密）  | 否   | 是    | 敏感数据、最高安全性要求       |

```ts title="auth.ts"
export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60,
            strategy: "compact" // 也可选择 "jwt" 或 "jwe"
        }
    }
});
```

**注意：** 所有策略在密码学上都是安全的且能防止篡改，主要区别在于体积、可读性和 JWT 规范合规性。

**使用建议：**

* **使用 `compact`**：当你需要最高性能和最小 Cookie 体积时使用。最适合 Cookie 仅由 Better Auth 在内部使用的大多数应用。
* **使用 `jwt`**：当你需要与外部系统兼容 JWT，或希望使用可由第三方工具验证的标准 JWT 令牌时使用。令牌可读（以 base64 编码的 JSON 形式），但无法被篡改。如果希望 Cookie 缓存 JWT 通过其 JWKS 而非共享密钥进行验证，请在 JWT 插件上启用 `sessionCookieCache`。
* **使用 `jwe`**：当你需要最高安全性并希望对客户端隐藏会话数据时使用。令牌经过完全加密，没有密钥就无法读取。适用于敏感数据或合规性要求。

### 基于 JWKS 的 Cookie 缓存 JWT [#基于-jwks-的-cookie-缓存-jwt]

如果你希望 `session_data` Cookie 通过 JWT 插件的 JWKS 端点而不是共享密钥进行验证，请启用 JWT 插件，并明确选择基于 JWKS 的签名：

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

export const auth = betterAuth({
    plugins: [jwt({
        sessionCookieCache: true
    })],
    session: {
        cookieCache: {
            enabled: true,
            strategy: "jwt"
        }
    }
})
```

这只会影响 `session_data` Cookie。主要的 `session_token` Cookie 仍然是不透明的会话标识符，不会通过 JWKS 暴露。Cookie 缓存 JWT 还使用与 JWT 插件 `/token` 端点不同的令牌配置，因此这些令牌不可互换。

如果你希望在获取会话时禁用从 Cookie 缓存返回会话，可以传入 `disableCookieCache:true`，这将强制服务器从数据库获取会话，同时刷新 Cookie 缓存。

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

const session = await authClient.getSession({ query: {
    disableCookieCache: true
}})
```

或者在服务器端：

```ts title="server.ts"
await auth.api.getSession({
    query: {
        disableCookieCache: true,
    }, 
    headers: await headers() // 包含用户会话令牌的请求头
});
```

## 次级存储中的会话 [#次级存储中的会话]

默认情况下，如果在身份验证配置中提供了[次级存储](/docs/concepts/database#二级存储)，会话将存储于次级存储中。

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

betterAuth({
  // ... 其他选项
  secondaryStorage: {
    // 在这里实现你的存储逻辑
  },
});
```

### 会话存储到数据库 [#会话存储到数据库]

默认情况下，Better Auth 将会话存储在数据库中。但如果提供了次级存储，Better Auth 会将会话存储到次级存储而非数据库。

设置 `storeSessionInDatabase: true` 可将会话同时存储在数据库和二级存储中。查询会话令牌时会先检查二级存储；除非启用了 `preserveSessionInDatabase`，否则找不到记录时会回退到数据库。

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

export const auth = betterAuth({
    secondaryStorage: { /** 你的次级存储实现 */ },
    session: { // [!code highlight]
        storeSessionInDatabase: true, // [!code highlight]
    } // [!code highlight]
});
```

### 会话保留 [#会话保留]

当会话被撤销时，它会从次级存储中移除；但是，如果启用 `preserveSessionInDatabase`，会话记录将保留在数据库中，而不是被删除。保留的记录会被标记为已结束（其 `expiresAt` 被设置为撤销时间），因此不会被恢复为活跃会话，并且与其绑定的任何令牌（例如 OAuth Provider 访问令牌）都会被视为已结束。

这对于需要追踪已撤销会话的场景非常有用。

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

export const auth = betterAuth({
    secondaryStorage: { /** 你的次级存储实现 */ },
    session: { // [!code highlight]
        preserveSessionInDatabase: true, // [!code highlight]
    } // [!code highlight]
});
```

## 无状态会话管理 [#无状态会话管理]

### 基础无状态设置 [#基础无状态设置]

如果不传入数据库配置，Better Auth 会自动启用无状态模式。

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

export const auth = betterAuth({
    // 不配置数据库
    socialProviders: {
        google: {
            clientId: process.env.GOOGLE_CLIENT_ID,
            clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        },
    },
});
```

若要手动启用无状态模式，需要配置 `cookieCache` 和 `account`：

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

export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 7 * 24 * 60 * 60, // 7 days cache duration
            strategy: "jwe", // can be "jwe", "jwt" or "compact"
            refreshCache: true, // Enable stateless refresh
        },
    },
    account: {
        storeStateStrategy: "cookie",
        storeAccountCookie: true, // OAuth 流程后将提供方账户数据存储在加密 Cookie 中
    }
});
```

<Callout type="info">
  如果未提供数据库配置，默认会为你提供上述配置。
</Callout>

在无状态 OAuth 流程中，`storeAccountCookie` 会将提供方账户数据（包括 OAuth 令牌材料）存储在加密的 `account_data` Cookie 中。当账户 Cookie 包含刷新令牌和已知的访问令牌过期时间时，`getAccessToken({ useAccountCookie: true })` 可以刷新已过期的提供方访问令牌。令牌刷新响应会设置更新后的账户 Cookie，因此服务器端集成必须将返回的 `Set-Cookie` 标头转发给浏览器。

Better Auth 会对过大的账户 Cookie 进行分块，但浏览器和代理仍可能限制总 Cookie 或请求头大小。对于发放大型 JWT 的提供方，或需要持久化令牌存储的生产流程，请使用由数据库支持的账户存储。

### 理解 `refreshCache` [#理解-refreshcache]

`refreshCache` 选项控制是否在过期前自动刷新 Cookie，且无需查询数据库：

* **`false`**（默认值）：不自动刷新。当 Cookie 缓存过期（达到 `maxAge`）时，如果数据库存在则会尝试从数据库获取。
* **`true`**：启用自动刷新，默认在剩余时间达到 80%（即还剩 20% 时间）时刷新。
* **`object`**：自定义刷新配置，可设置 `updateAge` 属性。

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

export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 300, // 5 分钟
            refreshCache: {
                updateAge: 60 // 剩余 60 秒时刷新
            }
        }
    }
});
```

### 无状态会话的版本控制 [#无状态会话的版本控制]

无状态会话的最大缺点之一是无法轻易失效所有会话。为了解决这个问题，你可以修改 Cookie 缓存的版本号并重新部署应用来使所有旧版本会话失效。

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

export const auth = betterAuth({
    session: {
        cookieCache: {
            version: "2", // [!code highlight] 修改版本号以失效所有旧会话
        }
    }
});
```

<Callout type="warning">
  这将使所有不匹配新版本号的会话失效。
</Callout>

### 无状态 + 次级存储 [#无状态--次级存储]

你可以将无状态会话与次级存储（如 Redis）结合使用，享受两者的优势：

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

export const auth = betterAuth({
    // 不需要主数据库
    secondaryStorage: {
        get: async (key) => await redis.get(key),
        set: async (key, value, ttl) => await redis.set(key, value, "EX", ttl),
        delete: async (key) => await redis.del(key)
    },
    session: {
        cookieCache: {
            maxAge: 5 * 60, // [!code highlight] 5 分钟短时 Cookie
            refreshCache: false // [!code highlight] 禁用无状态刷新
        }
    }
});
```

此配置的特点：

* 使用 Cookie 进行会话验证（不使用数据库查询）
* 使用 Redis 存储会话数据并在 Cookie 过期前刷新缓存
* 可从次级存储撤销会话，刷新时 Cookie 缓存失效

## 自定义会话响应 [#自定义会话响应]

调用 `getSession` 或 `useSession` 时，会返回包含 `user` 和 `session` 的数据对象。你可以使用 `customSession` 插件自定义返回内容。

```ts title="auth.ts"
import { customSession } from "better-auth/plugins";

export const auth = betterAuth({
    plugins: [
        customSession(async ({ user, session }) => {
            const roles = findUserRoles(session.session.userId);
            return {
                roles,
                user: {
                    ...user,
                    newField: "newField",
                },
                session
            };
        }),
    ],
});
```

这将向会话响应中添加 `roles` 和用户的 `newField` 字段。

**客户端侧推断类型**

```ts title="auth-client.ts"
import { customSessionClient } from "better-auth/client/plugins";
import type { auth } from "@/lib/auth"; // 以类型导入 auth 实例

const authClient = createAuthClient({
    plugins: [customSessionClient<typeof auth>()],
});

const { data } = authClient.useSession();
const { data: sessionData } = await authClient.getSession();
// 访问 data.roles
// 访问 data.user.newField
```

### 自定义会话响应的注意事项 [#自定义会话响应的注意事项]

1. 传递给回调的 `session` 对象不会推断插件添加的字段。

不过你可以通过导入你的 `auth` 配置对象并传入插件以协助类型推断：

```ts
import { betterAuth, BetterAuthOptions } from "better-auth";

const options = {
  //...配置项
  plugins: [
    //...插件列表
  ]
} satisfies BetterAuthOptions;

export const auth = betterAuth({
    ...options,
    plugins: [
        ...(options.plugins ?? []),
        customSession(async ({ user, session }, ctx) => {
            // 现在 user 和 session 都能推断插件及自定义字段
            return {
                user,
                session
            }
        }, options), // 传入配置对象  // [!code highlight]
    ]
})
```

2. 若服务器和客户端代码在不同项目或仓库且无法导入 `auth` 作为类型引用，客户端侧不会推断自定义会话字段。
3. 会话缓存（包括次级存储或 Cookie 缓存）不包含自定义字段。每次获取会话时，都会调用你的自定义会话函数。

**修改列表设备会话接口**

`/multi-session/list-device-sessions` 来自 [multi-session](/docs/plugins/multi-session) 插件，用于列出用户登录的设备。

你可以通过在 `customSession` 插件中设置 `shouldMutateListDeviceSessionsEndpoint` 选项来修改该接口的响应。

默认情况下，我们不修改该接口的响应。

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

export const auth = betterAuth({
    plugins: [
        customSession(async ({ user, session }, ctx) => {
            return {
                user,
                session
            }
        }, {}, { shouldMutateListDeviceSessionsEndpoint: true }), // [!code highlight]
    ],
});
```

