会话管理
更好的身份验证会话管理。
Better Auth 使用传统的基于 Cookie 的会话管理。会话存储在 Cookie 中,并在每个请求时发送到服务器。服务器随后验证会话,若会话有效则返回用户数据。
会话表
会话表存储会话数据。会话表包含以下字段:
id:会话的唯一标识符。token:会话令牌,同时用作会话 Cookie。userId:用户的用户 ID。expiresAt:会话的过期时间。ipAddress:用户的 IP 地址。userAgent:用户代理。存储请求中的用户代理头信息。
会话过期
默认情况下,会话在 7 天后过期。但每当会话被使用且达到 updateAge 时间时,会话过期时间会更新为当前时间加上 expiresIn 值。
你可以通过在 auth 配置中传入 session 对象来修改 expiresIn 和 updateAge 的值。
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... 其他配置选项
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 天
updateAge: 60 * 60 * 24 // 1 天(每隔 1 天更新一次会话过期时间)
}
})禁用会话刷新
你可以禁用会话刷新,使得无论 updateAge 怎样,都会话都不会被更新。
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... 其他配置选项
session: {
disableSessionRefresh: true
}
})延迟会话刷新
默认情况下,GET /get-session 会执行数据库写操作以刷新会话。这在读副本数据库环境中可能产生问题,因为 GET 请求会路由到只读副本。
启用延迟刷新后,GET 请求变为只读,需刷新时返回 needsRefresh: true,客户端会自动调用 POST 进行刷新。
import { betterAuth } from "better-auth"
export const auth = betterAuth({
session: {
deferSessionRefresh: true
}
})会话新鲜度
Better Auth 中的某些接口需要会话是新鲜的。如果会话的 createdAt 时间在 freshAge 限制内,则视为新鲜。默认 freshAge 设置为 1 天(60 * 60 * 24)。
你可以通过在 auth 配置中传入 session 对象自定义 freshAge:
import { betterAuth } from "better-auth"
export const auth = betterAuth({
//... 其他配置选项
session: {
freshAge: 60 * 5 // 5 分钟(会话在最近 5 分钟内创建即视为新鲜)
}
})要禁用新鲜度检查,将 freshAge 设置为 0:
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 操作提供了一个响应式方式访问当前会话。
import { authClient } from "@/lib/auth-client"
const { data: session } = authClient.useSession()列出会话
listSessions 函数返回用户的所有活跃会话列表。
import { authClient } from "@/lib/auth-client"
const sessions = await authClient.listSessions()撤销会话
用户在某台设备注销时,会话会自动结束。你也可以在任何登录设备上手动结束指定会话。
调用 revokeSession 函数,传入会话令牌即可结束该会话。
import { authClient } from "@/lib/auth-client"
await authClient.revokeSession({
token: "session-token"
})撤销其他会话
要撤销除当前会话外的所有其他会话,可使用 revokeOtherSessions 函数。
import { authClient } from "@/lib/auth-client"
await authClient.revokeOtherSessions()撤销所有会话
要撤销所有会话,可以使用 revokeSessions 函数。
import { authClient } from "@/lib/auth-client"
await authClient.revokeSessions()更新会话
如果你为会话配置了额外字段,可以使用 updateSession 函数来更新它们。
import { authClient } from "@/lib/auth-client"
await authClient.updateSession({
theme: "dark",
language: "en",
})核心会话字段(token、userId、expiresAt、createdAt、updatedAt、ipAddress、userAgent)不能通过此接口更新,仅允许修改自定义附加字段。
服务器端示例:
await auth.api.updateSession({
body: {
theme: "dark",
},
headers: await headers() // 包含用户会话令牌的请求头
});修改密码时撤销会话
可以在用户修改密码时撤销所有会话,只需在 changePassword 函数中传入 revokeOtherSessions: true。
import { authClient } from "@/lib/auth-client"
await authClient.changePassword({
newPassword: newPassword,
currentPassword: currentPassword,
revokeOtherSessions: true,
})会话缓存
Cookie 缓存
每次调用 useSession 或 getSession 时访问数据库不理想,尤其是会话不常变化时。Cookie 缓存通过将会话数据存储在短时效、已签名的 Cookie 中来解决这一问题——类似使用 JWT 访问令牌和刷新令牌的模式。
启用 Cookie 缓存后,服务器可以直接从 Cookie 验证会话,而不必每次访问数据库。Cookie 是签名的以防篡改,且较短的 maxAge 使会话数据定期刷新。如会话被撤销或过期,Cookie 会自动失效。
要启用 Cookie 缓存,只需在身份验证配置中设置 session.cookieCache:
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
Cookie 缓存策略
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 | 最大 | 最佳(加密) | 否 | 是 | 敏感数据,最高安全性要求 |
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 缓存。
import { authClient } from "@/lib/auth-client"
const session = await authClient.getSession({ query: {
disableCookieCache: true
}})或者在服务器端:
await auth.api.getSession({
query: {
disableCookieCache: true,
},
headers: await headers() // 包含用户会话令牌的请求头
});次级存储中的会话
默认情况下,如果你在身份验证配置中提供了次级存储,会话将存储于次级存储中。
import { betterAuth } from "better-auth";
betterAuth({
// ... 其他选项
secondaryStorage: {
// 在这里实现你的存储逻辑
},
});会话存储到数据库
默认情况下,Better Auth 已经将会话存储在数据库中。但如果提供了次级存储,Better Auth 会将会话存储到次级存储而非数据库。
你也可以通过在会话配置中传入 storeSessionInDatabase: true 来选择将会话存储回数据库,而非次级存储。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secondaryStorage: { /** 你的次级存储实现 */ },
session: {
storeSessionInDatabase: true,
}
});会话保留
当会话被撤销时,会从次级存储中删除会话记录,但如果启用了 preserveSessionInDatabase,会话将在数据库中保留且不删除。
这对于需要追踪已撤销会话的场景非常有用。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
secondaryStorage: { /** 你的次级存储实现 */ },
session: {
preserveSessionInDatabase: true,
}
});无状态会话管理
Better Auth 支持无状态会话管理,无需任何数据库。这意味着会话数据仅存储在签名/加密 Cookie 中,服务器无需查询数据库验证会话,仅需要验证 Cookie 签名和检查过期时间。
基础无状态设置
如果不传入数据库配置,Better Auth 会自动启用无状态模式。
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:
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属性。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 300, // 5 分钟
refreshCache: {
updateAge: 60 // 剩余 60 秒时刷新
}
}
}
});无状态会话的版本控制
无状态会话的最大缺点之一是不能轻易失效所有会话。为了解决这个问题,你可以修改 Cookie 缓存的版本号并重新部署应用来使所有旧版本会话失效。
import { betterAuth } from "better-auth";
export const auth = betterAuth({
session: {
cookieCache: {
version: "2", // 修改版本号以失效所有旧会话
}
}
});这将导致所有版本不匹配的旧会话被作废。
无状态 + 次级存储
你可以将无状态会话和次级存储(如 Redis)结合使用,享受两全其美:
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 缓存失效
自定义会话响应
调用 getSession 或 useSession 时,会返回包含 user 和 session 的数据对象。你可以使用 customSession 插件自定义返回内容。
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 字段。
客户侧推断类型
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自定义会话响应的注意事项
- 传入回调的
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), // 传入配置对象
]
})- 若服务器和客户端代码在不同项目或仓库且无法导入
auth作为类型引用,客户端侧不会推断自定义会话字段。 - 会话缓存(包括次级存储或 Cookie 缓存)不包含自定义字段。每次获取会话时,都会调用你的自定义会话函数。
修改列表设备会话接口
/multi-session/list-device-sessions 来自 multi-session 插件,用于列出用户登录的设备。
你可以通过在 customSession 插件中设置 shouldMutateListDeviceSessionsEndpoint 选项来修改该接口的响应。
默认情况下,我们不修改该接口的响应。
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 }),
],
});