Are you an LLM? You can read better optimized documentation at /middleware/cookie.md for this page in Markdown format
Cookie
@vafast/cookie 负责 解析请求 Cookie、HMAC 签名校验,以及用 CookieJar 写回 Set-Cookie。
读用中间件(cookies / signedCookies);写用 createCookieJar。没有 cookie() / setCookie() 这类导出。
先搞清几个概念(给新用户)
| 名词 | 白话 |
|---|---|
| Cookie | 浏览器按域名保存的一小段键值,之后请求会自动带上(受 Path / Domain / Secure / SameSite 等限制) |
Set-Cookie | 服务端在响应里写入 Cookie 的头;本包用 CookieJar.apply(response) 追加 |
Cookie 请求头 | 浏览器发来的已有 Cookie;cookies() / signedCookies() 负责解析 |
expires vs maxAge | 两种过期写法。maxAge 是「从现在起多少秒」;expires 是「具体哪一刻过期」(Date 或时间戳)。可同时写;浏览器以实现为准,常见以 Max-Age 优先 |
| SameSite | 限制跨站请求是否带上 Cookie,用来缓解 CSRF。见下方专表 |
| 签名 Cookie | 值形如 原文.HMAC签名。服务端能发现篡改,但不是加密:原文仍可读。机密内容不要只靠签名 |
| 加密 | 本包不做加密;若需要保密载荷,请自行加密后再 set,或改用服务端 Session + 随机 session id |
SameSite 怎么选?
| 值 | 白话 | 常见场景 |
|---|---|---|
Strict | 只有同站导航才带 Cookie;从外站点链接进来也不带 | 极敏感操作;可能影响「外链进站仍保持登录」 |
Lax | 同站都带;跨站时,顶级 GET 导航(点链接)会带,跨站 POST / iframe / XHR 一般不带 | 多数登录会话的默认首选 |
None | 跨站请求也可以带;必须同时 secure: true(仅 HTTPS),否则浏览器会拒收 | 跨子域前端、第三方嵌入且确实需要带 Cookie |
删除 Cookie 为何经常「删不掉」?
浏览器按 name + Domain + Path(等)匹配要覆盖的 Cookie。delete 必须用与写入时相同的 path / domain,否则等于写了另一条过期 Cookie,原来的还在。
安装
bash
npm install @vafast/cookie快速开始
typescript
import { Server, defineRoute, defineRoutes, json, serve } from 'vafast'
import { cookies, createCookieJar } from '@vafast/cookie'
const routes = defineRoutes([
defineRoute({
method: 'GET',
path: '/theme',
middleware: [cookies()],
handler: ({ cookies: jar }) => json({ theme: jar.theme ?? 'light' }),
}),
defineRoute({
method: 'POST',
path: '/theme',
handler: () => {
const jar = createCookieJar()
// 主题偏好需要前端 JS 读:显式关闭 httpOnly
jar.set('theme', 'dark', { maxAge: 3600, httpOnly: false })
return jar.apply(json({ ok: true }))
},
}),
])
const server = new Server(routes)
serve({ fetch: server.fetch, port: 3000 })用法
基础用法
cookies() 解析 Cookie 头,同时:
- 挂到
req.cookies - 通过
next({ cookies })注入上下文
typescript
const server = new Server(routes)
server.use(cookies())常见场景
1. 签名 Cookie(防篡改会话)
typescript
import { signedCookies, createCookieJar } from '@vafast/cookie'
import { Server, defineRoute, defineRoutes, err, json, serve } from 'vafast'
const secret = process.env.COOKIE_SECRET!
const routes = defineRoutes([
defineRoute({
method: 'POST',
path: '/login',
handler: () => {
const jar = createCookieJar(secret)
jar.setSigned('userId', 'u_1', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
maxAge: 7 * 24 * 3600,
})
return jar.apply(json({ ok: true }))
},
}),
defineRoute({
method: 'GET',
path: '/profile',
middleware: [signedCookies({ secret })],
handler: ({ signedCookies: signed }) => {
if (!signed.userId) throw err.unauthorized('请先登录')
return json({ userId: signed.userId })
},
}),
])
const server = new Server(routes)
serve({ fetch: server.fetch, port: 3000 })signedCookies 行为:
- 签名校验成功 → 进入
signedCookies(值为 原始未签名值) - 校验失败或未签名 → 留在
cookies
因此:不要把 cookies 里的同名键当成已信任数据。
2. 同时读写主题与会话
typescript
defineRoute({
method: 'GET',
path: '/me',
middleware: [signedCookies({ secret })],
handler: ({ cookies: plain, signedCookies: signed }) =>
json({
theme: plain.theme,
userId: signed.userId,
}),
})3. 删除 Cookie(登出)
typescript
defineRoute({
method: 'POST',
path: '/logout',
handler: () => {
const jar = createCookieJar(secret)
// path / domain 必须与 set / setSigned 时一致
jar.delete('userId', { path: '/' })
return jar.apply(json({ ok: true }))
},
})delete 会写入空值、Max-Age=0 与过去的 Expires(new Date(0)),促使浏览器丢弃。
4. expires 与 maxAge 示例
typescript
const jar = createCookieJar()
// 从现在起 1 小时
jar.set('a', '1', { maxAge: 3600 })
// 指定绝对过期时刻
jar.set('b', '2', { expires: new Date('2030-01-01T00:00:00Z') })
// 也可用毫秒时间戳
jar.set('c', '3', { expires: Date.now() + 60_000 })5. 手动 sign / unsign
无需中间件时也可直接用工具函数:
typescript
import { sign, unsign } from '@vafast/cookie'
const signed = sign('u_1', secret) // value.signature(HMAC base64url)
const raw = unsign(signed, secret) // 'u_1' 或 null(校验失败)unsign 使用 timingSafeEqual 做时间安全比较,降低时序攻击风险。
API
导出
| 导出 | 说明 |
|---|---|
cookies | 解析普通 Cookie 的中间件 |
signedCookies | 解析并校验签名 Cookie 的中间件 |
createCookieJar | 创建 CookieJar |
CookieJar | 响应侧 Cookie 组装类 |
parseCookies | 解析 Cookie 头字符串 → Record<string, string> |
serializeCookie | 序列化为单个 Set-Cookie 值 |
sign / unsign | HMAC 签名 / 验证 |
CookieOptions | 写入选项类型 |
SignedCookiesOptions | signedCookies 配置类型 |
选项 / 参数
cookies()
无参数。解析请求头 Cookie,设置 req.cookies,并 next({ cookies })。
signedCookies(options)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
secret | string | — | 必填。HMAC 密钥,务必用环境变量中的强随机串 |
algorithm | string | 'sha256' | 传给 crypto.createHmac 的算法名 |
createCookieJar(secret?, algorithm?)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
secret | string | — | 调用 setSigned 时必填;只 set 普通 Cookie 时可省略 |
algorithm | string | 'sha256' | 签名算法,与 signedCookies 保持一致 |
CookieOptions(set / setSigned / serializeCookie)
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
expires | Date | number | — | 绝对过期时间;number 会 new Date(number)。序列化为 Expires=...(UTC 字符串) |
maxAge | number | — | 相对存活秒数,序列化为 Max-Age=... |
domain | string | — | Domain=...。不设则默认为当前主机(不含子域)。删除时需与写入一致 |
path | string | '/' | Path=...。决定哪些路径会带上该 Cookie。删除时需与写入一致 |
secure | boolean | false(不写 Secure 属性) | 为 true 时仅 HTTPS 发送。SameSite=None 时浏览器通常要求开启 |
httpOnly | boolean | true | 默认写入 HttpOnly,禁止 document.cookie 读取;只有显式传 false 才关闭 |
sameSite | 'Strict' | 'Lax' | 'None' | — | 不传则不写 SameSite 属性(浏览器走自身默认)。取值含义见上文 |
serializeCookie(name, value, options?)
把单个 Cookie 编成 Set-Cookie 字符串:name / value 会 encodeURIComponent;未传 path 时使用 '/';httpOnly !== false 时带 HttpOnly。
parseCookies(cookieHeader)
| 参数 | 类型 | 说明 |
|---|---|---|
cookieHeader | string | null | 请求头原始值;null / 空则返回 {} |
值为 decodeURIComponent 后的字符串。
sign(value, secret, algorithm?) / unsign(signedValue, secret, algorithm?)
| 函数 | 返回 | 说明 |
|---|---|---|
sign | string | `${value}.${hmac_base64url}` |
unsign | string | null | 成功返回原文;格式不对或签名不匹配返回 null |
相关方法
CookieJar
| 方法 | 说明 |
|---|---|
set(name, value, options?) | 追加普通 Cookie |
setSigned(name, value, options?) | 追加签名 Cookie(需构造时传入 secret,否则抛错) |
delete(name, options?) | 删除;options 仅支持 domain / path(与 Pick<CookieOptions, 'domain' | 'path'> 一致) |
apply(response) | 把所有 Set-Cookie append 到新 Response 并返回;无 Cookie 时原样返回 |
链式调用:jar.set(...).setSigned(...).apply(response)。
最佳实践
- 会话类数据用
signedCookies+ 强随机secret(环境变量),读写两侧算法一致 - 生产开启
secure: true,并明确sameSite(多数会话用Lax;跨站带 Cookie 用None+Secure) - 默认
httpOnly: true,除非前端必须读(如主题偏好可关掉) - 写完务必
jar.apply(...),否则不会出现在响应头 - 删除时带上与写入相同的
path/domain - 签名 ≠ 加密:用户仍可解码看到原文,不要把密码等机密只放在 Cookie 里
注意事项
signedCookies会把 未通过校验 的值放进普通cookies,不要把cookies里的同名键当已信任数据setSigned未提供secret时抛错- 解析时对 value 做
decodeURIComponent;非法编码可能导致运行时异常 - 依赖 Node
crypto(HMAC /timingSafeEqual) - 本包不提供 Cookie 加密 API;需要保密请自行加密或改用服务端 Session