
JWT的四种设计策略——轻量负载、缓存外置、上下文线程、统一验证JWT登录不是把token发出去就完事了。怎么存、存多少、验证后怎么加载完整信息、权限怎么验——这四个决策决定了你的认证系统是轻量还是臃肿、安全还是漏洞。这篇文章以一个生产环境的JWT实现为例拆解这四个设计策略。文章目录JWT的四种设计策略——轻量负载、缓存外置、上下文线程、统一验证一、为什么JWT本身要尽量轻二、完整信息存哪里——缓存外置三、验证后的信息放哪里——当前请求线程内全局可用四、权限统一验证——不在Controller里一个一个写五、附赠Token黑名单——踢人下线和防止重放六、四种策略总结七、结语一、为什么JWT本身要尽量轻JWT的负载在每次请求时都会被传输——Header里塞的token越长每个HTTP请求就多占一截网络带宽。如果你把用户的所有信息全存进token里——用户名、身份证号、机构名称、角色列表、权限地图——这个token可能几百个字符长每次请求都要带着它从客户端到网关到服务端全链路传输。策略一JWT负载只存最少必要信息。// 生成Access Token——只放三个字段publicStringgenerateAccessToken(StringpsnId,StringpsnName,StringunitId){returnJwts.builder().setSubject(psnId)// 唯一标识.claim(psnId,psnId).claim(psnName,psnName)// 显示用的名字.claim(unitId,unitId)// 所属机构.claim(type,access).setIssuer(browise).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()accessExpiry)).signWith(SignatureAlgorithm.HS256,secretKey).compact();}只放三个字段用户ID、显示名、机构ID。完整的用户信息、角色列表、权限清单全部不在token里——token只是一个钥匙不是房子。token里不放敏感信息——身份证号、密码hash、手机号这些永远不放进JWT的Claims。token在网络上传输虽然是HTTPS加密的但一旦被截获解密后的负载就会暴露。负载越轻暴露面越小。二、完整信息存哪里——缓存外置Token只验证你是谁但权限判断需要你能做什么。用户完整信息、机构树、角色列表、菜单权限——这些不能放token里也不能每次都查数据库。策略二JWT验证通过后从缓存加载完整用户信息。// CacheManager——加载完整用户档案publicUserProfilegetUserProfile(StringpsnId){// 先查缓存UserProfilecachedcache.get(psnId);if(cached!null)returncached;// 缓存未命中——查数据库// 1. 查 OP_PERSON OP_UNIT → 姓名、机构、状态// 2. 查 SYS_USER_ROLE → 角色列表UserProfileprofilenewUserProfile();profile.setPsnId(psnId);profile.setPsnName(personMapper.selectByPsnId(psnId).getPsnName());profile.setUnitId(personMapper.selectByPsnId(psnId).getUnitId());profile.setRoles(roleMapper.selectByPsnId(psnId));cache.put(psnId,profile,ttl);returnprofile;}两点关键设计缓存优先。用户每次请求都走JWT验证→取psnId→从缓存加载完整档案。缓存命中时不需要查数据库。这在每次请求都执行的Filter里是绝对性能保障——一个HTTP请求从认证到权限验证到业务处理中间的认证环节不能慢。缓存兜底。如果缓存未命中刚重启、缓存过期自动回源查数据库。下次请求就命中缓存了。这个机制保证了缓存在正常时的高性能同时在异常时不会阻塞用户请求。三、验证后的信息放哪里——当前请求线程内全局可用JWT验证通过、完整档案加载完成之后这些信息需要在本次请求的任何地方都能拿到——Controller、Service、DAO、AOP切面——不需要每个方法签名都加一个UserProfile参数。策略三验证通过后用户信息存入ThreadLocal请求结束时清理。// JwtAuthFilter 核心逻辑try{// 1. 从Header取token → 解析 → 拿到psnIdStringtokenrequest.getHeader(Authorization);StringpsnIdjwtUtil.getSubject(token);// 2. 检查黑名单是否被踢下线、是否被强制失效if(jwtBlacklist.isBlacklisted(psnId,token)){thrownewAuthException(Token已失效);}// 3. 从缓存加载完整用户档案UserProfileprofilecacheManager.getUserProfile(psnId);// 4. 检查用户状态是否被禁用if(!profile.isEnabled()){thrownewAuthException(用户已被禁用);}// 5. 写入ThreadLocal——本次请求全局可用CurrentUser.set(profile);// 6. 权限校验——这个路径当前用户能否访问if(isProtected(path)!menuAuthProvider.canAccess(profile,path)){thrownewAuthException(无权访问);}filterChain.doFilter(request,response);}finally{// 请求结束时清理ThreadLocal——防止线程池复用造成信息泄漏CurrentUser.clear();}finally里的CurrentUser.clear()是安全底线。Tomcat的线程池会复用线程——上一个请求的用户信息残留在ThreadLocal里下一个请求分配到同一个线程时不清理可能读到最后一个人的数据。不是你想太多是线程池的物理特性决定了必须这么做。四、权限统一验证——不在Controller里一个一个写不要在业务代码里写权限判断——同一个业务的增删改查分散在多个Controller里每个判断手动写一次漏一个就是越权漏洞。策略四权限校验集中在后端不用前端校验替代。两套机制互补路径级校验——在Filter里完成不进入业务代码// MenuAuthProvider——预加载角色→菜单映射publicbooleancanAccess(UserProfileprofile,Stringpath){// roleMenuMap每10分钟从 SYS_ROLE_MENU SYS_MENU 加载一次// 查缓存该用户的角色集合能否访问这个路径returnprofile.getRoles().stream().anyMatch(role-{SetStringallowedPathsroleMenuMap.get(role);returnallowedPaths!nullallowedPaths.stream().anyMatch(p-path.startsWith(p));});}方法级校验——通过注解控制数据权限DataScope(typeDataScopeType.SELF)// 只能看自己的数据publicListRecordqueryMyRecords(){...}DataScope(typeDataScopeType.UNIT)// 只能看本机构的数据publicListRecordqueryUnitRecords(){...}DataScope注解被AOP切面拦截根据scope类型自动生成SQL的WHERE条件——PSN_ID 当前用户或UNIT_ID 当前机构。业务代码不需要自己拼SQL权限条件注解决定了查询范围。加一个注解就自动过滤去掉注解就全量查询——权限控制从业务代码中剥离出来。五、附赠Token黑名单——踢人下线和防止重放Access Token签发后无法撤销——JWT本身没有服务端状态。但如果需要强制下线、用户改密码后让旧token失效必须有一个黑名单机制// JwtBlacklist——两种失效策略// 1. 按时间戳失效mass-invalidate该用户某个时间点之前签发的所有tokenpublicvoidinvalidateByTime(StringpsnId,longtimestamp){invalidateTimes.put(psnId,timestamp);}// 2. 按Token本身失效单个token被加入黑名单用户主动退出登录publicvoidinvalidateByToken(Stringtoken){tokenBlacklist.add(hash(token));}按时间戳失效处理改密码后所有旧token失效——不是逐个查token列表是记录一个时间点该用户在这个时间点之前签发的所有token全部无效。按token失效处理用户主动退出登录——单个token加入黑名单只失效这一个。两种机制混合使用覆盖了不同场景下的失效需求。六、四种策略总结策略做法解决什么问题轻量负载JWT只存psnIdpsnNameunitId减小网络传输、降低泄露风险缓存外置完整信息存JVM缓存先查缓存再查数据库token不挑重担每次请求不查库上下文线程验证后set进ThreadLocalfinally清理请求链路内全局可用不层层传参统一验证Filter路径校验 注解数据权限业务代码不写权限判断七、结语JWT认证的设计核心不是怎么签发token——这个网上有上万个教程。核心是签发之后怎么处理Token里存最少信息——只做身份标识不做信息载体。验证通过后从缓存加载完整档案——token是钥匙缓存是房间。用户信息放ThreadLocal——一次请求全局可用请求结束清理。权限校验集中在后端——不在业务方法里写if判断不依赖前端校验是否传了正确的人。这四条定下来认证系统的骨架就稳了。