前后端分离项目中 Cookie 与 JWT 鉴权方案的差异
前几天把一个小后台从 JWT 换回了 Cookie Session,不是 JWT 不能用,而是这个项目一共就两个前端、一个后端,用户量也不大,我实在不想为了“无状态”三个字再维护一套 refresh token 和黑名单。换完之后顺手把两种方案又捋了一遍,发现网上很多对比文章写得像选边站,其实真到项目里,差别都在一些很具体的地方。
先说结论:不是谁更高级,而是谁更适合你的场景
Cookie 和 JWT 经常被放在一起比,但严格说它们不是同一个层面的东西。Cookie 是浏览器存数据、自动携带的机制;JWT 是一种令牌格式。你可以把 JWT 放在 Cookie 里,也可以放在 Authorization 头里。真正要选的是:会话状态放服务端还是客户端,令牌怎么传给后端,以及你能不能接受它带来的副作用。
我现在的习惯是,后台管理、同站点部署、需要即时踢人的项目,优先 Cookie Session;给移动端、第三方调用、多个服务之间传身份,JWT 更顺手。下面按实际差异说。
Cookie 方案:浏览器帮你带,服务端帮你记
Cookie Session 的流程很朴素:登录成功后服务端生成一个 session,把 sessionId 通过 Set-Cookie 写给浏览器,浏览器之后每次同站请求自动带上。服务端拿 sessionId 去内存、Redis 或数据库里查用户。
在前后端分离项目里,第一道坎就是跨域。前端如果跑在 localhost:5173,后端跑在 localhost:3000,虽然端口不同,但浏览器发请求时默认不会带 Cookie。前端得显式加 credentials:
fetch("http://localhost:3000/api/profile", {
credentials: "include",
});
后端 CORS 也不能再图省事写 *,得指定来源并允许凭证:
app.use(cors({
origin: "http://localhost:5173",
credentials: true,
}));
Cookie 本身还要设 HttpOnly、Secure、SameSite。HttpOnly 能防前端 JS 直接读 token,XSS 来了也没那么容易把登录态掏走;SameSite=Lax 或 Strict 能挡掉相当一部分 CSRF。如果是真正跨站的前后端,比如前端在 a.com、后端在 b.com,那 Cookie 得设 SameSite=None; Secure,还必须上 HTTPS,本地调试会麻烦一点。
这套东西的好处是注销简单:服务端删 session 就完事,用户下次请求立刻失效。坏处是服务端得存状态,横向扩容时要么做 session 共享,要么接受粘性会话。小项目用 Redis 存一下还好,大项目就得多想一层。
JWT 方案:令牌自己带信息,服务端少记东西
JWT 最常见的用法是登录后后端签发 token,前端存起来,之后请求塞进 Authorization 头:
fetch("http://localhost:3000/api/profile", {
headers: {
Authorization: `Bearer ${token}`,
},
});
后端验签、看过期时间,完事。服务端不用查 session,多个服务只要共享密钥或公钥就能各自验,微服务场景下确实舒服。JWT 里还能塞一点非敏感信息,比如用户 id、角色,省一次查库。
但“无状态”是有代价的。最直接的问题就是注销。服务端没存 token,用户点了退出,前端把本地 token 删了,可这个 token 在过期前拿到别处照样能用。要解决就得加黑名单、token 版本号或者短过期时间加 refresh token,一加这些,状态又回来了,只是换了个地方存。
另一个坑是存哪里。放 localStorage 最简单,但 XSS 一旦进来就能直接读走;放 HttpOnly Cookie 能防读,可又回到了 Cookie 的 CSRF 和跨域问题,而且请求得带 Cookie,不再是纯 Authorization 头那套。很多人说“JWT 天然适合前后端分离”,其实只说了一半,另一半是安全存储和续期策略。
跨域和 SameSite,是 Cookie 方案绕不过去的
前后端分离最容易卡在 Cookie 上。开发环境两个端口,生产环境两个域名,表现还不一样。SameSite=Lax 对大部分 GET 导航请求友好,但跨站 POST 不一定带 Cookie;SameSite=None 又必须配 Secure,本地 HTTP 直接不生效。我见过有人本地调半天,最后发现是 Chrome 把没有 Secure 的 SameSite=None Cookie 丢了。
JWT 放 Authorization 头就绕开了浏览器自动带 Cookie 这套规则,CORS 配好允许头就行,调试时反而直观:没带就是没带,不用猜浏览器为什么又把 Cookie 吞了。
注销、续期和即时失效,差别很大
Cookie Session 的续期通常靠服务端滑动过期,用户活跃就延长,不活跃就过期,逻辑集中。JWT 一般设短一点的 access token,再加一个 refresh token,前端发现 401 就去换新 token。听着优雅,写起来一堆边界:并发刷新、刷新失败跳登录、refresh token 存哪里、要不要旋转。项目小的时候,这些代码比业务还多。
即时失效也是。后台系统经常要“禁用某个账号,让他立刻下线”。Cookie Session 删 session 就完了;JWT 要么维护黑名单,要么等它自然过期。所以涉及权限敏感的后台,我越来越倾向于 Cookie Session,至少省心。
安全侧重点不一样,不是谁绝对安全
Cookie 方案主要防 CSRF,靠 SameSite、CSRF token、检查 Origin。JWT 放 localStorage 主要防 XSS,但 XSS 这东西一旦有,啥方案都别太自信。JWT 放 Cookie 则两边都得防。还有 JWT 的签名密钥不能泄露,算法要固定,别让前端随便改 alg。Cookie 的 HttpOnly 也不是万能,但至少让“一行 localStorage.getItem 就把 token 带走”这种事没那么容易发生。
我现在的实际选择
同站点后台、需要即时踢人、服务端本来就有 Redis 的项目,我用 Cookie Session。配置不复杂,注销干净,前端也不用管 token 过期。给 App、开放 API、多个后端服务互调,我用 JWT,放 Authorization 头,access token 短一点,refresh token 单独管理。
如果非要说前后端分离项目里的差异,我的感受是:Cookie 方案把复杂度放在跨域和 CSRF 上,JWT 把复杂度放在存储、注销和续期上。没有哪个更“现代”,只有哪个坑你更愿意踩。像我这次这个小后台,真不想为了注销再写一套黑名单,所以又老老实实把 Cookie 捡回来了。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。