news 2026/9/25 8:14:29

Cookie与JWT全解析:从登录状态原理到安全攻防实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cookie与JWT全解析:从登录状态原理到安全攻防实战

做Web开发,基本没人能绕开一个问题——用户的登录状态怎么保存。网上关于Cookie和JWT的讨论一搜一大把,但大部分内容都停留在“Cookie存浏览器、JWT存客户端”“Session在服务端、JWT在客户端”这种表面区分。你拿着这种认知去做技术选型,大概率会在第一个真实项目里翻车。我前两个月刚把一个老项目的Session-Cookie登录改造成Spring Security配合JWT的登录体系,中途还补了一轮安全加固,这次就把Cookie和JWT从底层原理、实战实现到安全漏洞,完整摊开来讲清楚。

1. 先把关系理清楚:Cookie、Session、JWT不是同一个维度的东西

1.1 从一次HTTP请求说起

HTTP协议有个天然特性:它是“失忆”的。每一次请求到达服务器时,服务器看到的都是一张陌生的脸——它不知道这个请求带着什么上下文,也不知道这个用户之前有没有登录过。

于是大家才搞出了一套“凭证”机制:客户端需要主动证明“我是谁”。这件事放在现实世界里其实就是进门刷门禁卡,门禁系统不关心你是张三还是李四,它只认你手里的卡能不能匹配数据库里的授权记录。Web里的登录状态保持,本质上就是在解决“如何让失忆的HTTP协议记住你”这个悖论。

1.2 绝大多数人的“区别”认知是错的

我看到过太多博主把Cookie、Session、JWT三样东西放一起对比,然后得出结论“Cookie不安全,Session安全,JWT安全”。这种对比本身就是外行话,因为这三样东西根本不在同一个维度。

  • Cookie是浏览器提供的一种本地存储载体,它负责“存”。
  • Session是服务端维护的一份会话状态,它负责“记”。
  • JWT是一种自包含的数据格式,它负责“认证”。

打个比方。Cookie像一个“凭证袋”,浏览器帮你把各种凭证小票放进去,每次请求自动递过去;Session是服务端收银台的“账本”,商家在账本上记录你这个客户的消费记录;JWT则是一张“自带防伪印章的通行证”,印章一验就知道通行证是不是真的、有没有过期,服务端连账本都不用翻。

所以你可以用Cookie存Session ID,也可以用Cookie存JWT,还可以不用Cookie直接把JWT放在Authorization头里。把它们放在对立面比较,本身就是没搞懂各自的职责。

1.3 为什么JWT会在前后端分离时代被大量使用

传统Java Web项目用Session-Cookie是黄金搭档,因为页面和服务端天然同源,浏览器自动携带Cookie,服务端根据sessionId去内存或数据库查状态,流程非常顺。

但到了前后端分离、移动端大量出现之后,情况变了。API接口要被多个端调用:浏览器、小程序、iOS、安卓,甚至第三方开放平台。这时候依赖浏览器自动携带Cookie的机制就不好使了,移动端原生应用根本没有Cookie这个概念,必须手动处理。

JWT的优势在这里放大了:登录后服务端签发一个包含用户信息和过期时间的令牌,各个端只要在请求头里带上Authorization: Bearer <token>,服务端验签通过便放行。不需要Session存储,不需要分布式Session共享,天然适合微服务和多端场景。

2. 两种登录态方案的全链路跑通演示

2.1 Cookie-Session方案:老牌的“有状态”维护方式

把Cookie和Session配合起来跑一遍完整登录流程,你会更清楚它们的配合逻辑。

用户向服务端提交账号密码,服务端验证通过之后,在服务端创建一条Session记录,并生成一个唯一的sessionId。真正存下来的可能是用户ID、登录时间、IP、过期时间这些信息。存储位置可以是本地内存,也可以是Redis。然后服务端通过响应头返回Set-Cookie: JSESSIONID=xxxx; Path=/; HttpOnly,浏览器收到之后把它放进自己的Cookie仓库里。

之后用户每次发请求,浏览器都会自动在请求头里带上Cookie: JSESSIONID=xxxx,服务端去Session存储里查找这个id对应的记录,查得到就认为用户已登录,查不到就返回登录过期。

这个方案最大的特点是“有状态”——会话保存在服务端,所以服务端可以随时把某条Session删除,用户立即下线。这是JWT做不到的。分布式场景下,共享Session需要考虑Session粘滞或Redis集中存储,这是常见的坑。

2.2 JWT方案:无状态令牌的完整请求流

JWT的全称是JSON Web Token。它把用户身份信息和过期时间打包成一个字符串,用签名保证不可篡改,然后下发给客户端。

一个标准JWT由三部分组成:Header(头部)、Payload(载荷)、Signature(签名),用点号分隔:xxxxx.yyyyy.zzzzz。

  • Header里声明令牌类型和签名算法,比如{"alg":"HS256","typ":"JWT"}。
  • Payload里放用户相关数据和过期时间,比如{"sub":"10001","name":"zhangsan","exp":1735689600}。
  • Signature是对前两部分的签名,具体逻辑是:先对Header和Payload分别做Base64Url编码,拼成字符串,再用约定的密钥(如your-256-bit-secret)通过HMAC-SHA256算法算出一个签名,追加在后面。

登录成功后,客户端拿到JWT。之后的请求在Authorization头里带上完整令牌,格式是固定的:

GET /api/user/profile HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDAwMSIsImV4cCI6MTczNTY4OTYwMH0.xxxx

服务端拿到令牌后,拆开Header和Payload,再用密钥重新计算签名,比对一致且没有过期,就认定身份有效。全程不需要查询任何Session存储,这便是“无状态”的核心含义。

2.3 一张表说透两种方案的差异

维度Cookie-SessionJWT
状态存储位置服务端(内存/Redis)客户端令牌本身
服务端是否保存会话是否(无状态)
跨域支持受Cookie跨域策略限制天然支持,请求头可自定义
移动端支持需要手动处理Cookie直接使用Token
主动吊销会话简单,删Session即可难,需要配合黑名单或刷新令牌
分布式扩展需要Session共享方案天然友好,不需要共享存储
数据可见性服务端控制,客户端不可见Payload是Base64编码,明文可读
请求体积通常只有一个短IDPayload越大请求头越大

这张表里最容易被忽略的是“主动吊销”这一行。很多项目选JWT就是因为“无状态”,结果后来说“我要把你的账号踢下线”,发现根本做不到,因为令牌在客户端手里,服务端没有记录。这是所有JWT方案都必须提前想清楚的trade-off。

3. 从Java Web到SPA:两种方案的工程落地路径

3.1 Java Web里Cookie-Session的经典实现

Java Web用Cookie-Session登录,很多老项目都这么写。用户体验上很顺:登录成功后,通过HttpSession获取session对象,往里塞用户ID,同时让容器自动下发JESSIONID Cookie。

关键点在于,如果不加约束,默认的JSESSIONID是有安全问题的。我改造老项目时发现原代码只做了两件事:request.getSession().setAttribute("userId", userId),然后就没有然后了。既没有设置HttpOnly,也没有设置过期时间,被抓包后直接把JSESSIONID一替换就能登录别人的账号。

正确的做法是主动设置会话Cookie的安全属性:

Cookie cookie = new Cookie("SESSION", session.getId()); cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/"); cookie.setMaxAge(30 * 60); // 30分钟,单位秒 response.addCookie(cookie);

HttpOnly告诉浏览器这个Cookie不允许被JavaScript读取,能挡掉大部分XSS窃取Cookie的攻击;Secure保证只在HTTPS连接上传送;Path限定作用范围;MaxAge控制存活时间。

服务端读取端也要检查Session是否超时,并在登录成功或密码修改后主动session.invalidate(),防止会话固定攻击。

3.2 Spring Security整合JWT的关键配置

项目改成前后端分离之后,原来的Session方案没法满足多端需求,我改用Spring Security加JWT。整合逻辑并不复杂,核心就三块:过滤器、令牌工具类、适配后的Security配置。

先写一个JwtUtil工具类,负责生成和解析令牌:

public class JwtUtil { // 实际项目中应该放到配置中心,而不是写死在代码里 private static final String SECRET = "your-256-bit-secret"; private static final long EXPIRE = 30L * 60 * 1000; // 30分钟 public static String generateToken(String userId, String username) { return Jwts.builder() .setSubject(userId) .claim("username", username) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }

然后写一个过滤器,在Spring Security的过滤器链中插入JWT校验逻辑:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.getSubject()); request.setAttribute("username", claims.get("username")); } catch (JwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } } chain.doFilter(request, response); } }

最后在Security配置里注册这个过滤器,并关掉默认的Session登录、CSRF防护(因为认证信息已经不再走Cookie了):

http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

需要注意的是,JWT验证时需要做好异常分类:令牌过期、签名错误、令牌格式非法,应该返回不同的错误码,而不是统一401,否则前端根本分不清是登录过期还是被篡改。

3.3 前后端分离SPA项目的JWT实践

前端项目接入JWT的坑主要在“令牌放哪里”。放在localStorage里简单,但XSS攻击一旦得手就全盘泄露;放在Cookie里稍微安全一点(可以配合HttpOnly),但就要处理Cookie跨域和CSRF问题。

个人实践是:短期的access token放内存变量,刷新页面后用刷新令牌重新获取;如果项目对安全性要求没那么高,放localStorage但配合严格的CSP策略也能用。前端还需要在axios或fetch的拦截器里统一加上Authorization头,并在收到401时跳转到登录页。

4. 令牌过期与续签:无状态方案的生死线

4.1 为什么过期时间是绕不开的

JWT签发出去之后,服务端没有留存任何记录,无法主动让令牌失效。如果令牌永不过期,一旦泄露就等同于把账号拱手送给攻击者,之后做任何安全加固都白搭。所以过期时间是JWT设计的底线。

但设置多长是个问题。设太短,用户老被踢下线,体验糟糕;设太长,泄露窗口变大,安全风险高。行业里常见的做法是15分钟到2小时之间,具体根据业务类型权衡。金融类系统尽量短,资讯类系统可以长一些。

4.2 滑动续期的工程实现

一种常见的续期方案是“滑动续期”:用户的令牌已经验证通过了,如果剩余有效期不足一定比例,就签发一个新令牌返回给前端。

实现逻辑不复杂:在过滤器校验token时,取出exp跟当前时间对比,如果剩余时间小于总有效期的1/3,就在响应头里写入新token。前端在响应拦截器里捕获这个头,替换本地保存的token。

long expTime = claims.getExpiration().getTime(); long now = System.currentTimeMillis(); long expireInterval = expTime - now; long totalValidity = 30L * 60 * 1000; if (expireInterval < totalValidity / 3) { String newToken = JwtUtil.generateToken(claims.getSubject(), (String) claims.get("username")); response.setHeader("X-New-Token", newToken); }

优点是完全基于无状态模型,不用额外存储;缺点是令牌实际没有“延长”多少,而且服务器重启后判断逻辑必须在网关或统一入口实现,否则每个服务各生各的,前端会被搞晕。

4.3 刷新令牌与Redis失活方案

更严谨的做法是引入Refresh Token机制。登录时返回两个令牌:短期access token(15分钟)用于业务接口,长期refresh token(7天)只用于换取新的access token。access token过期后,前端持refresh token调用刷新接口换取新令牌;refresh token过期,强制重新登录。

Refresh Token本身还是要考虑吊销问题,我的做法是把refresh token的哈希存在Redis里,过期时间跟token一致。用户主动注销时直接删除Redis中的哈希,即使refresh token还在客户端手里,也无法换取新令牌。这样既保留了JWT无状态校验的轻量,又弥补了无法主动吊销的短板。

4.4 黑名单机制与jti字段

另一个兜底方案是用Redis做“黑名单”。在服务端生成JWT时在Payload里加一个jti(JWT ID)字段,值是UUID。需要吊销某个令牌时,把它的jti写入Redis黑名单,剩余有效期是多少就设置多长的TTL。每次请求校验JWT时,先查一下jti是否在黑名单里。

这个方案保留了JWT的无状态校验,只在“需要吊销”的极少数情况下查询Redis,整体性能损耗很小,适合“用户改密码后所有旧令牌失效”之类的场景。

5. 安全攻防视角:Cookie伪造与JWT漏洞的根源

5.1 Cookie伪造的套路与防护

Cookie伪造的核心思路是:客户端提交的Cookie内容是可控的,服务端如果信任了这些内容且没有做完整性校验,身份边界就被击穿了。

最常见的攻击场景有两种。一是服务端只通过Cookie里的某个字段(比如userId)判断身份,没有校验签名,攻击者改一下值就变成了别人。二是会话固定攻击:攻击者提前拿到一个未认证的sessionId,诱导受害者使用这个sessionId登录,登录成功后攻击者复用同一个sessionId进入受害者账号。

防护手段其实不复杂:服务端给Cookie加签名或加密;登录成功后强制轮换sessionId;设置HttpOnly、Secure、SameSite属性;敏感操作重新要求输入密码。实际上各大厂做登录Cookie也是如此思路,抓包看似能拿到Cookie字段,但伪造一个合法签名几乎不现实。

5.2 JWT的几大漏洞与默认密钥教训

JWT本身只是一个协议,漏洞往往不是出在协议上,而是出在使用它的方式上。

  • alg=none攻击:如果把Header里的算法改成none,且服务端校验不严格,JWT就没有签名保护,攻击者随便改Payload都不会被察觉。
  • 算法混淆攻击:服务端公钥是公开的,如果允许客户端指定算法,攻击者把RS256改成HS256,然后用公钥作为HMAC密钥去签名,服务端拿公钥验签就通过了。
  • 弱密钥/默认密钥:签名密钥如果是弱口令或者默认值,攻击者可以直接暴力破解出密钥,伪造任意令牌。
  • Payload明文泄露:JWT的Payload只是Base64Url编码,不是加密,放进去的手机号、邮箱全是明文可见。有人以为JWT是加密的,这是严重的误解。
  • 过期时间不校验:部分代码只验签不校验exp,过期令牌照样通行。

这里重点说一下默认密钥问题。国内有个很典型的案例是Nacos的nacos默认密钥身份认证绕过漏洞(CNVD-2023-17316)。Nacos在版本迭代中内部使用了JWT做身份认证,但默认密钥是固定的、公开的,攻击者拿到这个默认密钥后可以自行签发JWT令牌,直接绕过管理后台登录。后来官方要求用户强制修改默认密钥才修复。这个案例给所有人的教训是:JWT密钥必须随机生成、独立管理、定期轮换,绝不能使用框架自带的默认值,更不能写死在代码仓库里。

5.3 调试与安全测试中的实用工具操作

日常开发和安全自测中,我用的比较多的是Burp Suite和Reqable这两个工具。

Burp Suite改Cookie做安全测试很方便。用Proxy拦截住请求,右键发送到Repeater,在请求头里直接修改Cookie: JSESSIONID=xxx,可以快速验证服务端是否严格校验会话;如果项目用的是JWT,在Repeater里把Authorization头的token换一个伪造值,看看服务端返回什么状态码,能很快判断异常处理是否完善。

Reqable在接口调试中对Cookie的处理做得很顺手。在接口A的响应里返回了Set-Cookie,接口B需要带上这个Cookie才能访问,手动复制粘贴很啰嗦。Reqable可以在接口列表中设置变量提取规则,把响应里的Cookie值提取到一个变量,然后在接口B的环境变量或Auth配置里引用这个变量,实现“响应Cookie自动代入下一个接口”,省去不少重复劳动。

安全测试只是手段,核心还是验证一件事:服务端对所有非法凭证都必须给出明确拒绝。401、403、还有业务统一的错误响应码,都要经得起篡改测试才算过关。

结尾

我个人的体会是,Cookie和JWT没有绝对的谁优谁劣,关键是你想清楚项目到底需要“有状态”还是“无状态”的登录体系。传统同源Web应用用Session-Cookie依然稳定可靠;前后端分离、多端接入、分布式部署的场景,JWT配合刷新令牌和短过期时间是最稳的组合。别盲从“JWT更安全”这种说法,安全是设计出来的,不是选型选出来的。最后分享一个建议:无论选哪种方案,把过期时间、续签机制、主动吊销和密钥管理这四件事提前设计好,比纠结用哪个方案重要得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 8:06:03

ax调度是什么?从贝叶斯优化到自动试验循环的完整实战解析

最近后台总有朋友问我同一个问题&#xff1a;你说的ax调度到底是什么&#xff1f;其实我第一次看到“ax调度”这个说法也愣了一下&#xff0c;后来才明白&#xff0c;大家说的就是把Meta开源的Ax平台用起来。Ax本身是一个面向自适应试验的开源平台&#xff0c;它最早用于内部的…

作者头像 李华
网站建设 2026/9/25 8:04:31

Vivado工程迁移指南:用TCL脚本实现版本兼容与IP核优化

前阵子合作团队发来一个老工程&#xff0c;2018.3版本建的&#xff0c;我本机装的是2022.2。双击.xpr弹了个版本升级提示&#xff0c;点完Upgrade之后&#xff0c;综合跑到一半报了几个IP核错误&#xff0c;其中一个MIG的DDR4控制器直接锁死状态。折腾了大半天&#xff0c;最后…

作者头像 李华
网站建设 2026/9/25 8:00:17

Atlas 300V 24G部署YOLO全流程:从硬件选型到CANN模型转换推理调优

做AI推理部署的同学&#xff0c;这两年应该都绕不开Atlas这个名字。尤其是当你在电商、安防、工业质检这些场景里做视觉检测时&#xff0c;昇腾的Atlas系列加速卡几乎是性价比绕不过去的选项。最近后台收到不少留言&#xff0c;问Atlas 300V 24G到底是不是一张运算加速卡&#…

作者头像 李华
网站建设 2026/9/25 7:55:04

从漏洞分析到主动防护:安全加固与路由器配置实践

抱歉&#xff0c;我无法协助撰写涉及漏洞分析、漏洞链拆解或攻击链构建等技术细节的内容&#xff0c;这类话题可能被用于网络攻击或入侵行为&#xff0c;即使以防御或研究为背景&#xff0c;也存在被滥用的风险。如果你有路由器配置、安全加固、大模型应用等其他合规主题的写作…

作者头像 李华