# express+cookie_parser签名机制深入剖析
30 秒速记
cookie签名不是加密:值还是明文,只是后面加了一段HMAC,防的是篡改不是偷看cookie-parser:res.cookie('uid', 'zhang', { signed: true })写出来是s:zhang.签名- 校验通过的值放
req.signedCookies,被改过的变成false,没签名的在req.cookies express-session也是同一套:cookie里存签过名的sessionId,数据放服务端storesecret泄露 = 谁都能伪造合法cookie,要立刻换;secret可以传数组,第一个用来签,其余用来验,平滑轮换
cookie-parser 的签名机制说白了就是在 cookie 值后面附一段用 secret 算出来的 HMAC,服务端收到后重算一遍对比,对不上就说明被改过。 比如 res.cookie('uid', 'zhang', { signed: true }),浏览器里看到的是 s:zhang.xxxx,前面的 zhang 还是明文,所以签名防的是篡改不是泄露,敏感信息照样不能放。读的时候签名正确的值在 req.signedCookies.uid 里,被改过的会变成 false。express-session 也用同一个库给 sessionId 签名,真正的用户数据放在服务端,cookie 里只有一个改不了的编号。
理解「express+cookie_parser签名机制深入剖析」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 security 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
cookie-parser 和 express-session 底层都用 cookie-signature 这个小库做签名,原理就是 HMAC-SHA256:用 secret 对 cookie 值算一段摘要,转成 base64 去掉末尾的 =,拼在值后面。为了标记这是签过名的值,写入 cookie 时前面再加一个 s: 前缀。
下面这段可以直接运行,看清签名长什么样、篡改后会怎样:
const signature = require('cookie-signature')
const secret = 'my-secret'
const signed = signature.sign('zhang', secret)
console.log(signed) // zhang.<一串 base64>
console.log(signature.unsign(signed, secret)) // zhang
// 改掉值、保留签名
const forged = 'admin.' + signed.split('.')[1]
console.log(signature.unsign(forged, secret)) // false
放到 Express 里:
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
app.use(cookieParser('my-secret'))
app.get('/login', (req, res) => {
res.cookie('uid', 'zhang', { signed: true, httpOnly: true })
res.send('ok') // 浏览器里看到 uid=s%3Azhang.xxxx
})
app.get('/me', (req, res) => {
const uid = req.signedCookies.uid // 被改过就是 false
if (!uid) return res.status(401).send('未登录')
res.send('hi ' + uid)
})
app.listen(3000)
几个结论:签名后的值仍是明文,只能防篡改;被篡改的签名 cookie 在 req.signedCookies 里是 false,不会跑到 req.cookies 里去;secret 是整个机制的根,泄露了等于任何人都能签出合法 cookie。直接在 cookie 里放 uid 还有一个问题是没法让某个登录态主动失效,所以正式的登录态一般用 express-session:cookie 里只放签过名的随机 sessionId,用户信息和过期时间都在服务端,退出登录时删掉服务端记录就行。
面试官追问
追问 1签名 cookie 里的 uid=zhang 浏览器能直接看到,这不是泄露吗?
是能看到,签名本来就不管保密,只保证改了会被发现。所以签名 cookie 里只放无所谓被看见的标识,敏感信息要么放服务端 session,要么另外加密。
追问 2测试把签名 cookie 里的 zhang 改成 admin,服务端会发生什么?
cookie-parser 重算 HMAC 对不上,req.signedCookies.uid 直接是 false。业务代码要判断这个值,false 一律当未登录处理,别回头去 req.cookies 里取未签名的值兜底。
追问 3secret 不小心提交到公开仓库了,怎么处理才不把用户都踢下线?
secret 支持数组:cookieParser(['newSecret', 'oldSecret']),新签发的用 newSecret,旧 cookie 还能用 oldSecret 验过。过一个会话有效期后把旧的删掉。不过如果怀疑已经有人伪造 cookie,就不能平滑了,直接换掉并让所有会话失效。
追问 4用了 express-session,还需要再挂 cookie-parser 吗?
从 express-session 1.5 开始就不需要了,它自己解析和签名 cookie。非要一起用的话,两边的 secret 必须一致,否则 sessionId 验签会失败。
追问 5签名比对为什么要用常量时间比较?
普通字符串比较遇到第一个不同字符就返回,响应时间会泄露签名对了几位,理论上可以被逐位试出来。cookie-signature 内部做了防时序的比较,自己实现签名时要用 crypto.timingSafeEqual。
# 写在前面
30 秒速记
- 这篇的主线:先理清
session和cookie的关系,再拆express-session怎么实现登录态 cookie是浏览器端存的小数据,每次请求自动带上;session是服务端存的数据,靠cookie里的id找到express-session的核心配置:secret、store、saveUninitialized、resave、cookie.maxAge- 登录态三个关注点:防篡改、超时、主动失效
express-session1.5之后自己处理cookie,不再依赖cookie-parser
这一章的路线是先搞清 session 和 cookie 怎么配合,再看 express-session 是怎么把这套配合封装成中间件的。 理解顺序很重要:cookie 存在浏览器、每次请求自动带上,session 存在服务端,两者靠一个 sessionId 对上号。有了这个底子再看 express-session 的配置就好懂了,secret 管 cookie 签名,store 管数据存哪,saveUninitialized 和 resave 管什么时候落盘。最后落到登录态要关心的三件事:cookie 不能被篡改、要会过期、要能主动注销。
本文先简单介绍session跟cookie的区别与联系,接着深入剖析express-session中间件的实现。关于express-session的基础使用,可参见笔者前面的文章。
面试官追问
追问 1有人说 session 和 cookie 是两种互不相关的存储方案,对吗?
不对,经典的 session 方案离不开 cookie:服务端存数据,cookie 里存 sessionId,浏览器每次请求带上它,服务端才知道该取哪份数据。两者是配合关系,不是二选一。
追问 2浏览器禁用了 cookie,session 还能用吗?
默认的 express-session 就用不了了,因为 sessionId 带不过去。以前有人把 sessionId 拼在 URL 上,但容易泄露,现在一般不这么干;前后端分离的项目多用 Authorization 头带 token。
追问 3给刚接手认证模块的同学安排阅读,顺序怎么排?
先跑通一个最小的 express-session 登录示例,在 DevTools 里看 Set-Cookie 和后续请求带的 Cookie;再看配置项各自管什么;最后读中间件源码。基础行为没亲眼见过就读源码,很容易把配置问题当成内部缺陷。
追问 4session 和 JWT 怎么选?
session
