# koa2实现session
30 秒速记
Session= 浏览器只拿一个sessionId,用户数据存在服务端,按id查- 存哪:单进程
demo可以放内存,多实例 / 要扛重启就放Redis/MySQL这类共享存储 - 登录成功后换一个新的
sessionId,防止会话固定攻击 - 存储里的
TTL和Cookie的过期时间对齐,滑动过期还是固定过期要想清楚 - 存储挂了就当没登录,宁可让用户重新登录也不能放行
Session 就像存包处:浏览器只拿一张号码牌(sessionId),东西都存在服务端。 每次请求带着号码牌来,服务端凭号码去存储里取出用户信息。Koa 本身不带 session,要用中间件,比如官方的 koa-session 或者 koa-session-minimal。存储选型看部署:单进程 demo 放内存没问题,但进程一重启全员掉线,多实例时请求落到另一台机器也查不到,所以生产一般用 Redis。还有两个安全细节:登录成功要换新的 sessionId,存储读失败要按未登录处理。
理解「koa2实现session」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 security 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Session 和 Cookie 经常被一起问,其实是两个层面的东西:Cookie 是浏览器端的存储和自动携带机制,Session 是服务端维护的会话状态。最常见的组合是用 Cookie 运 sessionId,用服务端存储放数据。
用官方的 koa-session 接 Redis,核心代码就这么几行:
const Koa = require('koa')
const session = require('koa-session')
const Redis = require('ioredis')
const redis = new Redis()
const app = new Koa()
app.keys = [process.env.SESSION_SECRET]
app.use(session({
key: 'sid',
maxAge: 30 * 60 * 1000, // 毫秒
rolling: true, // 每次请求都续期
httpOnly: true,
sameSite: 'lax',
store: { // 只要实现 get / set / destroy 三个方法
async get(key) { const v = await redis.get(key); return v && JSON.parse(v) },
async set(key, sess, maxAge) { await redis.set(key, JSON.stringify(sess), 'PX', maxAge) },
async destroy(key) { await redis.del(key) }
}
}, app))
app.use(async (ctx) => {
ctx.session.views = (ctx.session.views || 0) + 1
ctx.body = `第 ${ctx.session.views} 次访问`
})
app.listen(3000)
注意 set 里用 PX 把 Redis 的过期时间和 maxAge 对齐,不然要么 Cookie 还在数据没了,要么数据永远堆在 Redis 里。退出登录写 ctx.session = null,中间件会调 destroy 把服务端记录也删掉。
面试官追问
追问 1服务部署了 4 个实例,用户刷新几下就随机掉登录,最可能是什么原因?
session 存在各个进程的内存里,请求被负载均衡到别的实例就查不到。换成 Redis 这类共享存储;靠负载均衡做粘性会话只是绕开问题,实例重启照样丢。
追问 2什么是会话固定攻击,怎么防?
攻击者先拿到一个合法的 sessionId,想办法让受害者用这个 id 登录,登录后攻击者手里的 id 就成了已登录状态。防法很简单:登录成功后生成新的 sessionId,旧的作废。
追问 3Session 设了 30 分钟过期,用户一直在操作也会被踢出,怎么改?
那是固定过期。要「操作就续期」得开滑动过期,koa-session 里是 rolling: true 或 renew: true。安全要求高的系统会再加一个绝对上限,比如最多 12 小时必须重新登录。
追问 4Redis 抖了一下,读 session 报错,中间件该放行还是拒绝?
拒绝,当成未登录处理,同时打日志和告警。读失败就放行等于把鉴权关了,这种漏洞比用户重新登录严重得多。
追问 5koa-session 不配 store,数据存在哪?
默认存在 Cookie 里,把整个 session 对象序列化后签名写给浏览器,所以叫 Cookie Session。好处是服务端无状态,坏处是有 4KB 上限、内容用户可见,而且服务端没法主动让它失效。
# koa2实现session
# 前言
30 秒速记
Koa只给了ctx.cookies,session要自己实现或用中间件- 数据量小、单进程:直接放进程内存,简单但重启就丢
- 数据量大或多实例:放
MySQL/Redis这类存储介质 Cookie存用户信息 ≠session,前者数据在客户端,后者在服务端
Koa 的核心很精简,只提供了读写 Cookie 的能力,session 得自己做或者找中间件。 方案按数据放哪分两类:数据少、就一个进程,可以直接存内存,一个 Map 就能跑;数据多或者要部署多台机器,就得放到 MySQL、Redis 这种外部存储里。内存方案的硬伤是进程重启全员掉线、多实例之间不共享,所以我只在本地 demo 里用。生产环境基本都是 Redis,读写快,还自带过期。
koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只用自己实现或者通过第三方中间件实现。在koa2中实现session的方案有一下几种
- 如果session数据量很小,可以直接存在内存中
- 如果session数据量很大,则需要存储介质存放session数据
面试官追问
追问 1登录后把用户信息 JSON 直接写进 Cookie,这算实现了 session 吗?
不算,这是把数据存在客户端,用户能看能改,还受 4KB 限制。session 的意思是数据在服务端,Cookie 里只放一个查数据用的 id。
追问 2用一个 Map 存 session,跑了几天内存一直涨,为什么?
过期的会话没人清理,每个新访客都塞一条进去。内存方案要自己定时扫过期项,这也是生产直接用 Redis 的原因之一,它的 TTL 到点自动删。
追问 3线上发版重启后所有用户都要重新登录,Cookie 明明还在,问题在哪?
session 存在进程内存里,重启就清空了,浏览器带着旧 sessionId 来也查不到。换成外部存储就解决了。
追问 4session 存 MySQL 还是 Redis?
一般选 Redis:每个请求都要读一次 session,Redis 读写是亚毫秒级,还有原生 TTL。MySQL 能用,但要自己清过期数据,而且会话读写会挤占业务库的连接。
# 数据库存储方案
30 秒速记
- 流程:登录时生成
sessionId→ 数据写进MySQL→sessionId写进Cookie→ 之后每个请求凭Cookie查库 koa-session-minimal负责会话流程和ctx.session,koa-mysql-session负责具体的MySQL读写- 业务代码只读写
ctx.session,不碰SQL - 好处是多实例共享、重启不丢;代价是每个请求多一次查库,库挂了全员掉登录
- 过期数据要定期清理,量大时换
Redis更合适
数据库方案就是把 session 数据存进 MySQL,浏览器的 Cookie 里只放 sessionId。 分工上 koa-session-minimal 管流程:读 Cookie 里的 id、在请求开始时把数据装到 ctx.session、结束时把改动写回去;koa-mysql-session 只负责实现存储接口,把数据存到表里。业务代码只需要操作 ctx.session,完全不用写 SQL。这样多台机器读同一个库,会话就共享了。代价是每个请求都多一次数据库查询,所以访问量大了我会换成 Redis。
- 将session存放在MySQL数据库中
- 需要用到中间件
- koa-session-minimal 适用于koa2 的session中间件,提供存储介质的读写接口 。
- koa-mysql-session 为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。
- 将sessionId和对应的数据存到数据库
- 将数据库的存储的sessionId存到页面的cookie中
- 根据cookie的sessionId去获取对于的session信息
面试官追问
