news 2026/7/5 1:46:12

从电商项目课程设计,搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从电商项目课程设计,搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题

从电商项目课程设计,搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题

做课程设计的时候,我们小组完成了电商项目,写完了双层拦截器鉴权和 Redis 缓存,但说实话,当时只是照着敲代码,并不真正理解这两个东西为什么要这样设计。写完之后自己回头梳理了一遍原理,记录下来,也希望能帮到有同样困惑的同学。

一、先搞清楚:我们的鉴权到底是不是"纯 JWT"

很多资料一说 JWT,就是"服务器不用存储任何东西,token 里带着所有信息,验证签名就行,天然适合分布式"。但我们项目里的做法,其实是JWT + Redis 的混合方案,跟教科书里说的"无状态 JWT"不完全一样。先看核心代码:

// 登录时生成 tokenpublicstaticStringgenToken(StringuserId,Stringusername){returnJWT.create().withAudience(userId).sign(Algorithm.HMAC256(username));}
// 每次请求都要走的第一层拦截器Stringtoken=request.getHeader("token");Useruser=redisTemplate.opsForValue().get(RedisConstants.USER_TOKEN_KEY+token);if(user==null){thrownewServiceException(Constants.TOKEN_ERROR,"token失效,请重新登陆");}UserHolder.saveUser(user);redisTemplate.expire(RedisConstants.USER_TOKEN_KEY+token,RedisConstants.USER_TOKEN_TTL,TimeUnit.MINUTES);

关键点在这里:拦截器判断用户有没有登录,靠的不是解析 JWT 里的内容,而是拿这个 token 去 Redis 里查有没有对应的 User。也就是说,token 本质上被当成了一把"钥匙",真正的用户信息还是存在服务端(Redis)里的。这和最原始的Session 机制其实是同一个思路:

  • Session 机制:登录成功后,服务器在内存/Redis 里保存一份"用户会话",给浏览器一个sessionId(通常放在 Cookie 里)。以后每次请求带着这个sessionId,服务器凭它去查会话数据。

  • 我们项目里的做法:登录成功后,服务器生成一个 JWT 字符串当token,同时把用户信息存进 Redis,key 就是user:token:<token>。以后每次请求带着这个 token,服务器凭它去 Redis 查用户数据。

这俩本质上是一回事:状态都保存在服务端,客户端只拿一个"凭证"。区别只是这个凭证的载体——一个是随机生成的 sessionId,一个是格式化的 JWT 字符串,以及存储位置的默认实现——传统 session 常放在内存或 Servlet 容器管理,这里换成了 Redis。

那如果真的用"纯 JWT"(不查 Redis)会怎样?

真正的无状态 JWT 应该是:拦截器只做一件事——验证签名、解析出里面的 payload(比如 userId、role),不去查任何数据库/缓存,所有信息都从 token 本身解出来。

如果我们的项目改成这种做法,会有什么后果?

  1. 优点:不用查 Redis 了,理论上每个服务器节点都能独立验证 token,扩展性更好,这也是 JWT 最常被提起的卖点。

  2. 代价也很明显

    • 没法主动"踢人下线"。比如管理员想封禁一个用户、或者用户改了密码想让所有旧 token 失效,纯 JWT 做不到——因为 token 一旦签发,只要没到过期时间,签名验证一直能通过,服务端没有地方能"删除"它。而我们项目里因为把 token 和 User 的映射存在 Redis,只要redisTemplate.delete()一下,这个 token 立刻失效,这是纯 JWT 做不到的。

    • 续期不自然。我们代码里每次请求都会redisTemplate.expire(...)刷新过期时间,实现"只要一直在用就不会掉线"的效果。纯 JWT 的过期时间是签发时就写死在 token 里的,想要滑动续期得额外发一个"刷新 token"的机制,更复杂。

    • 用户信息变了不会立刻生效。比如管理员改了某用户的角色,纯 JWT 因为角色信息编码在 token 里,除非用户重新登录换新 token,否则旧 token 里的角色信息是过时的。我们的方案因为每次都是现查 Redis 里的最新 User,改了立刻生效。

所以,我们项目的选择其实是工程上很常见的一种折中:用 JWT 的形式,但保留服务端可控的能力,牺牲一点点"纯无状态"的理论优雅,换来更好的可控性。这也是我在准备面试的时候才想明白的一点——技术选型没有绝对的对错,得看你要解决的问题是什么。

二、Redis Cache-Aside:缓存和数据库不一致了怎么办

商品详情页这种"读多写少"的数据,我们用了旁路缓存(Cache-Aside)模式,核心代码:

// 读:先查缓存,没有再查数据库,查到了回填缓存publicGoodgetGoodById(Longid){StringredisKey=GOOD_TOKEN_KEY+id;GoodredisGood=valueOperations.get(redisKey);if(redisGood!=null){redisTemplate.expire(redisKey,GOOD_TOKEN_TTL,TimeUnit.MINUTES);returnredisGood;}GooddbGood=getOne(queryWrapper);// 查数据库if(dbGood!=null){valueOperations.set(redisKey,dbGood);// 回填缓存redisTemplate.expire(redisKey,GOOD_TOKEN_TTL,TimeUnit.MINUTES);}returndbGood;}// 写:更新数据库之后,直接删除缓存,而不是更新缓存publicvoidupdate(Goodgood){updateById(good);redisTemplate.delete(GOOD_TOKEN_KEY+good.getId());}

这里有一个很容易被忽略、但面试官很爱问的细节:为什么写操作是"删除缓存"而不是"更新缓存"?

如果写操作直接更新缓存(set新值),表面上看好像更高效(少一次查库),但会有两个问题:

  1. 并发写的时候容易把旧数据留在缓存里。假设两个请求同时更新同一个商品:请求 A 先把数据库改成"新价格 100",请求 B 紧接着把数据库改成"新价格 200";但如果两个请求更新缓存的顺序反过来(网络延迟导致 A 的缓存写入晚于 B),缓存里最终留下的是"新价格 100",而数据库里其实是"新价格 200"——缓存和数据库不一致了,而且不会自动恢复,除非缓存过期。

  2. 如果这条数据本来就没人读过,直接写缓存是浪费。删除缓存的做法,等下次真的有人来读这条数据时才回填,天然避免了"写了缓存但没人用"的浪费。

而"删除缓存"这种做法,最坏情况下也只是让下一次读请求多查一次数据库、重新回填,缓存里绝不会留下一个"确定是错的"旧值——顶多是短暂地"没有缓存",而不是"缓存里是错的"。这就是业界常说的 **Cache-Aside 模式里"更新数据库 + 删除缓存"优于"更新数据库 + 更新缓存"**的原因。

那这样就完全没有不一致的风险了吗?

严格来说没有 100% 保证,还有一种经典的竞态条件:

  1. 请求 A 读缓存,没命中,准备去查数据库;

  2. 就在 A 查数据库、还没来得及回填缓存之前,请求 B 把这条数据更新了,并删除了缓存(此时缓存本来就是空的,删除等于没做什么);

  3. A 才慢悠悠地把它查到的旧数据回填进缓存;

  4. 结果缓存里躺着一个旧值,一直到 TTL 过期才会被清除。

这就是为什么我们代码里给缓存加了TTL(GOOD_TOKEN_TTL,30 分钟)——TTL 存在的意义,很大程度上就是给这种"理论上小概率但无法完全避免"的不一致情况兜底:就算真的出现了脏数据,最多也只脏 30 分钟,到期自动清除、下次读取重新回填。这也是我认为这道题目面试官更想听到的答案:不是问你有没有 100% 的解决方案,而是问你知不知道这个方案的边界在哪、怎么兜底。

三、写在最后

这两个设计点(JWT + Redis 混合鉴权、Cache-Aside 缓存策略)看起来是课程设计里很小的两块代码,但拆开看,背后其实是分布式系统里两个很基础也很常被问到的话题:状态该放哪里缓存一致性怎么兜底。写这篇总结的过程,也是我自己把"跟着敲代码"补成"知道为什么这么写"的过程,希望对同样在啃这块内容的同学有帮助。

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

面试官问:“模型一本正经胡说时,logprobs 抓得到吗?“

面试官问&#xff1a;“模型一本正经胡说时&#xff0c;logprobs 抓得到吗&#xff1f;” “3 年 LLM 应用开发&#xff0c;主导过企业 RAG 知识库和多个 Agent 项目&#xff0c;熟悉主流大模型 API 与推理优化。” 简历挺漂亮。我没问框架&#xff0c;先问了个最朴素的问题&am…

作者头像 李华
网站建设 2026/7/5 1:40:45

你往 AI 里装的那些 skill,打开看过一眼吗?

写在前面&#xff1a;如果你也在往 Claude、Codex 这些 agent 里装第三方 skill&#xff0c;装之前最好扫一遍。工具是 NVIDIA 出的 SkillSpector&#xff0c;pip 一行装完&#xff0c;省心。相信大部分人都从 GitHub 上一口气装过很多 skill——写前端的、跟 Codex 配合干活的…

作者头像 李华
网站建设 2026/7/5 1:40:46

【图像分类】实战ResNet——从零构建到CIFAR-10分类(Pytorch)

1. 初识ResNet&#xff1a;为什么它能解决深度神经网络的瓶颈问题第一次接触ResNet是在处理一个图像分类项目时&#xff0c;当时我遇到了所有深度学习工程师都会面临的经典问题&#xff1a;随着网络层数增加&#xff0c;模型性能不升反降。这就像给小孩子叠积木&#xff0c;叠得…

作者头像 李华
网站建设 2026/7/5 1:40:12

Agent记忆系统设计与实现

一个 Agent 有没有记忆&#xff0c;很大程度上决定了它只是用完即弃的工具&#xff0c;还是能越用越懂你的搭档。没有记忆的 Agent&#xff0c;每次会话都从零开始&#xff0c;你得反复交代项目背景、代码风格、踩过的坑&#xff1b;有记忆的 Agent&#xff0c;能跨会话保留对你…

作者头像 李华
网站建设 2026/7/5 1:39:31

别把知识图谱做成高级文档库——定制化做企业级知识图谱

别把知识图谱做成高级文档库 知识图谱的价值&#xff0c;不是把文档连成网&#xff0c;而是让知识可以被治理 最近我们在做一个知识图谱项目&#xff0c;越做越觉得&#xff0c;很多人对图谱的期待其实放错了地方。 大家一听“知识图谱”&#xff0c;脑子里很容易出现一张很…

作者头像 李华
网站建设 2026/7/5 1:38:06

【面板数据模型实战】从理论到Stata/R/Python实现与选择

1. 面板数据模型入门&#xff1a;从超市会员卡说起想象你是一家连锁超市的数据分析师&#xff0c;手上有过去三年每位会员的月度消费记录。这些数据既有横向维度&#xff08;不同会员&#xff09;&#xff0c;又有纵向维度&#xff08;不同月份&#xff09;&#xff0c;这就是典…

作者头像 李华