H3 会话选项与过期

2026-10-30 17:48 更新

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 的封印就全部失效——用户会被集体登出。轮换密钥前要有心理准备,通常只在怀疑密钥泄露时才做。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号