会话管理

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

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

会话表

会话表用于存储会话数据,包含以下字段:

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

会话过期

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

你可以通过在 auth 配置中传入 session 对象来修改 expiresInupdateAge 的值:

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 怎样,会话都不会被更新。

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

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

延迟会话刷新

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

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

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

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

会话新鲜度

部分 Better Auth 接口要求会话必须是 新鲜的。如果会话的 createdAtfreshAge 限制范围内,则该会话被视为新鲜。默认情况下,freshAge 设置为 1 天(60 * 60 * 24)。

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

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

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

禁用新鲜度检查,将 freshAge 设置为 0

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

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

会话管理

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

获取会话

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

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

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

有关自定义会话响应的说明,请参阅 自定义会话响应 部分。

使用会话

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

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

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

列出会话

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

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

const sessions = await authClient.listSessions()

撤销会话

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

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

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

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

撤销其他会话

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

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

await authClient.revokeOtherSessions()

撤销所有会话

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

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

await authClient.revokeSessions()

更新会话

如果你为会话配置了额外字段,可以使用 updateSession 函数来更新它们。

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

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

核心会话字段(tokenuserIdexpiresAtcreatedAtupdatedAtipAddressuserAgent)不能通过此接口更新,仅允许修改自定义附加字段。

服务器端示例:

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

修改密码时撤销会话

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

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

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

会话缓存

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

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

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

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

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

注意事项

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

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

如果需要立即撤销会话:

  • 完全禁用 cookieCache,或
  • 设置较短的 maxAge(例如 60 秒),或
  • 对敏感操作使用 disableCookieCache: true

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

  • compact(默认):使用带 HMAC-SHA256 签名的 base64url 编码。无 JWT 规范开销,体积最小。适合性能和体积敏感的场景。
  • jwt:标准的 JWT(HMAC-SHA256 签名,HS256)。已签名但未加密——任何人可读但无法篡改。遵循 JWT 规范以便互操作。
  • jwe:使用 JWE(JSON Web Encryption)和 A256CBC-HS512 及 HKDF 密钥派生。完全加密的令牌——既不可读也无法篡改。最安全但体积最大。

比较:

策略体积安全性可读性互操作性适用场景
compact最小良好(已签名)性能敏感、内部使用
jwt中等良好(已签名)需要 JWT 兼容或与第三方系统集成
jwe最大最佳(加密)敏感数据、最高安全性要求
auth.ts
export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 5 * 60,
            strategy: "compact" // 也可选择 "jwt" 或 "jwe"
        }
    }
});

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

使用建议:

  • 使用 compact:当你需要最佳性能和最小 Cookie 体积时。适用于大多数仅由 Better Auth 内部使用的场景。
  • 使用 jwt:当你需要与外部系统进行 JWT 兼容,或希望使用可通过第三方工具验证的标准 JWT 令牌时。令牌是可读的(Base64 编码的 JSON)但无法篡改。
  • 使用 jwe:当你需要最高安全性并希望隐藏会话数据时。令牌完全加密,无法在没有密钥的情况下读取。用于敏感数据或合规性要求。

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

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

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

或者在服务器端:

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

次级存储中的会话

默认情况下,如果在身份验证配置中提供了次级存储,会话将存储于次级存储中。

import { betterAuth } from "better-auth";

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

会话存储到数据库

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

你也可以通过在会话配置中传入 storeSessionInDatabase: true 来选择将会话存储回数据库,而非次级存储。

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

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

会话保留

当会话被撤销时,会从次级存储中删除会话记录,但如果启用了 preserveSessionInDatabase,会话将在数据库中保留且不删除。

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

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

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

无状态会话管理

基础无状态设置

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

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,
        },
    },
});

若要手动启用无状态模式,需要配置 cookieCacheaccount

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

export const auth = betterAuth({
    session: {
        cookieCache: {
            enabled: true,
            maxAge: 7 * 24 * 60 * 60, // 7 天缓存时间
            strategy: "jwe", // 可选 "jwt" 或 "compact"
            refreshCache: true, // 启用无状态刷新
        },
    },
    account: {
        storeStateStrategy: "cookie",
        storeAccountCookie: true, // OAuth 流程后将账号数据存入 Cookie(适用于无数据库)
    }
});

如果未提供数据库配置,默认会为你提供上述配置。

理解 refreshCache

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

  • false(默认值):不自动刷新。当 Cookie 缓存过期(达到 maxAge)时,如果数据库存在则会尝试从数据库获取。
  • true:启用自动刷新,默认在剩余时间达到 80%(即还剩 20% 时间)时刷新。
  • object:自定义刷新配置,可设置 updateAge 属性。
auth.ts
import { betterAuth } from "better-auth";

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

无状态会话的版本控制

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

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

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

这将使所有不匹配新版本号的会话失效。

无状态 + 次级存储

你可以将无状态会话与次级存储(如 Redis)结合使用,享受两者的优势:

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, // 5 分钟短时 Cookie
            refreshCache: false // 禁用无状态刷新
        }
    }
});

此配置的特点:

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

自定义会话响应

调用 getSessionuseSession 时,会返回包含 usersession 的数据对象。你可以使用 customSession 插件自定义返回内容。

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 字段。

客户侧推断类型

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 配置对象并传入插件以协助类型推断:

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), // 传入配置对象
    ]
})
  1. 若服务器和客户端代码在不同项目或仓库且无法导入 auth 作为类型引用,客户端侧不会推断自定义会话字段。
  2. 会话缓存(包括次级存储或 Cookie 缓存)不包含自定义字段。每次获取会话时,都会调用你的自定义会话函数。

修改列表设备会话接口

/multi-session/list-device-sessions 来自 multi-session 插件,用于列出用户登录的设备。

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

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

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 }), 
    ],
});