H3 会话选项与过期
useSession 的第二个参数除了必填的 password,还有一组可选配置。理解它们,你才能驾驭会话的命名、过期与隔离。
完整选项
import { useSession } from "h3";
app.use(async (event) => {
const session = await useSession(event, {
name: "my-session", // 会话 cookie 名,默认 "h3"
password: process.env.SESSION_PASSWORD ?? "[演示占位符]",
cookie: {
httpOnly: true, // 默认就是 true
secure: true, // 默认就是 true
sameSite: "strict",
},
maxAge: 60 * 60 * 24 * 7, // 绝对有效期 7 天
});
return session.data;
});
password 是唯一的必填项;其余都可选。
name 的玄机:cookie 名与 header 名同源
name 决定两件事:会话 cookie 的名字,以及一个派生请求头 x-${name.toLowerCase()}-session。默认的 name: "h3" 因此同时对应:
- cookie 名:
h3 - 请求头:
x-h3-session
更微妙的是大小写:name: "MyApp" 产生的请求头会被归一化成小写 x-myapp-session,但 cookie 名保留原始大小写 MyApp。也就是说,服务端还原会话时看的是小写 header,浏览器存储时看的是原始名。这就是为什么前面的示例里 cookie 都叫 h3。
💡 如果你要同时在 header 和 cookie 两条通道兼容,记住「header 永远小写」这个规则,客户端传 header 时别猜错大小写。
cookie 默认值与本地开发的坑
H3 的会话 cookie 默认就是 secure: true、httpOnly: true、sameSite: "lax"、path: "/",任何一项都可以通过 cookie 选项覆盖。
⚠️ 本地开发必踩:
secure: true告诉浏览器「只在 HTTPS 下携带」。在纯 HTTP 的本地环境(如http://localhost:3000)下,合规浏览器——尤其是 Safari、iOS,以及部分本地域下的 Chrome——会静默丢弃这个 cookie,于是会话怎么都存不住。本地开发时临时设cookie: { secure: false }绕过即可,上线前记得改回true。
两种独立的过期机制
会话有两套互不相干的有效期,可以单独用,也可以叠加:
maxAge:绝对寿命,从会话创建那一刻算起。不管用户多活跃,到点就过期。idleTimeout:滑动寿命,从最后一次请求算起。活跃用户一直在线,闲置的会被踢。
const session = await useSession(event, {
password: process.env.SESSION_PASSWORD ?? "[演示占位符]",
idleTimeout: 60 * 30, // 闲置 30 分钟就登出……
maxAge: 60 * 60 * 24 * 7, // ……但无论多活跃,7 天也强制过期
});
这两个概念和 express-session / koa-session 的 rolling 类似,区别在于 idleTimeout 自带时长,而不是复用 maxAge——开启它不会让你失去绝对上限。
reseal:滑动窗口的代价
开启 idleTimeout 后,H3 需要「重新封印」会话 cookie,把新的滑动起点盖进 cookie 里;createdAt 则保持不变,这样 maxAge 仍能压在顶层当硬上限。cookie 的 Expires 取两者中先到的一个。
重新封印是会话里最贵的操作,所以 H3 不会每次请求都做:只有当「已超过半个窗口」时才重新封印一次,而 update(写)本身就等同于一次重新封印。于是活跃用户的登出时间会落在「距离上次请求 15~30 分钟」之间,而不是精确的 30 分钟:
// idleTimeout: 60 * 30 时
// 真实登出发生在「最后一次请求后 15 到 30 分钟」,不会更晚
⚠️ 如果你需要那个区间的短端(15 分钟)就是硬限制,把
idleTimeout减半即可。
并发读写的坑
因为会话存在 cookie 里,一个只读会话的请求在滑动窗口时也会把 cookie 写回去。如果它和一个写入会话的请求并发发生,浏览器最后应用哪个响应,哪个就生效——写操作可能因此丢失。没有 idleTimeout 时,只读请求不设 cookie,也就不会覆盖并发写。
💡 这意味着:高并发下对「读后写」要格外小心。要么保证关键写操作使用
update(它本身会重新封印并带新 cookie),要么把绝对上限和滑动窗口的时序想清楚。
多会话:不同 name 各自独立
因为每个会话存在自己名字的 cookie 下,同一请求上可以跑多个互不相干的会话,适合把「长期登录态」和「短期闪信」分开:
import { useSession } from "h3";
app.use(async (event) => {
const auth = await useSession(event, {
name: "auth",
password: process.env.SESSION_PASSWORD ?? "[演示占位符]",
});
const flash = await useSession(event, {
name: "flash",
password: process.env.SESSION_PASSWORD ?? "[演示占位符]",
});
await flash.update({ message: "已保存!" }); // 短期提示
// auth 与 flash 由不同 cookie 承载,互不覆盖
return { user: auth.data.user, flash: flash.data.message };
});
⚠️ 给每个会话取不同的 name。两个会话若共享同一个 name,就共享同一个 cookie,结果「后写的覆盖先写的」。
密码轮换的代价
password 变了,所有旧会话 cookie 的封印就全部失效——用户会被集体登出。轮换密钥前要有心理准备,通常只在怀疑密钥泄露时才做。

TRAE-AI编程


更多建议: