# koa2使用cookie
30 秒速记
Koa自带ctx.cookies,底层是npm的cookies模块,读请求头里的Cookie、写响应头里的Set-Cookie- 单个
Cookie大约4KB上限,同域每个请求都会带上,只放sessionId这类标识 httpOnly(Koa默认true)防脚本读、secure只走HTTPS、sameSite管跨站携带、path/domain管作用范围signed: true要先配app.keys,只防篡改不加密,内容照样明文可见- 删除 = 同名同
path/domain再写一次空值让它过期,作用域对不上就删不掉
Cookie 说白了是服务端塞给浏览器的一张小纸条,浏览器之后每次请求同域都会自动带回来。 在 Koa 里写用 ctx.cookies.set(),读用 ctx.cookies.get()。因为每个请求都会带、容量又只有 4KB 左右,我只往里放 sessionId 这种标识,真正的用户数据放服务端。几个属性各管一摊:httpOnly 让 document.cookie 读不到,secure 让它只在 HTTPS 下发送,sameSite 限制第三方站点发请求时带不带它。签名只是防止用户改值,不是加密,敏感信息一样不能放。
理解「koa2使用cookie」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 security 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Cookie 的工作流程只有两步:服务端在响应里写 Set-Cookie,浏览器存下来;之后浏览器发请求时,凡是域名、路径、协议都匹配的 Cookie,自动拼进请求头 Cookie 里带回去。Koa 把这两步封装成了 ctx.cookies.set() 和 ctx.cookies.get(),底层是 pillarjs/cookies 这个包。
const Koa = require('koa')
const app = new Koa()
app.keys = [process.env.COOKIE_SECRET || 'dev-secret'] // 用 signed 必须配
app.use(async (ctx) => {
if (ctx.path === '/login') {
ctx.cookies.set('sid', 'abc123', {
httpOnly: true, // 默认就是 true,document.cookie 读不到
sameSite: 'lax', // 第三方站点发起的 POST 不带它
maxAge: 30 * 60 * 1000, // 毫秒,30 分钟
signed: true // 额外写一个 sid.sig 防篡改
})
ctx.body = 'logged in'
} else {
ctx.body = ctx.cookies.get('sid', { signed: true }) || 'no cookie'
}
})
app.listen(3000)
// curl -i localhost:3000/login -> 响应头里有两条 Set-Cookie:sid 和 sid.sig
记住三条就够用:只存标识不存数据;登录凭证一律 httpOnly,生产环境加 secure;删除时 path、domain 要和写入时完全一致。
面试官追问
追问 1Cookie 里存了 userId=123,用户自己改成 456 是不是就登录别人账号了?
不签名的话就是。要么开 signed: true(配合 app.keys),改了值签名对不上,get 时带 { signed: true } 会读到 undefined;更好的做法是只存随机的 sessionId,用户信息放服务端。
追问 2前端想用 document.cookie 读登录态,却发现读不到 session,是出问题了吗?
不是,Koa 的 ctx.cookies.set 默认就是 httpOnly: true,脚本读不到正是想要的效果。前端要知道登录没登录,调一个 /api/me 接口就行,别为这个把 httpOnly 关掉。
追问 3Cookie 和 localStorage 存 token,怎么选?
localStorage 不会自动带上请求,要手动塞 Authorization 头,XSS 能直接读走;httpOnly 的 Cookie 脚本读不到,但会自动携带,要靠 sameSite 和 CSRF token 防跨站请求。同域的传统 Web 应用我倾向 httpOnly Cookie。
追问 4给 Cookie 加了 signed: true,服务启动就报错了,缺了什么?
缺 app.keys。签名要用密钥算 HMAC,写成 app.keys = ['some-secret'],密钥走环境变量别写死。设置后响应里会多一个 name.sig 的 Cookie。
追问 5页面一个请求的请求头里带了 6KB 的 Cookie,会有什么问题?
每个请求都白白多传这么多字节,静态资源请求也不放过;有的服务器或网关对请求头大小有限制,超了直接返回 400 / 431。把大数据挪到服务端,静态资源放到不带 Cookie 的独立域名。
# koa2使用cookie
# 使用方法
30 秒速记
- 读:
ctx.cookies.get(name, [options]),读的是这次请求带上来的Cookie - 写:
ctx.cookies.set(name, value, [options]),写进这次响应的Set-Cookie - 同一个请求里先
set再get拿不到新值,要等浏览器下一次请求带回来 - 选项语义以底层
cookies模块为准:maxAge是毫秒、httpOnly默认true、overwrite默认false - 删除:
ctx.cookies.set(name, null, sameScopeOptions)
Koa 在上下文上直接给了 ctx.cookies.get 和 ctx.cookies.set 两个方法,一个读请求、一个写响应。 这一点很多人会搞混:set 只是往响应头里加了一条 Set-Cookie,当前请求的 Cookie 头不会变,所以紧接着 get 是拿不到的。它底层用的是 pillarjs/cookies,选项怎么写、默认值是什么都去看那个包的文档。比如 maxAge 单位是毫秒,httpOnly 默认开着,这两个最容易写错。
koa提供了从上下文直接读取、写入cookie的方法
- ctx.cookies.get(name, [options]) 读取上下文请求中的cookie
- ctx.cookies.set(name, value, [options]) 在上下文中写入cookie
koa2 中操作的cookies是使用了npm的cookies模块,源码在https://github.com/pillarjs/cookies,所以在读写cookie的使用参数与该模块的使用一致。
面试官追问
追问 1登录接口里 set 完 token,下一行 get('token') 是 undefined,为什么?
get 读的是请求头,set 写的是响应头,两者不是同一份数据。同一个请求里要用这个值就直接用变量,别绕回 Cookie 读。
追问 2写了 maxAge: 3600,结果 Cookie 几秒就没了?
ctx.cookies 的 maxAge 单位是毫秒,3600 就是 3.6 秒。一小时要写 3600 * 1000。
追问 3想删掉一个 Cookie,怎么写?
ctx.cookies.set('token', null, { path: '/', domain: 'example.com' }),值传 null,模块会把过期时间设到 1970 年。path 和 domain 必须和写入时一样,否则删的是另一枚。
追问 4同事想绕开 ctx.cookies,自己拼 Set-Cookie 头,你同意吗?
一般不同意。自己拼要处理编码、多条 Set-Cookie 合并、签名这些细节,很容易写出浏览器拒收的格式。ctx.cookies 不支持的属性才考虑手写,而且要集中在一个工具函数里。
追问 5签名 Cookie 读的时候拿到 undefined,但浏览器里明明有值,怎么回事?
先看 get 有没有传 { signed: true },再看 app.keys 是不是换过。密钥轮换要把新密钥放数组第一位、旧的留在后面,cookies 会用旧密钥验签并自动重签,直接替换会让所有老 Cookie 失效。
# 例子代码
30 秒速记
- 访问
/index时写入cid=hello world,其他路径只返回hello world domain: 'localhost'意味着必须用localhost访问,用127.0.0.1打开浏览器会拒收path: '/index'→ 只有/index及其子路径的请求会带上它,全站用的要写/maxAge和expires同时写时,cookies
