Koa 中如何管理 Cookie 和 Session?
Cookie 和 Session 到底有什么区别?
HTTP 协议是无状态的,服务器收到请求后没办法知道这个请求是谁发的。Cookie 和 Session 都是为了解决这个问题,但路子完全不同:
- Cookie 是服务器写给浏览器的一小段数据,浏览器每次请求自动带上,容量约 4KB
- Session 是服务器自己存的数据,通过一个 Session ID 跟浏览器对应起来,大小没限制
两者的配合方式:服务端创建 Session,生成唯一的 Session ID,通过 Set-Cookie 响应头下发给浏览器;后续每次请求浏览器自动带着这个 Cookie,服务器拿 Session ID 去查对应的 Session 数据。
一个容易混淆的点:Session ID 本身就是通过 Cookie 传递的,所以 Session 依赖 Cookie,但 Cookie 可以独立使用(比如存用户偏好、主题设置等)。
Koa 里怎么读写 Cookie?
Koa 内置了 Cookie 支持,不需要额外装中间件,直接用 ctx.cookies 就行。
设置 Cookie
javascriptapp.use(async (ctx) => { // 最简单的写法 ctx.cookies.set('name', 'value'); // 带完整选项 ctx.cookies.set('token', 'abc123', { maxAge: 86400000, // 有效期,单位毫秒,这里是一天 expires: new Date('2026-12-31'), // 过期时间点,和 maxAge 二选一 path: '/', // 生效路径,默认 / domain: '.example.com', // 生效域名 secure: true, // 只在 HTTPS 下传输 httpOnly: true, // JS 不能读,防 XSS sameSite: 'strict', // 同源才带,防 CSRF signed: true // 签名防篡改 }); ctx.body = 'Cookie 已设置'; });
几个选项容易踩坑:
httpOnly: true不是可选项,是必选项。没有它,一段 XSS 脚本就能用document.cookie把你的登录凭证偷走sameSite有三个值:strict(最严,跨站一律不带)、lax(导航到目标站点的 GET 请求会带,是浏览器默认值)、none(都带,但必须配secure: true)signed: true依赖app.keys,没设置 keys 会报错。签名防的是篡改,不是加密——签过名的 Cookie 值客户端仍然能解码看到
读取 Cookie
javascriptapp.use(async (ctx) => { const name = ctx.cookies.get('name'); ctx.body = `你好,${name}`; });
设置了 signed: true 的 Cookie,ctx.cookies.get() 会自动校验签名。签名不对返回 undefined,不是报错——这一点要留意,调试时别以为是自己没存上。
删除 Cookie
javascriptctx.cookies.set('name', null, { maxAge: 0 });
把 maxAge 设成 0 就行。有个细节:path 和 domain 必须跟设置时完全一致,否则浏览器匹配不到那个 Cookie,删除操作会静默失败。这个坑在本地调试时特别容易遇到——设置了 /api 路径的 Cookie,删除时没带路径,结果怎么也删不掉。
Koa 中 Session 怎么用?
Koa 核心不带 Session,需要装 koa-session 中间件。
安装和基本配置
bashnpm install koa-session
javascriptconst session = require('koa-session'); // 必须先设置 keys,用于 Cookie 签名 app.keys = ['some-secret-key']; app.use(session({ key: 'koa.sess', // Cookie 里存 Session ID 的字段名 maxAge: 86400000, // Session 有效期,毫秒 httpOnly: true, // JS 不可读 signed: true, // 签名防篡改 rolling: false, // 每次请求是否重置过期倒计时 renew: false // 快过期时是否自动续期 }, app));
app.keys 支持数组,用于密钥轮换:
javascriptapp.keys = ['new-key', 'old-key'];
签名用第一个密钥,校验按顺序尝试。换密钥时把新密钥放前面、旧密钥保留一段时间,已有的 Session 不会突然失效。
两个容易忽略的配置项:
rolling: true-- 每次请求都刷新 Cookie 的过期时间,适合需要保持活跃会话的场景(比如后台管理系统),但会增加 Cookie 写入频率renew: true-- 只在 Session 快过期时自动续期,比 rolling 更轻量,是大多数场景的推荐选项
读写 Session
javascript// 登录 —— 写入 Session app.use(async (ctx) => { if (ctx.path === '/login' && ctx.method === 'POST') { const { username, password } = ctx.request.body; const user = await authenticate(username, password); if (user) { ctx.session.user = { id: user.id, name: user.name }; ctx.body = { message: '登录成功' }; } else { ctx.throw(401, '用户名或密码错误'); } } }); // 受保护页面 —— 读取 Session app.use(async (ctx) => { if (ctx.path === '/profile') { if (!ctx.session.user) { ctx.throw(401, '请先登录'); } ctx.body = `欢迎,${ctx.session.user.name}`; } }); // 登出 —— 销毁 Session app.use(async (ctx) => { if (ctx.path === '/logout') { ctx.session = null; ctx.body = '已登出'; } });
销毁 Session 用 ctx.session = null 就够了,不需要逐个删属性。koa-session 会同时清掉服务端的 Session 数据和浏览器端的 Cookie。
koa-session 默认把 Session 数据存在哪里?
这是个关键问题:koa-session 默认把 Session 数据序列化后直接塞进 Cookie 里。也就是说,浏览器每次请求都带着完整的 Session 数据。
这种默认行为有三个问题:
- 4KB 上限 -- Cookie 有大小限制,Session 数据稍大就会被截断,而且报错不明显,容易排查半天才发现是 Cookie 溢出
- 数据可读 -- 签名只防篡改,不防窥探。Session 数据只是 Base64 编码,浏览器开发者工具里一眼就能看到内容,敏感信息绝不能放进去
- 带宽浪费 -- 每次请求都带着全量 Session 数据往返,用户量大了以后带宽开销不小
开发阶段用默认配置图方便没问题,上线之前必须换外部存储。
生产环境怎么用 Redis 存储 Session?
为什么选 Redis
- 纯内存操作,读写延迟在微秒级,Session 是高频读写场景,非常匹配
- 原生支持 TTL 过期,和 Session 的生命周期管理天然吻合
- 支持多实例共享,部署多个 Node 进程时只要连同一个 Redis 就行
配置方式
bashnpm install koa-session ioredis
javascriptconst session = require('koa-session'); const Redis = require('ioredis'); const redis = new Redis({ host: '127.0.0.1', port: 6379, password: 'your-password', db: 0 }); // koa-session 需要的 store 接口只有三个方法 const redisStore = { async get(key) { const data = await redis.get(`session:${key}`); return data ? JSON.parse(data) : null; }, async set(key, sess, maxAge) { await redis.set(`session:${key}`, JSON.stringify(sess), 'EX', maxAge / 1000); }, async destroy(key) { await redis.del(`session:${key}`); } }; app.use(session({ store: redisStore, key: 'koa.sess', maxAge: 86400000, httpOnly: true, signed: true }, app));
配置 Redis 之后,Cookie 里只剩一个 Session ID,真正的数据全在 Redis 里。应用部署多个实例也没问题,只要连的是同一个 Redis 集群,Session 就能跨实例共享。
几个生产环境的注意点:
- Redis 连接建议用连接池或集群模式,单点 Redis 挂了 Session 全丢
- key 的前缀(上面的
session:)按业务区分,避免和其他 Redis 数据冲突 maxAge / 1000是把毫秒转成秒,Redis 的EX参数单位是秒,这里容易写错
其他存储方案
除了 Redis,常见的还有:
- MongoDB -- 用
connect-mongo之类的适配器,适合已经有 MongoDB 的项目,但性能不如 Redis - MySQL -- 不推荐,关系型数据库做高频 Session 读写是大材小用,性能也跟不上
- Memcached -- 和 Redis 类似的内存缓存,但不如 Redis 生态完善,现在用的人少了
怎么实现登录认证中间件?
认证中间件的核心就是一件事:检查 Session 里有没有用户信息,没有就拦截。
javascriptasync function authRequired(ctx, next) { if (!ctx.session.user) { ctx.throw(401, '未登录'); } await next(); } // 只对需要认证的路由生效 router.get('/api/profile', authRequired, async (ctx) => { ctx.body = ctx.session.user; }); router.get('/api/settings', authRequired, async (ctx) => { ctx.body = await getUserSettings(ctx.session.user.id); });
中间件放在路由处理函数前面,没登录的请求在中间件层就打回去了,不会进业务逻辑。
更完善的做法是加上角色校验:
javascriptfunction roleRequired(...roles) { return async (ctx, next) => { if (!ctx.session.user) { ctx.throw(401, '未登录'); } if (!roles.includes(ctx.session.user.role)) { ctx.throw(403, '权限不足'); } await next(); }; } router.delete('/api/users/:id', roleRequired('admin'), async (ctx) => { // 只有 admin 角色能访问 });
Session 和 JWT 该怎么选?
两者不是非此即彼,但在不同场景下各有优势:
| 对比维度 | Session | JWT |
|---|---|---|
| 存储位置 | 服务端(内存/Redis) | 客户端(Cookie/Header) |
| 状态 | 有状态,服务端维护会话 | 无状态,服务端不存数据 |
| 水平扩展 | 需要共享存储(Redis) | 天然支持,哪里都能验 |
| 主动失效 | 删掉服务端 Session 就行 | 做不到,只能等过期 |
| 数据安全 | 数据在服务端,客户端看不到 | Payload 只是 Base64,谁都能解码 |
| 实现复杂度 | 需要维护存储和清理 | 签发即忘,但吊销很麻烦 |
选型建议:
- 传统 Web 应用(SSR) -- 用 Session。浏览器自动管理 Cookie,登出即失效,权限变更即时生效,开发体验最简单
- 前后端分离 / API 服务 -- 用 JWT。无状态减少服务端压力,适合微服务架构,客户端自己存 token
- 高安全要求 -- 两者结合:JWT 做接口认证(短期有效),关键操作再校验 Session(服务端可控)。银行、支付这类场景经常这么干
一个常见误区:觉得 JWT 无状态就一定比 Session 好。实际上 JWT 做不到主动失效,一旦签发就无法撤回。如果你需要"踢人下线"或"立即撤销权限"的能力,Session 反而更合适。
Cookie 和 Session 的安全防护有哪些要点?
Cookie 安全清单
httpOnly: true-- 必设项。没有这个,XSS 攻击能直接偷 Cookiesecure: true-- 生产环境必须开。确保 Cookie 只在 HTTPS 下传输,防止中间人窃听sameSite: 'strict'或'lax'-- 阻止跨站请求携带 Cookie,从源头防 CSRF。strict最安全但可能影响从外链跳转的体验,lax是较好的折中signed: true-- 签名防篡改,客户端改了 Cookie 值服务端能发现- Cookie 前缀 --
__Host-前缀强制secure、不设domain、path为/;__Secure-前缀强制secure。浏览器会自动执行这些约束,推荐用在敏感 Cookie 上
Session 安全清单
app.keys用强随机字符串,至少 32 位,从环境变量读取,不要硬编码在代码里- 设置合理的
maxAge,不要设成永不过期。通常 1-7 天,根据业务调整 - 登出必须
ctx.session = null彻底销毁,别只删ctx.session.user - 生产环境必须用 Redis 等外部存储,内存存储重启就丢,也没法跨进程共享
Session Fixation 防护
Session Fixation 攻击的原理是:攻击者获取一个有效的 Session ID,诱骗受害者使用这个 ID 登录,攻击者就能用同一个 Session ID 访问受害者的会话。
防护方法:登录成功后重新生成 Session ID。
javascriptapp.use(async (ctx) => { if (ctx.path === '/login' && ctx.method === 'POST') { const user = await authenticate(ctx.request.body); if (user) { // 登录成功,先销毁旧 Session 再创建新的 ctx.session = null; ctx.session.user = { id: user.id, name: user.name }; // koa-session 会在响应时生成新的 Session ID ctx.body = { message: '登录成功' }; } } });
其他防护措施
- 限制同一账号的并发 Session 数量,防止 Session 被盗用后长期使用
- 记录认证日志(登录 IP、时间、设备),异常行为可以及时发现
- 实现登录失败次数限制和延迟,5 次失败后锁定 15 分钟,防暴力破解
- Session ID 要足够长且随机,用
crypto.randomBytes(32)生成,避免被猜测或碰撞 - 敏感操作(修改密码、绑定手机)要求重新验证身份,不要仅依赖已有 Session