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

📅 2026/7/5 1:46:18
从电商项目课程设计,搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题
从电商项目课程设计搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题做课程设计的时候我们小组完成了电商项目写完了双层拦截器鉴权和 Redis 缓存但说实话当时只是照着敲代码并不真正理解这两个东西为什么要这样设计。写完之后自己回头梳理了一遍原理记录下来也希望能帮到有同样困惑的同学。一、先搞清楚我们的鉴权到底是不是纯 JWT很多资料一说 JWT就是服务器不用存储任何东西token 里带着所有信息验证签名就行天然适合分布式。但我们项目里的做法其实是JWT Redis 的混合方案跟教科书里说的无状态 JWT不完全一样。先看核心代码// 登录时生成 tokenpublicstaticStringgenToken(StringuserId,Stringusername){returnJWT.create().withAudience(userId).sign(Algorithm.HMAC256(username));}// 每次请求都要走的第一层拦截器Stringtokenrequest.getHeader(token);UseruserredisTemplate.opsForValue().get(RedisConstants.USER_TOKEN_KEYtoken);if(usernull){thrownewServiceException(Constants.TOKEN_ERROR,token失效,请重新登陆);}UserHolder.saveUser(user);redisTemplate.expire(RedisConstants.USER_TOKEN_KEYtoken,RedisConstants.USER_TOKEN_TTL,TimeUnit.MINUTES);关键点在这里拦截器判断用户有没有登录靠的不是解析 JWT 里的内容而是拿这个 token 去 Redis 里查有没有对应的 User。也就是说token 本质上被当成了一把钥匙真正的用户信息还是存在服务端Redis里的。这和最原始的Session 机制其实是同一个思路Session 机制登录成功后服务器在内存/Redis 里保存一份用户会话给浏览器一个sessionId通常放在 Cookie 里。以后每次请求带着这个sessionId服务器凭它去查会话数据。我们项目里的做法登录成功后服务器生成一个 JWT 字符串当token同时把用户信息存进 Rediskey 就是user:token:token。以后每次请求带着这个 token服务器凭它去 Redis 查用户数据。这俩本质上是一回事状态都保存在服务端客户端只拿一个凭证。区别只是这个凭证的载体——一个是随机生成的 sessionId一个是格式化的 JWT 字符串以及存储位置的默认实现——传统 session 常放在内存或 Servlet 容器管理这里换成了 Redis。那如果真的用纯 JWT不查 Redis会怎样真正的无状态 JWT 应该是拦截器只做一件事——验证签名、解析出里面的 payload比如 userId、role不去查任何数据库/缓存所有信息都从 token 本身解出来。如果我们的项目改成这种做法会有什么后果优点不用查 Redis 了理论上每个服务器节点都能独立验证 token扩展性更好这也是 JWT 最常被提起的卖点。代价也很明显没法主动踢人下线。比如管理员想封禁一个用户、或者用户改了密码想让所有旧 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){StringredisKeyGOOD_TOKEN_KEYid;GoodredisGoodvalueOperations.get(redisKey);if(redisGood!null){redisTemplate.expire(redisKey,GOOD_TOKEN_TTL,TimeUnit.MINUTES);returnredisGood;}GooddbGoodgetOne(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_KEYgood.getId());}这里有一个很容易被忽略、但面试官很爱问的细节为什么写操作是删除缓存而不是更新缓存如果写操作直接更新缓存set新值表面上看好像更高效少一次查库但会有两个问题并发写的时候容易把旧数据留在缓存里。假设两个请求同时更新同一个商品请求 A 先把数据库改成新价格 100请求 B 紧接着把数据库改成新价格 200但如果两个请求更新缓存的顺序反过来网络延迟导致 A 的缓存写入晚于 B缓存里最终留下的是新价格 100而数据库里其实是新价格 200——缓存和数据库不一致了而且不会自动恢复除非缓存过期。如果这条数据本来就没人读过直接写缓存是浪费。删除缓存的做法等下次真的有人来读这条数据时才回填天然避免了写了缓存但没人用的浪费。而删除缓存这种做法最坏情况下也只是让下一次读请求多查一次数据库、重新回填缓存里绝不会留下一个确定是错的旧值——顶多是短暂地没有缓存而不是缓存里是错的。这就是业界常说的 **Cache-Aside 模式里更新数据库 删除缓存优于更新数据库 更新缓存**的原因。那这样就完全没有不一致的风险了吗严格来说没有 100% 保证还有一种经典的竞态条件请求 A 读缓存没命中准备去查数据库就在 A 查数据库、还没来得及回填缓存之前请求 B 把这条数据更新了并删除了缓存此时缓存本来就是空的删除等于没做什么A 才慢悠悠地把它查到的旧数据回填进缓存结果缓存里躺着一个旧值一直到 TTL 过期才会被清除。这就是为什么我们代码里给缓存加了TTLGOOD_TOKEN_TTL30 分钟——TTL 存在的意义很大程度上就是给这种理论上小概率但无法完全避免的不一致情况兜底就算真的出现了脏数据最多也只脏 30 分钟到期自动清除、下次读取重新回填。这也是我认为这道题目面试官更想听到的答案不是问你有没有 100% 的解决方案而是问你知不知道这个方案的边界在哪、怎么兜底。三、写在最后这两个设计点JWT Redis 混合鉴权、Cache-Aside 缓存策略看起来是课程设计里很小的两块代码但拆开看背后其实是分布式系统里两个很基础也很常被问到的话题状态该放哪里和缓存一致性怎么兜底。写这篇总结的过程也是我自己把跟着敲代码补成知道为什么这么写的过程希望对同样在啃这块内容的同学有帮助。