会话管理

更好的身份验证会话管理。

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 中的某些接口需要会话是新鲜的。如果会话的 createdAt 时间在 freshAge 限制内,则视为新鲜。默认 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 时,已撤销的会话可能会在其他设备上继续保持有效,直到缓存失效(maxAge 时间到)。

原因:

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

若需立即撤销会话,请考虑:

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

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

  • compact(默认):使用 base64url 编码并配合 HMAC-SHA256 签名。最紧凑格式,无 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 大小,适合大多数仅供内部使用的场景。
  • 选择 jwt:需要与外部系统互操作,或想获取可被第三方工具验证的标准 JWT 令牌。
  • 选择 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 支持无状态会话管理,无需任何数据库。这意味着会话数据仅存储在签名/加密 Cookie 中,服务器无需查询数据库验证会话,仅需要验证 Cookie 签名和检查过期时间。

基础无状态设置

如果不传入数据库配置,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(默认):无自动刷新。Cache 过期时尝试从数据库获取(如果有)。
  • true:开启自动刷新,默认在达到 maxAge 的 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 }), 
    ],
});