Cookies 与 Sessions
Vext 提供 Cookie 读取/写入、配置驱动的 Session 和 CSRF 中间件,覆盖 Native、Hono、Fastify、Express、Koa adapter。基础 Cookie 和内存 Session 不需要额外安装库;使用 Redis 等外部 Store 时需要对应客户端。本页先跑通一个完整流程,再说明保存、隔离和失败边界。
先运行 Cookie、Session 与 CSRF 流程
按快速开始准备 TypeScript 应用,npm scripts 为 dev: vext dev、build: vext build、start: vext start。放入以下两个文件;已有配置时合并字段。本例用访问次数演示会话状态,身份认证另见认证与安全。
执行 npm run dev。下列请求使用 cookies.txt 保存/发送 Cookie;请在示例目录执行,Windows PowerShell 可使用 curl.exe。先验证缺少token被拒绝,再取得token:
第一条返回403、CSRF_TOKEN_MISSING。第二条返回200、data.token,并设置 vext.sid 和 Cache-Control: no-store。把下面 TOKEN 替换成返回的 token;保留同一份 cookies.txt,不能只发送token而丢失关联会话:
以上使用默认成功响应包装,值位于 data 内。停止开发服务,执行 npm run build -- --typecheck 和 npm start,重新获取token及Cookie后复验。内存 Store 的数据不会跨应用进程重启保留;这也是重新获取token的原因。
Cookies
每个请求都会暴露已解析的 cookies。以下路由片段放在 defineRoutes((app) => { ... }) 内:
通过 res.cookie() 设置 cookie,通过 res.clearCookie() 清除 cookie:
多次调用 res.cookie() 会输出多个 Set-Cookie 响应头。Vext 不会把它们用逗号合并。
req.cookies 是只读对象,重复 cookie name 采用 first-wins 语义。res.cookie() 也支持 priority、partitioned 和自定义 encode 函数等高级选项。
maxAge 的单位是秒,expires 接收 Date;普通 Cookie 的 secure 是布尔值,只有 Session/CSRF 配置额外支持 "auto"。清除时要匹配原 Cookie 的 path/domain。这些方法设置响应头,不会修改当前请求的 req.cookies,需要下一次请求携带新 Cookie 才能读到。
Cookie 校验
validate.cookie 会校验已解析的 cookie 值,并生成 OpenAPI in: cookie 参数:
校验顺序为 param -> query -> header -> cookie -> body。
内置 OpenAPI 文档会把 validate.cookie 展示为 cookie 参数。浏览器 Try it out 不能直接设置受限的 Cookie header;如需手动 cookie 值,请使用同源页面已有 cookie、浏览器登录流程,或使用 cURL 等 HTTP 客户端。
Sessions
Session 默认关闭。通过配置启用后,开发与生产启动链会注册运行时,软重载也遵循对应配置。createTestApp() 不自动读取项目配置,测试时必须显式传入 config.session.enabled: true:
在 route handler 中使用 req.session:
Session 对象支持:
id、isNew、save、regenerate、destroy 等 session 元数据不可枚举,也不会被持久化进 store。
autoCommit: true 是默认值。普通响应发送屏障会等待需要执行的异步Store提交;失败时不会继续发出尚未发送的原成功响应和新会话Cookie。显式方法应顺序 await,然后再发送响应;不要并发调用它们或在响应后继续修改会话。已完成save且数据未再修改时,正常autoCommit不会重复写入;这不代表多请求之间有事务或并发更新保护。
regenerate() 不清空数据,也不会自动完成登录认证;autoCommit: false 时须再调用 save() 保存新id。destroy() 会删除Store条目并清除Cookie,不应在销毁后继续写入业务状态。流式响应或下载前,若有待保存的会话或启用了rolling,应先 await req.session!.save(),再调用 res.stream() / res.download()。
未修改的新会话在默认非rolling模式下不会仅因读取就写入Store或发送Cookie。普通读请求也不会刷新既有会话的TTL;rolling启用后会在响应时刷新。可持久化数据应与所选Store/serializer相容;多个请求同时修改同一会话需要应用/Store提供并发策略,框架不承诺自动合并。
配置
config.session.enabled: true 启用全局 Session 运行时,其余字段用于配置运行时:
ttl单位为秒,默认86400;Cookie maxAge未单独指定时跟随ttl。idLength是生成ID的随机字节数(16~128,默认32),不是编码后的字符串长度。secure: "auto" 仅在 req.protocol === "https" 时添加Secure,反向代理部署需核对trustProxy及实际协议。
默认memory Store会在访问时移除已过期条目,并通过有界的机会式 sweep 清理其余过期项,不创建阻止进程退出的timer;它适合开发、测试和接受进程内状态的单进程部署。多worker/多实例需共享Store;软重载不能代替对进程重启与数据持久化的设计。
可选:接入共享 Store
可使用根入口导出的 createCacheSessionStore() 接入结构型cache。以下为替换session字段的Redis配置片段,合并时保留主例的其他配置。消费项目先执行 npm install cache-hub ioredis,并准备自己的Redis服务;示例地址需按实际环境调整:
createCacheSessionStore() 接收具备 get、set、del 的结构型 cache,把 VextSessionStore 的 TTL 秒转换为 cache 毫秒,默认把 session data 写成 JSON string,并用 cache get + set 实现 rolling touch()。消费项目需要自行安装 cache-hub 和选用的后端 client,例如 ioredis。
config.cache.cacheHub 与 app.cache 只服务路由响应缓存,不是 Session Store 捷径,也应与session使用不同键前缀。这里的prefix由应用选择,默认 vext:session: 不含自动项目/环境隔离;同组实例要一致,不同应用/环境应区分。touch() 的get/set不是原子更新,应按并发要求选择实现。
每个Session运行时关闭时会调用所用Store暴露的 close(),且仅调用一次;createCacheSessionStore() 仅在传入close回调时暴露它。上述cache-hub adapter包装外部Redis实例,adapter.close不会代替调用方关闭,所以示例将这个独占实例的关闭交给Store回调;若把同一客户端共享给其他模块,应另设统一关闭方。直接以URL创建cache-hub adapter时,它才拥有并关闭自己创建的连接。需要特殊持久化契约时,可直接实现get/set/delete及可选touch/close的 VextSessionStore。
公开路由可设置 session: false 跳过Session;这不会销毁已有Store数据或清除Cookie。全局运行时关闭时,可通过 session: true 或 { session: { enabled: true, rolling: true } } 为单个路由启用;路由只覆盖enabled/rolling/autoCommit,不改变Store或Cookie身份。显式 session() 中间件仍保留给作用域化或手动注册场景;不要与全局配置重复使用。
CSRF 防护
CSRF 防护通过 csrf() 与 config.csrf 提供。mode: "auto" 下,若配置的 Session 运行时已提供 req.session,Vext 会使用 session 同步 token;否则可在配置 config.csrf.secret 后使用签名 double-submit cookie。
本页开头的两文件示例提供完整token获取与提交流程。Session模式的token保存在服务端会话中;signed-cookie模式需稳定secret,用Cookie中的签名值核对客户端提交的原始token。多实例必须使用一致的secret/Store等对应条件。
默认保护 POST、PUT、PATCH、DELETE。客户端可通过 x-csrf-token、x-xsrf-token 或 body 字段 _csrf 提交 token。必须开放的 unsafe 路由可在 route options 中设置 { csrf: false } 跳过。
设置 config.csrf.enabled: true 可自动全局注册CSRF。它在body parsing、全局Session和插件全局中间件之后执行,但早于路由专用Session与路由身份Guard。需要Session模式时,优先像主例一样全局启用Session;只在路由启用Session,不能保证它在全局CSRF执行时已存在。若要在插件内按路径手动调用 csrf(),应保证它之前已挂载所需Session,并避免与全局CSRF重复注册。
未注册CSRF时,路由 csrf: true 不会自行增加中间件。csrf: false 跳过检查,但中间件仍会挂载token方法;调用token方法仍要求可用Session或secret。req.csrfToken() 会设置 Cache-Control: no-store,不要把token响应放入共享缓存。
默认启用Fetch Metadata检查:保护的方法若带 Sec-Fetch-Site: cross-site 会返回403。Origin检查默认关闭;配置 origin: { trustedOrigins: [...] } 后会检查已有Origin,缺少时尝试Referer,两者都没有时不靠此项拒绝。token、来源检查和业务身份授权需分别验证。
缓存安全
Cookie 与响应缓存需同时验证冷缓存和已有缓存:
- 当前默认禁止带
Cookie的回源结果写入,但带 Cookie 的请求仍可能读到已存在的公开缓存;完全绕过须显式添加condition: (req) => req.headers.cookie === undefined或使用cache: false,见响应缓存限制 - 包含
Set-Cookie的响应永不写入缓存 - 只有确认 cookie 输入安全时,才为路由设置
allowCookieCache: true
该片段只演示已明确按Cookie区分的响应缓存,不用于会话token或私有用户数据。创建Session或更新Cookie的响应仍不得写入共享缓存。
当前缓存实现的“不写入”不等于“不参与并发合并”:同 key 的在途请求仍可能复用首份正文。创建/修改会话、返回私有数据或必须每次独立执行的接口,应在进入缓存前使用 cache: false 或 condition 排除,不能只依赖 Set-Cookie / private / no-store 响应头。具体限制见响应缓存的并发回源。