Spring Boot整合JWT+Shiro+Redis实现token无感续签
2026/9/8 10:30:04 网站建设 项目流程

简介:面向Java后端开发者,提供一套Spring Boot整合JWT、Shiro与Redis的Token自动刷新实现方案,适用于需要单点登录、接口权限控制与长效会话保持的Web项目。资源围绕令牌认证与无感续期展开,包含Shiro自定义Realm、JWT工具类、Redis令牌存储、刷新过滤器等核心模块,能直接参考快速落地到业务系统中。压缩包内共32个文件,以27个Java源码为主,配合3个XML配置、1个SQL脚本和1个YAML配置文件,覆盖认证流程、数据初始化与参数设置,整体仅38KB,目录结构清晰便于定位。目前已有6589人学习下载,对希望理解Shiro与JWT结合方式、解决Token过期问题的开发者颇具参考价值。通过阅读该项目,能够掌握JWT生成与解析、Shiro多Realm认证、Redis键过期与自动续期的完整思路,缩短自行摸索的时间。 后端做登录认证,token过期这件事,几乎每个Java开发都遇到过。最恨的不是用户要重新登录,而是用户刚填完一个长表单、写完一段长文案,提交的一瞬间系统弹出一句“登录已过期,请重新登录”。数据丢了,体验也崩了。后来我在Spring Boot项目里把JWT、Shiro、Redis三个东西组合到一套认证体系里,实现了token自动刷新,也就是大家常说的“无感续签”,这套方案在多个后台管理系统里跑了很久,稳定性和可维护性都经得起检验。这篇文章把完整的设计思路、关键代码、以及踩过的坑全部整理出来,给正准备做权限认证升级的朋友一个可以直接抄作业的参考。

先说明一下这套方案的适用人群。如果你正在做单体后台管理系统,或者刚接手一个登录模块被吐槽“老是掉线”的项目,又或者你想搞懂token续签到底怎么落地,那这篇文章很适合你。方案核心是:JWT负责无状态身份令牌,Shiro负责认证授权和拦截链路,Redis负责token状态存储和刷新控制。三者分工明确,没有谁替代谁,而是互相补位。

1. 写这套认证体系前,先想清楚三个问题

1.1 为什么纯JWT在真实业务里不够用

JWT最吸引人的地方就是无状态、自包含。服务端不用存session,拿到token一验签名就能确认身份,天然适合分布式和跨域场景。但真实业务里它有几个绕不开的痛点。

第一,签发之后不可控。JWT一旦发出去,只要没到过期时间,它始终有效。用户改了密码、被封禁了、角色权限降级了,服务端想让它立刻失效,做不到。第二,过期就得重新登录。token有效期设短了,用户频繁被踢下线;设长了,token泄露后的风险窗口又太大,怎么调都不舒服。第三,续签能力弱。JWT续签只能重新生成一个新的token,但旧的token在失效前依然能被使用,这在“强制下线”类场景下就是漏洞。

所以需要Redis来兜底。Redis在这里不是存session替代品,而是给无状态的JWT加上一层“状态控制”。token的合法生命周期由Redis说了算,JWT的签名和过期时间反而退居其次。这样既保留JWT无状态传输的优势,又获得了服务端主动撤销的能力。

1.2 Shiro和JWT怎么分工

很多人一听Shiro就想到session管理,感觉和JWT“无状态”是冲突的。其实关键在于怎么分配职责。

我的做法是:Shiro负责认证授权流程,JWT负责身份传递载体。Shiro的Realm从JWT中解析出用户身份,再把用户权限信息加载进来;Shiro的Filter负责拦截请求、触发登录校验;Shiro的注解(比如@RequiresPermissions)负责接口级权限控制。JWT本身只存放userId、username这类非敏感信息,权限数据还是走系统查询,这样权限变更立即生效,不会被token里缓存的旧权限拖后腿。

Shiro默认依赖session,我们通过配置把session存储禁用掉,让Subject的创建和销毁完全围绕JWT来进行。这一步做好了,Shiro和JWT非但不冲突,反而互补得相当舒服。

1.3 Redis在自动刷新中的定位

Redis在整个方案里主要有四个用途:保存token签发的“白名单状态”、保存用户会话信息、控制token续签的窗口期、实现登出后的主动失效。

自动刷新的核心思想是滑动过期(Sliding Expiration):用户只要一直在操作,token有效期就不断顺延;用户长时间不活跃,token自然过期。这个策略比固定过期友好太多。Redis可以很方便地给token设置TTL,每次请求时如果判定需要续签,就重新签发token并更新Redis里的过期时间,前端拿到的是一套“一直在活动的会话”。

2. 版本搭配和基础依赖,先避开几个老坑

2.1 依赖版本怎么选

这套方案的坑,一半在版本兼容上。我推荐一套目前实测非常稳的组合:Spring Boot 2.7.x + Shiro 1.8.0 + jjwt 0.11.5 + Redis(Lettuce客户端)。

组件推荐版本说明
Spring Boot2.7.x成熟稳定,与Shiro 1.8兼容好
shiro-spring-boot-starter1.8.0自动配置省事,支持Spring Boot 2.x
jjwt-api / jjwt-impl / jjwt-jackson0.11.5API清晰,支持JDK 8+
Spring Data Redis随Boot版本默认Lettuce连接池

不建议用jjwt 0.9.1。那个版本在高版本JDK下会报ClassNotFoundException: javax.xml.bind.DatatypeConverter,需要额外引入JAXB依赖,属于历史遗留问题。0.11.x改用Keys.hmacShaKeyFor()生成密钥,用parserBuilder()解析,API更安全,也少踩很多坑。如果你用的是Spring Boot 3.x,要注意Shiro 1.8官方对Jakarta EE的兼容并不好,需要加额外的适配包,建议还是老老实实停在Boot 2.7,这是当前最省心的组合。

2.2 JWT工具类,核心就这三段

JWT的生成、解析、校验逻辑不复杂,但一定要写对。我封装了一个JwtUtil类,核心代码分三块。

@Component public class JwtUtil { // 密钥建议从配置文件读取,不要硬编码在代码里 private static final SecretKey KEY = Keys.hmacShaKeyFor( "your-secret-key-please-change-to-a-long-random-string-32bytes".getBytes(StandardCharsets.UTF_8)); private static final long ACCESS_TOKEN_TTL = 2 * 60 * 60 * 1000L; // 2小时 public String createToken(Long userId, String username) { Date now = new Date(); Date expire = new Date(now.getTime() + ACCESS_TOKEN_TTL); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .setIssuedAt(now) .setExpiration(expire) .signWith(KEY, SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(KEY) .build() .parseClaimsJws(token) .getBody(); } }

生成和解析是对称操作。setSubject里我放的是userId,这样后面Shiro的Principal直接用Long类型,避免字符串和数字之间反复转换。claim里放username,方便日志和审计,但绝对不能放密码等敏感信息。signWith一定要指定算法并传入SecretKey对象,如果直接传字符串,不同版本的jjwt处理方式不一致,很容易埋坑。

2.3 Redis相关的配置要点

Redis配置不多说,核心是使用StringRedisTemplate来操作token键值,避免对象序列化带来的类型混乱问题。键的设计推荐这种格式:auth:token:{userId}:{token},值是token本身或JSON化的用户会话信息,TTL和access_token的有效期保持一致。这样每个用户的每个token独立可控,后续做踢人下线、设备管理都有数据基础。

3. 实现token自动刷新的核心设计

3.1 双过期时间模型

这套方案里其实存在两套过期时间,理解清楚这个概念,后面代码才写得明白。

第一套是JWT自身的exp过期时间,我这里设成2小时。第二套是Redis里存储的token记录TTL,同样也是2小时。但Redis的TTL不是固定的,每次用户活跃时,如果满足刷新条件,Redis里的TTL会被重置。换句话说:JWT的过期时间兜底,Redis的TTL掌握实际生命周期

实际运行效果是:用户登录后连续操作3小时,因为每2小时内的活跃请求都会触发续签,Redis里的token记录一直续命,JWT本身也一直在重新签发,所以用户无感知,不会掉线。用户关了电脑去睡觉,第二天Redis里的TTL早就清零了,这时候携带旧token访问系统,Redis查不到记录,直接判定未授权,用户重新登录即可。

3.2 滑动过期:每个请求如何判断要不要刷新

自动刷新不是定时任务,而是“懒刷新”。我在JwtFilter里拦截每个请求,解析出token后执行一段刷新判断逻辑。核心算法如下:

private String handleTokenRefresh(String token, HttpServletResponse response) { Claims claims = jwtUtil.parseToken(token); Long userId = Long.valueOf(claims.getSubject()); String redisKey = "auth:token:" + userId + ":" + token; // 1. Redis里没有这条token记录,说明已失效或已登出,直接拒绝 if (!redisTemplate.hasKey(redisKey)) { throw new UnauthorizedException("token已失效,请重新登录"); } // 2. 计算剩余有效时间 long remain = claims.getExpiration().getTime() - System.currentTimeMillis(); long threshold = 30 * 60 * 1000L; // 30分钟 // 3. 剩余时间不足阈值,签发新token并刷新Redis if (remain < threshold) { String newToken = jwtUtil.createToken(userId, claims.get("username", String.class)); redisTemplate.delete(redisKey); redisTemplate.opsForValue().set( "auth:token:" + userId + ":" + newToken, newToken, ACCESS_TOKEN_TTL, TimeUnit.MILLISECONDS); response.setHeader("X-Auth-Token", newToken); return newToken; } // 4. 还有充足时间,原样返回 return token; }

为什么阈值设成30分钟而不是更低?根据我的实测,如果阈值设成5分钟,那只有非常活跃的接口才会触发刷新,对“长时间填写表单后一次性提交”的场景保护不足;如果设成90分钟,token就等于长期有效,失去了定期轮换的意义。2小时有效期加30分钟阈值,安全性和体验比较均衡。刷新生效后,前端从响应头X-Auth-Token里读取新token,替换本地旧token即可。服务端不管前端何时切换新token,旧token在Redis里的记录已经删除了,所以不存在“双token同时有效”的状态窗口。

3.3 并发请求下的重复刷新问题

同一时刻多个接口并发请求,每个请求都发现剩余时间不足阈值,各自生成新token并刷新Redis,就会出现A请求生成token1、B请求生成token2,最终Redis里只剩token2,但A请求携带token1回来时反而被判定失效。这个问题我确实遇到过。

解法有几种。最简单的是加JVM锁,因为单体应用里所有请求都在同一个JVM中:

private synchronized String refreshTokenInRedis(Long userId, String oldToken, String username) { String currentToken = redisTemplate.opsForValue().get("auth:token:" + userId + ":" + oldToken); if (currentToken == null) { throw new UnauthorizedException("token已失效"); } String newToken = jwtUtil.createToken(userId, username); redisTemplate.delete("auth:token:" + userId + ":" + oldToken); redisTemplate.opsForValue().set( "auth:token:" + userId + ":" + newToken, newToken, ACCESS_TOKEN_TTL, TimeUnit.MILLISECONDS); return newToken; }

synchronized锁的是JVM内部,对单体系统完全够用。如果以后拆微服务,可以把锁升级成Redis分布式锁,用setIfAbsent加一个锁键,拿到锁的节点负责刷新,其他节点等待后重查Redis。另外我建议在刷新动作前再检查一次Redis里的旧token是否还存在,如果已经被其他线程删掉了,说明另一个请求已经刷新成功,当前请求直接返回旧的token放行即可,不要重复生成。

3.4 登出与token主动撤销

这套方案里,登出操作变得极其简单,一个delete命令就完成:

@PostMapping("/logout") public Result logout(@RequestHeader("Authorization") String authHeader) { String token = resolveToken(authHeader); Claims claims = jwtUtil.parseToken(token); redisTemplate.delete("auth:token:" + claims.getSubject() + ":" + token); return Result.success(); }

Redis记录删掉后,旧token即使还在JWT的有效期内,再来访问时也会因为查不到记录而被拦截。这一点正是纯JWT方案做不到的“后悔药”。我还建议在Redis里维护一个用户维度的会话列表,用Set结构存储该用户所有有效的token值,这样管理员后台做“强制下线”“踢人”功能时,遍历Set删除即可,不需要额外设计表。

4. Shiro整合细节和完整链路落地

4.1 Shiro配置类,重点看两处

Shiro整合的关键是配置类,我把核心配置梳理出来。第一处是SecurityManager里要禁用session存储,第二处是ShiroFilterFactoryBean里要注册自定义的JwtFilter。

@Configuration public class ShiroConfig { @Bean public JwtFilter jwtFilter() { return new JwtFilter(); } @Bean public Realm jwtRealm() { return new JwtRealm(); } @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(realm); // 禁用session,Session的创建和存储全部关闭 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator sessionEvaluator = new DefaultSessionStorageEvaluator(); sessionEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(sessionEvaluator); manager.setSubjectDAO(subjectDAO); return manager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, JwtFilter jwtFilter) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 注册自定义JwtFilter,替换默认的authc拦截器 factoryBean.getFilters().put("jwt", jwtFilter); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "anon"); filterChainDefinitionMap.put("/**", "jwt"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }

禁用session存储这步非常关键。如果不关,Shiro每次请求都会尝试创建session,轻则多出一堆Redis垃圾数据,重则接口响应莫名变慢,甚至出现“明明登录了但权限丢失”的诡异问题。关掉之后,Subject的身份信息完全靠JWT还原,链路干净很多。

过滤链里有一个很隐蔽的坑:登录接口必须配置成anon,否则shiro默认的认证过滤器会抢在JwtFilter之前拦截掉登录请求,前端永远登不进去。/logout也配成anon,避免“登出接口还要带token才能调用”的尬局面。其他接口统一走jwt过滤器。

4.2 JwtRealm、JwtToken、JwtFilter三个类配合

Shiro的原生逻辑是:Filter把请求包装成AuthenticationToken,交给SecurityManager,SecurityManager再交给Realm做认证。我们要做的就是让这个过程适配JWT。

JwtToken很简单,就是一个AuthenticationToken的实现类,持有JWT字符串。

public class JwtToken implements AuthenticationToken { private final String jwt; public JwtToken(String jwt) { this.jwt = jwt; } @Override public Object getPrincipal() { return jwt; } @Override public Object getCredentials() { return jwt; } }

JwtRealm负责两个核心方法。doGetAuthenticationInfo负责校验token、查用户;doGetAuthorizationInfo负责加载权限数据。

public class JwtRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String jwt = (String) token.getCredentials(); Claims claims; try { claims = jwtUtil.parseToken(jwt); } catch (Exception e) { throw new AuthenticationException("token无效或已过期"); } Long userId = Long.valueOf(claims.getSubject()); // 这里查一次用户,确保用户未被禁用、删除 User user = userService.getById(userId); if (user == null || user.getStatus() == 0) { throw new AuthenticationException("用户不存在或已被禁用"); } return new SimpleAuthenticationInfo(userId, jwt, getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Long userId = (Long) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 根据userId查角色、权限集合 info.addRoles(userService.listRoles(userId)); info.addStringPermissions(userService.listPermissions(userId)); return info; } }

注意Realm里我用@Autowired注入Service,有人会遇到为null的情况,通常是因为Realm被new出来而不是被Spring管理。解决方法是把Realm也注册成@Bean(上面配置类里已经写了),这样Spring容器才能完成注入。

JwtFilter继承BasicHttpAuthenticationFilter,重写两个方法:

public class JwtFilter extends BasicHttpAuthenticationFilter { @Autowired private JwtUtil jwtUtil; @Autowired private StringRedisTemplate redisTemplate; @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpRequest = WebUtils.toHttp(request); // 放行OPTIONS预检请求 if (HttpMethod.OPTIONS.name().equalsIgnoreCase(httpRequest.getMethod())) { return true; } return executeLogin(httpRequest, WebUtils.toHttp(response)); } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpRequest = WebUtils.toHttp(request); HttpServletResponse httpResponse = WebUtils.toHttp(response); String token = resolveToken(httpRequest.getHeader("Authorization")); if (token == null) { throw new AuthenticationException("请求头中未找到token"); } // 先走刷新逻辑,刷新后token可能被替换 String newToken = handleTokenRefresh(token, httpResponse); // 用最新的token执行Shiro登录,把用户信息塞进Subject getSubject(request, response).login(new JwtToken(newToken)); return true; } private String resolveToken(String authHeader) { if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } return authHeader; } }

这里有个细节:我先把刷新逻辑放在Shiro登录之前,这样Redis状态校验和token轮换在认证前就完成了,后续Realm里拿到的一定是当前有效的token。如果先执行Shiro登录再刷新,会出现Redis里的token已经被更新、但Subject里持有的还是旧token的不一致情况。

BasicHttpAuthenticationFilter默认在登录失败时会重定向到loginUrl,这对前后端分离项目非常不友好。所以异常抛出后,我建议在onAccessDenied里统一处理成JSON响应返回。多个请求并发时synchronized方法会短暂排队,但锁粒度很小,实测对接口性能影响可以忽略。

4.3 登录接口如何发token

登录成功后,同时把token写进Redis,构成完整的闭环。这里我贴一下登录接口的关键写法:

@PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } String token = jwtUtil.createToken(user.getId(), user.getUsername()); redisTemplate.opsForValue().set( "auth:token:" + user.getId() + ":" + token, token, ACCESS_TOKEN_TTL, TimeUnit.MILLISECONDS); return Result.success().put("token", token); }

登录接口本身不做刷新判断,因为新token刚签发是全新的,有效期完整。刷新逻辑只发生在后续的业务请求里。这里的Redis写入和JwtFilter里的刷新逻辑共享同一套TTL常量,避免两边设置不一致。

4.4 请求链路的完整时序

把这套方案串起来,一次带token的请求走完是这样:

  1. 前端请求携带Authorization: Bearer xxxx
  2. JwtFilter拦截请求,先放行OPTIONS预检。
  3. 从Header解析token,调用刷新逻辑,检查Redis里token是否存在、剩余有效期是否小于阈值。
  4. 如果剩余时间不足,签发新token,删除Redis旧记录,写入新记录,并在响应头X-Auth-Token带上新token。
  5. 用最终可用的token调用Subject.login(),进入Shiro认证,Realm解析身份和权限。
  6. 请求进入Controller,Shiro注解执行接口级权限校验。
  7. 前端读取响应头里的新token,替换本地存储,下次请求自动使用新token。

这套链路把“自动刷新”和“权限校验”合二为一,业务代码完全不用关心token续签这件事。

5. 常见问题排查与避坑记录

这套方案我已经看了很多人在网上提问,整理成表格方便大家直接查。

现象根本原因解决思路
解析JWT报SignatureException签名密钥不一致,或构建Key对象时编码不统一所有地方都用同一个SecretKey对象,不要在配置里手输中文密钥不指定UTF-8
Shiro的JwtFilter不生效,接口直接403Filter没注册到ShiroFilterFactoryBean,或者bean名字自定义不规范检查factoryBean.getFilters().put("jwt", jwtFilter)是否执行,filter要声明为IoC管理的Bean
登录接口都进不去,直接报未授权登录地址没配anon,被默认认证过滤器拦截/login/logout配置为anon,其他接口再走jwt
刷新后旧token依然能访问并发请求里两个线程都拿到了旧token,一个刷新删key,另一个已经通过校验对刷新方法加synchronized锁,或者用Redis分布式锁,确保同一时刻只有一个刷新动作
接口响应很慢,Redis里大量垃圾keyShiro默认创建session并存储按上文配置禁用session存储
Authorization入参被日志打印出来,不安全感爆棚日志框架打印了请求全部Header对Header打印做脱敏,只记录前10位
前端刷新后收到的响应头里找不到X-Auth-Token跨域CORS没配置暴露该自定义响应头服务端CORS配置里增加exposedHeaders("X-Auth-Token")
OPTIONS预检请求被拦截自定义filter没有放行OPTIONSisAccessAllowed里直接对OPTIONS返回true
Spring Boot 3 + Shiro报错Shiro 1.8基于Servlet/Jakarta EE兼容性问题方案推荐用Spring Boot 2.7,或者引入shiro的jakarta适配
注解@RequiresPermissions不生效缺少DefaultAdvisorAutoProxyCreator或开启注解代理配置类里加一个静态的DefaultAdvisorAutoProxyCreator,setUsePrefix(true)

再分享几个从实践里总结的细节技巧。

第一,密钥管理必须重视。密钥至少32字节,不要硬编码在Java类里,放在application.yml里并用环境变量注入。生产环境建议用配置中心或KMS托管。JWT的Payload里只放非敏感标识,绝不能放密码、手机号、身份证这类数据。

第二,异常响应的格式要统一。Shiro抛出的AuthenticationException和AuthorizationException默认会走页面重定向,前后端分离项目要统一捕获并返回JSON。可以在配置里注册一个全局异常处理器,把这几个异常单独映射,返回401403状态码,附带codemsg字段,前端好做拦截和提示。

第三,前端切换token的策略也有讲究。响应头X-Auth-Token只有在发生刷新时才会出现,所以前端正常情况不需要处理,只有读到该响应头时才把它覆盖到本地存储。而且要注意在同一个请求的响应处理里完成替换,而不是等页面下一次回调节点再换。

关于安全加固,这里特别提醒一句:JWT历史上出过alg=none绕过签名校验的漏洞。在使用jjwt解析时,务必严格指定签名算法并用SecretKey对象,不要用那种接受任意alg的宽松解析库,也不要接受不带签名的token。解析代码里要捕获ExpiredJwtExceptionSignatureException等异常,统一抛出业务异常,避免把底层异常细节直接回传给前端。

6. 双token方案的一个补充说明

有些网上的方案会采用access_token + refresh_token这种双token机制,简单说就是:短时token负责请求,长时refresh token负责续期,access过期后前端用refresh token换新的。这套方案在开放平台场景下确实好用,但在我这套“后台管理系统”的场景里,双token有两个问题:一是前端要多管理一个refresh_token,存储暴露面变大;二是每次换token都要前端主动发起请求,做不到完全无感。

我实现的是单token + 滑动过期方案,刷新动作隐藏在拦截器里,用户无感知,前端代码量也更小。如果你的项目以后要开放第三方API授权能力,那时候再加双token机制也不迟。两种方案的取舍我个人的建议是:内部系统用单token滑动过期,对外平台用双token。

最后说几句大实话

这套方案我在项目里稳定运行一段时间之后,最大的感触是:把刷新和校验放进拦截器里形成闭环,比在业务代码里到处塞“检查过期、重新登录”逻辑舒服太多。业务层永远拿到的都是干净且有效的登录态,没有人再去纠结token哪一秒过期。

如果你现在项目里还是纯JWT裸奔,或者还在用传统session方式做登录认证,我建议你找个周末把Redis加上,把这套滑动过期逻辑移植过去。配置文件不复杂,代码量也不大,但用户体感会明显上一个台阶。“用户一直用就一直不掉线”,这句话听起来很简单,真正做到位,需要把JWT、Shiro、Redis三者之间的边界划分清楚。希望这篇文章能帮你少走几步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询