news 2026/10/15 15:22:40

Spring Boot+Vue前后端分离整合Shiro与JWT无状态认证实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot+Vue前后端分离整合Shiro与JWT无状态认证实践

最近在给一个内部管理系统做前后端分离改造,后端是 Spring Boot,前端是 Vue 全家桶。原系统的权限体系用了很多年的 Shiro,所以第一反应是“不就是把 session 换成 token 嘛”,结果真动手之后才发现,Shiro 这套老框架默认走的是服务端 Session + 重定向逻辑,到了前后端分离场景里处处别扭。折腾几天之后,我整理出了一套可以快速落地的整合方案,把认证、授权、跨域、无状态会话这些点全部过了一遍。

这篇文章就是用我的实际接入过程来讲这件事,适合谁看?三种人:第一种,老项目是 Shiro,现在要拆前后端;第二种,新项目选了 Shiro 做权限,但不想再被 session 绑架;第三种,网上 Shiro 教程看了一堆,一接入 Vue/小程序就翻车的人。文章不吹概念,直接给你能跑通的依赖、配置、过滤器和 Realm 代码,以及我踩过的坑。如果你只是想要一个最小可运行版本,跟着第 2、3 章做就行;如果担心上线出问题,第 4、5 章才是真正的精华。

1. 项目背景与整合思路

1.1 为什么传统 Shiro 模式在前后端分离下“水土不服”

Shiro 的核心能力可以压缩成三个词:认证、授权、会话管理。在老式单体 Web 项目里,浏览器和 Tomcat 之间有 Cookie 和 Session 的天然通道,Shiro 的 authc 过滤器会在用户未登录时,直接返回 302 重定向到配置好的登录页。这个流程在服务端渲染的年代非常好用,前后端都是同一个域名,浏览器自动带 Cookie,整个会话体系几乎不用自己落地。

但到了前后端分离,情况就变了。前端可能部署在另一个域名,后端只对外暴露 JSON API,浏览器不会主动帮你维护 Session。你用 Shiro 默认配置会遇到三个明显的问题:

  • 未登录请求会收到 302 而不是 401,前端 axios 根本没法拦截处理。
  • Shiro 的 Subject 基于 Session 存储,请求之间靠 JSESSIONID 关联,后端一旦集群部署,Session 就必须引入 Redis 作为额外存储。
  • 权限注解虽然能用,但登录状态完全绑定在服务端 Session 上,和“无状态 API”的诉求直接冲突。

所以整合的第一步,不是急着写配置,而是想清楚一件事:要把 Shiro 的“认证”和“授权”能力保留下来,把“会话”这个环节换成更符合前后端分离的 token 载体。

1.2 方案选型:JWT + 无状态 Shiro 的落地路径

我最终采用的方案可以概括为:前端在登录成功后拿到一个 JWT,后续所有请求在 Header 里带这个 token,后端用自定义 Shiro Filter 统一解析 token,再交给 Realm 完成权限加载,最后由 Shiro 的注解体系控制接口权限。

这个方案的好处是改动集中。你不需要把 Shiro 连根拔起,只需要:

  • 关闭或绕开默认 Session 机制。
  • 用一个自定义 JwtFilter 替换默认的 authc 过滤器。
  • 让 Realm 支持从 JWT 中解析身份。
  • 登录接口从服务端返回 token,而不是种 Cookie。

那为什么不直接用 Redis 存 token?这里说句公道话:Redis token 方案需要每次请求都查一次缓存,服务端可以随时把用户踢下线,权限变更也能立刻生效,适合对权限时效要求极高的系统。但 JWT 的好处是无状态,后端不用存任何会话数据,集群部署天然友好,实现起来也更轻。我的建议是:如果是内部管理系统,权限变更不频繁,选 JWT 完全够用;如果要做开放平台或对用户下线有强诉求,再考虑 JWT + Redis 黑名单的组合。多数快速整合场景,JWT 最省事。

1.3 整合后的整体请求流程

我先把处理链路画出来,后面所有代码都是围绕这条链走的:

  1. 用户请求/api/login,传账号密码。
  2. 登录接口校验账号密码,成功后用用户信息生成 JWT 返回给前端。
  3. 前端把 JWT 放到请求头Authorization: Bearer 一串token。
  4. 后端所有/api/**请求进入自定义 JwtFilter。
  5. JwtFilter 先放行OPTIONS预检请求,然后解析 token:签名错了、过期了、token 不存在,直接返回 401 JSON。
  6. token 有效,则用 token 里的用户名构造 JwtToken,调用Subject.login(jwtToken)。
  7. Shiro 根据 Realm 的supports方法,把 JwtToken 交给对应 Realm,Realm 查出用户和权限,完成 Subject 身份标记。
  8. @RequiresPermissions("system:user:list")这类注解开始生效,权限不足返回 403 JSON。

这套链路保留了 Shiro 最核心的“Realm 负责取权限、SecurityManager 统一管理”的设计,只是把最外层的会话和过滤器换成 JWT 适配器。下面开始真正写代码。

2. 依赖引入与基础配置

2.1 版本选型与依赖清单

先说版本,这个坑最容易被忽视。Spring Boot 2.x 对应 Shiro 1.x,Spring Boot 3.x 已经切换到 Jakarta EE,要 Shiro 2.x 才支持,不要拿 1.x 硬靠 Boot 3,否则会出现各种ClassNotFoundException: javax.servlet.*。

我这套环境用的是 Spring Boot 2.7 + Shiro 1.9,需要三个依赖:

<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.9.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

注意,shiro-spring-boot-web-starter已经包含了 Shiro 和 Spring Boot 的自动配置,不需要再单独引shiro-web或shiro-spring,否则可能出现依赖冲突。JWT 库用 jjwt 就够了,没有必要为了一个 token 功能引一个全家桶工具包。

2.2 Shiro 核心配置类

接着是最关键的一个配置类。Shiro 在 Spring Boot 里的配置比原生 SSH 时代简单不少,核心 Bean 只有三个:SecurityManager、Realm、ShiroFilterChainDefinition。

@Configuration public class ShiroConfig { @Bean public UserRealm userRealm() { return new UserRealm(); } @Bean public DefaultWebSecurityManager securityManager(UserRealm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); // 关闭 Shiro 自带的 Session 存储,后续按无状态模式走 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); return securityManager; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); chain.addPathDefinition("/api/login", "anon"); chain.addPathDefinition("/api/logout", "anon"); // 放行静态资源和健康检查 chain.addPathDefinition("/favicon.ico", "anon"); chain.addPathDefinition("/actuator/health", "anon"); // 其余接口全部交给自定义 jwt 过滤器 chain.addPathDefinition("/**", "jwt"); return chain; } }

这里有两个初学者容易犯的错。第一,shiroFilterChainDefinition里的拦截器名称jwt不是 Shiro 内置的,必须手动把这个过滤器注册进 Shiro 的过滤链,不然启动直接报No filter named jwt。所以我给 JwtFilter 类上加了@Component("jwt"),让 Spring 容器里的这个 Bean 正好对应过滤链的名字,Shiro starter 会自动收集它。第二,路径匹配顺序和 Spring Security 一样,/**一定要放在最后,否则后面的 anon 配置全部失效。

2.3 自定义 JwtFilter 与过滤链的注册

JwtFilter 是整个无状态改造的核心入口。我选择继承 Shiro 自带的BasicHttpAuthenticationFilter,理由是这个类已经处理了请求头获取、写出响应等基础操作,可以少写很多样板代码。核心实现如下:

@Component("jwt") public class JwtFilter extends BasicHttpAuthenticationFilter { @Autowired private JwtUtils jwtUtils; @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // 跨域预检请求直接放行 if (HttpMethod.OPTIONS.equals(httpServletRequest.getMethod())) { return true; } // 登录、退出接口已经在过滤链里配置成 anon,这里做双保险 String uri = httpServletRequest.getRequestURI(); if (uri.endsWith("/api/login") || uri.endsWith("/api/logout")) { return true; } // 其他请求都交给 onAccessDenied 做统一认证 return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = jwtUtils.getTokenFromRequest(httpServletRequest); if (StringUtils.hasText(token)) { try { // 先做签名和过期校验,快速失败 jwtUtils.parseToken(token); executeLogin(request, response); return true; } catch (Exception e) { writeUnauthorizedResponse(response); return false; } } writeUnauthorizedResponse(response); return false; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { String token = jwtUtils.getTokenFromRequest((HttpServletRequest) request); JwtToken jwtToken = new JwtToken(token); Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { subject.login(jwtToken); } return true; } private void writeUnauthorizedResponse(ServletResponse response) throws IOException { response.setContentType("application/json;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或token已失效\"}"); } }

这里有个细节必须说明:BasicHttpAuthenticationFilter.isAccessAllowed返回 true 时请求会被直接放行,不会再执行认证。所以我把“是否需要认证”的判断放到isAccessAllowed,把“如何认证”放到onAccessDenied,只有没有 token 或者 token 校验失败时才中断请求。这样进入 Controller 时,SecurityUtils.getSubject()一定是已认证状态,权限注解才能正常拿到用户信息。

3. 认证与授权核心代码

3.1 自定义 JwtToken 与登录接口

JWT 方案下,Shiro 需要知道自己要处理一种新型 Token。虽然也可以直接复用UsernamePasswordToken,但那样语义上很别扭,后续 Realm 的supports也不好判断。我建议定义一个小类:

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

这里把principal和credentials都放 token 本身。你可能会问:principal 不应该是用户名吗?其实在 Realm 里可以从 token 解析出用户名,再拿用户名查库,所以 principal 保持 token 也能工作。不过个人经验是主键用用户 ID 更好,避免用户名变更导致权限缓存错乱。下面的代码用用户名演示,方便理解。

登录接口的职责很简单:接收账号密码,校验通过后签发 JWT。

@RestController @RequestMapping("/api") public class AuthController { @Autowired private UserService userService; @Autowired private JwtUtils jwtUtils; @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { User user = userService.findByUsername(request.getUsername()); if (user == null) { return Result.fail("用户不存在"); } // 这里用加密工具校验密码,不调 Shiro 的 UsernamePasswordToken boolean ok = PasswordUtils.matches(request.getPassword(), user.getPassword(), user.getSalt()); if (!ok) { return Result.fail("密码错误"); } if (user.getStatus() != 1) { return Result.fail("账号已被禁用"); } String token = jwtUtils.generateToken(user.getUsername(), user.getId()); return Result.ok(token); } }

严格说,这个登录接口绕过了 Shiro 的认证流程。但这种做法对快速整合很友好:登录是独立接口,没有 Shiro 过滤器参与,密码校验逻辑完全由业务层掌控。Shiro 的认证能力保留在“token 请求进来后识别当前用户是谁”,授权能力保留在 Realm 和注解上,两者各司其职。

3.2 Realm 实现:从 Token 到用户权限

Realm 是 Shiro 的“数据桥”,它负责把 Token 代表的身份转换成完整的用户和权限信息。我的UserRealm长这样:

public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private JwtUtils jwtUtils; @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); User user = userService.findByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(userService.listRoleCodes(user.getId())); info.addStringPermissions(userService.listPermissionCodes(user.getId())); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String jwt = (String) token.getPrincipal(); Claims claims = jwtUtils.parseToken(jwt); String username = claims.get("username", String.class); if (username == null) { throw new AuthenticationException("token中不存在用户名"); } User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } if (user.getStatus() != 1) { throw new LockedAccountException("账号已被禁用"); } // 注意这里不需要再校验密码,JWT 签名已经在过滤器里校验过了 return new SimpleAuthenticationInfo(user.getUsername(), jwt, getName()); } }

有几个细节容易被新手忽略。

第一,supports方法只认JwtToken。这意味着登录接口如果没有真正走 Shiro 的Subject.login(new UsernamePasswordToken(...)),Realm 就不会去校验账号密码。所以代码里业务层自己校验密码,这个关系你要理清楚。

第二,doGetAuthenticationInfo返回的SimpleAuthenticationInfo第一个参数,会成为SecurityUtils.getSubject().getPrincipal()的值,也就是后续权限注解里拿到的身份。如果你想把整个 User 对象放进去,也不是不行,但主键尽量简洁,避免序列化问题。

第三,权限部分一定要有兜底:如果服务查权限出错,建议抛出AuthorizationException,而不是返回空的 AuthorizationInfo,否则接口权限控制会“静默失守”,用户明明没有权限,但因为权限列表为空,@RequiresPermissions反而可能直接放行。这个细节最坑。

3.3 开启注解和 AOP 支持

写好了 Realm,还需要让 Shiro 的注解在 Spring MVC 里真正生效。核心是授权属性源的通知器,否则你写一百个@RequiresPermissions也不会拦截。

@Configuration public class ShiroConfig { // 接前面其他 Bean @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }

AuthorizationAttributeSourceAdvisor的作用是把@RequiresRoles、@RequiresPermissions这些注解翻译成 Shiro 的 AOP 拦截逻辑。DefaultAdvisorAutoProxyCreator是让 Spring AOP 能识别这个 Advisor。这两兄弟缺一个,注解都会静默失效,而且启动时不报错,只在实际调用时不做任何处理,非常隐蔽。

如果项目里已经自定义了 Spring AOP 切面,要注意代理方式。setProxyTargetClass(true)表示使用 CGLIB 代理。如果你的某个类是 final 的,或者方法不是 public,代理会失效,权限注解也会失效。所以,被权限注解标记的方法尽量写在 Service 接口实现类或者 Controller 的普通方法上。

3.4 权限缓存与动态权限刷新

权限数据每次都查数据库显然不划算。Shiro 自带CacheManager接口,最简单的是接一个内存缓存,比如MemoryConstrainedCacheManager,或者接 Redis。

@Bean public CacheManager cacheManager() { return new MemoryConstrainedCacheManager(); }

然后把 CacheManager 设置给 SecurityManager:

securityManager.setCacheManager(cacheManager());

加了缓存之后马上会遇到一个新问题:用户角色或权限变了,Shiro 缓存里还是老数据。标准的做法是在修改权限的接口里主动清理当前用户的授权缓存,比如调用:

Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { DefaultWebSecurityManager manager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); manager.getRealm().clearCache(subject.getPrincipals()); }

注意,如果启用了 Redis 缓存,而且权限数据粒度是“按用户 + 按应用”,缓存 key 的隔离很重要,别让 A 项目的权限缓存串到 B 项目。我在项目里习惯在 CacheManager 的 key 前缀里带一个租户或应用标识,线上排查权限问题时能省一半时间。

4. 前后端分离改造的关键细节

4.1 CORS 跨域与 Shiro 过滤链的顺序矛盾

这是前后端分离项目里 90% 的人都会踩的坑。前端页面跑在http://前端域名,后端接口跑在http://后端域名,浏览器发起非简单请求之前会先发一个OPTIONS预检请求。如果预检请求根本没有到达 Spring MVC 的 CORS 处理器,或者到达后被 Shiro 的认证过滤器拦了,前端控制台就会报CORS policy错误,但后端日志里什么都没有。

我的做法是两步走。第一步,在JwtFilter里无条件放行OPTIONS请求,也就是前面代码里写的:

if (HttpMethod.OPTIONS.equals(httpServletRequest.getMethod())) { return true; }

这一步保证即使 Shiro 过滤链先执行,预检请求也不会被 401 拦掉。

第二步,注册一个 Spring 的跨域过滤器,并保证它排在 Shiro 过滤器之前。常见方式是用FilterRegistrationBean:

@Bean public FilterRegistrationBean<CorsFilter> corsFilterRegistration() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source)); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); return bean; }

这里有个争议点:setAllowCredentials(true)时,addAllowedOriginPattern("*")在 Spring Boot 2.7 以上是允许的,但不能用addAllowedOrigin("*"),否则会和 credentials 冲突,启动不会报错但运行时报错。如果你项目里确实不需要带 Cookie,建议setAllowCredentials(false)。

还有一点容易被忽略:认证失败返回 401 时,如果 CORS 过滤器没有先执行,响应头里没有Access-Control-Allow-Origin,那么前端在跨域状态下根本读不到后端返回的 JSON,浏览器直接把请求判定为跨域失败。所以 Filter 顺序真的很重要。

提示:如果前端项目和后端 API 域名完全一致,也不涉及跨域,可以跳过 CorsFilter 那段;但生产环境仍然建议按跨域场景处理,因为以后业务很可能拆独立子域名。

4.2 无状态会话:别让 JSESSIONID 偷偷“复活”

很多项目用了 JWT,但测试时会发现登录后 Response Header 里还是会出现JSESSIONID,或者服务端内存里还有 Session 对象。原因就是 Shiro 默认的Subject还是基于 Session 的,SecurityManager一旦发现需要存储就顺手创建了 Session。

解决方式有两个,最好同时做。

第一种,在DefaultWebSecurityManager里把 Session 存储关掉,我第 2 章的DefaultSessionStorageEvaluator.setSessionStorageEnabled(false)就是这个目的。

第二种,自定义一个SubjectFactory,在创建 Subject 时传入空的 SessionContext,强制让 Subject 的 session 为空。代码大致是:

public class StatelessDefaultSubjectFactory extends DefaultSubjectFactory { @Override public Subject createSubject(SubjectContext context) { context.setSessionCreationEnabled(false); return super.createSubject(context); } }

然后在 SecurityManager 里设置:

securityManager.setSubjectFactory(new StatelessDefaultSubjectFactory());

这两步配合之后,Bean 容器里不会再出现 Shiro 管理的 Session。如果你的项目中因为其他历史原因必须保留 Session,那至少保证SecurityManager.setSessionManager用的是自定义的DefaultWebSessionManager,并且把sessionManager.setSessionIdCookieEnabled(false)关掉 Cookie 输出。否则即使用了 JWT,浏览器还是会拿到 JSESSIONID,前端 axios 可能还会自动维护一套无用的会话。

4.3 Token 过期、刷新与退出登录

JWT 是无状态的,这意味着服务端无法主动让一个已经签发的 token 失效。所以“退出登录”功能基本都是前端删除 token + 后端维护黑名单的组合。简单项目里我建议这样处理:

  • 前端退出时,删掉本地 token,请求GET /api/logout。后端即使什么都不做,只要保证 token 过期后不能使用,就已经达到目的。
  • 如果一定要服务端强制踢人,比如用户改密码后要求所有旧 token 失效,就用 Redis 存一个“用户 token 版本号”,JWT 里带上版本号,JwtFilter 解析后比对 Redis 里的版本号,不一致就拒绝。

刷新策略我建议做“滑动过期”。也就是每次请求都检查 token 剩余有效期,如果小于比如 30 分钟,就在响应头里放一个new-token,前端 axios 拦截到后自动替换本地 token。这样用户体验最好,也不会频繁要求用户重新登录。要注意的是刷新本身也有成本,建议刷新的触发阈值按业务容忍度调,不要每个请求都刷。

一个更简单的思路:把 JWT 过期时间设置成 8 小时,内部系统的使用场景够用了。如果 8 小时太长,设置 2 小时,配一个刷新接口。刷新接口和登录接口一样,走 anon 权限,但必须校验旧 token 的签名和用户状态,不能因为过期就无条件签发新 token。

5. 实操中遇到的问题排查与避坑

5.1 常见异常速查表

我把实际接入中高频碰到的现象整理成一个速查表,遇到问题直接对号入座。

现象原因解决方案
请求被重定向到 loginUrl,返回 302authc 过滤器默认行为改用自定义 jwt filter,onAccessDenied 中写 401 JSON
打印No filter named jwt过滤链里用了 jwt 但没注册过滤器给 JwtFilter 加上@Component("jwt"),确认 Bean 名称和链里的名称一致
注解 @RequiresPermissions 不生效没配置 AuthorizationAttributeSourceAdvisor配置 advisor 和 DefaultAdvisorAutoProxyCreator
前端报 CORS,但后端无日志Shiro 先拦截了 OPTIONSJwtFilter 放行 OPTIONS,CORS 过滤器 order 提前
能登录,但所有接口返回 401JwtFilter 里只校验了 token,没做 Subject 登录在 onAccessDenied 里调用 executeLogin,完成 subject.login
权限改了,用户立刻能用新权限权限缓存未清理修改权限后 clearCache 或 Subject.logout
报ConcurrentAccessException同一个请求里多次调用 Subject.login登录前先判断 isAuthenticated,每个请求只 login 一次
返回的 401 是 HTML使用了默认重定向或错误页自定义 filter 里设置 contentType 并写 JSON
JSESSIONID 一直出现Shiro 默认 Session 没关关闭 SessionStorageEnabled,设置自定义 SubjectFactory
静态资源/actuator 也被拦截过滤链配置了 /** 且没放行提前加 anon 路径,/** 放在最后

5.2 几个容易让新人翻车的细节

第一个是并发登录的问题。BasicHttpAuthenticationFilter在每次请求进来都会执行认证逻辑,如果你在过滤器里不加判断就Subject.login,同一个请求在多次转发时可能重复登录,抛出ConcurrentAccessException。解决办法是在执行登录之前先检查SecurityUtils.getSubject().isAuthenticated(),已经认证过就直接放行。

第二个是 filter 的响应编码。很多人收到 401 看响应内容是乱码,是因为没有设Content-Type: application/json;charset=UTF-8。这个细节无关技术难度,但直接影响前端联调效率。响应内容里也不要直接返回redirecting...之类的 Shiro 默认文案,前端会一脸懵。

第三个是过滤器里别做太多数据库查询。JwtFilter 其实只需要解析 token 并拿用户名,真正的用户校验和权限加载放到 Realm 里做。如果 filter 里也查一次用户,Realm 里再查一次,一个请求两三次数据库查询,压力全在 DB。合理做法是在 Realm 的doGetAuthenticationInfo里用缓存,或者干脆在 filter 里只做签名校验。

第四个是不要把 JWT 密钥硬编码到代码里。登录接口每次请求都会用到密钥,泄漏等于用户可以被任意伪造。建议放到配置中心或环境变量,比如shiro.jwt.secret。同时密钥至少 32 个字符,JWT 用的 HS256 算法要求密钥长度不少于 256 bit,也就是 32 字节。网上很多教程用secret这种短字符串,生产环境千万别学。

5.3 关于权限模型的一点建议

最后说点权限模型层面的事。Shiro 的授权模型很简单:角色 + 权限。快速整合时建议先做一套“用户-角色-权限”的三层关系表,权限点用模块:操作这种字符串表示。比如:

  • user:add:新增用户
  • user:delete:删除用户
  • order:export:导出订单

Controller 上写@RequiresPermissions("user:delete"),授权在 Realm 动态加载。这套模型在项目初期完全够用,也方便和前端按钮级权限联动:登录后后端把当前用户权限码列表返回给前端,前端根据权限码控制按钮显隐,后端再做一次真正的安全拦截。权限码风格统一、命名规范,比维护一张混乱的接口权限表好维护得多。

如果你的系统后面膨胀到几百个接口,建议再引入“角色分组 + 数据权限范围”的概念,但这些属于权限模型演进的问题,不是 Shiro 本身能帮你解决的。能在一开始把权限码设计好,已经赢在起跑线。


最后分享一个我自己的使用习惯:接入之前,我会先在本地用一个最小 Demo 把“无状态 Shiro + JWT”跑通,再倒回到真实项目里改配置。原因是 Shiro 的配置项很多,真实项目里各种 Filter、拦截器纠缠在一起,出错时你分不清是 Shiro 的问题还是业务代码的问题。最小 Demo 能帮你把 Shiro 的行为边界摸清楚,然后回项目里就好定位了。

这套方案稳定跑了一段时间之后,我的体感是:Shiro 本身不复杂,复杂的是“你打算怎么定义会话”和“你的过滤链顺序如何”。只要这两个问题想清楚,前后端分离整合 Shiro 就是一个半天能搞定的工作量。如果你正准备改造,按文章顺序先把 JwtFilter 和 Realm 搭出来,再处理跨域和缓存,基本不会走弯路。

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

Simscape机械臂建模与仿真:从刚体搭建到PID控制实战

简介&#xff1a;面向机械臂建模初学者与Simscape仿真开发者&#xff0c;这份压缩包提供了一个三自由度&#xff08;DOF&#xff09;机械手臂从物理建模到动力学仿真的完整案例。模型源自roboholicmaniacs&#xff0c;采用步进电机作为执行器&#xff0c;以三个关节的扭矩角作为…

作者头像 李华
网站建设 2026/10/15 15:22:02

碰碰好评卡功能模块开发实战指南 附全流程实现思路与踩坑汇总

碰碰好评卡功能模块开发实战指南 附全流程实现思路与踩坑汇总 在电商、本地生活、上门服务等各类线上业务场景中&#xff0c;用户评价是影响商品/服务转化的核心指标&#xff0c;碰碰好评卡作为引导用户主动完成评价的核心功能模块&#xff0c;需要兼顾触发灵活性、多端兼容性、…

作者头像 李华
网站建设 2026/10/15 15:22:01

Grad-CAM原理解析与工业级实现指南

1. 这不是“画个热力图”那么简单&#xff1a;Grad-CAM到底在解决什么问题&#xff1f;Grad-CAM可视化&#xff0c;这五个字最近在模型可解释性讨论里出现频率极高&#xff0c;但很多人一上手就卡在“为什么我的热力图全是噪点”“为什么关键区域没亮起来”“为什么不同层的图完…

作者头像 李华
网站建设 2026/10/15 15:21:42

Ceph RGW从零搭建全攻略:角色定位、配置实践与排障详解

刚开始学Ceph&#xff0c;很多人会把RGW当作一个里程碑去追求。我早期也一样&#xff0c;总想着在集群里加上几个rgw进程&#xff0c;能用S3客户端上传下载文件&#xff0c;就算是把对象存储跑通了。回头看&#xff0c;这种想法太乐观。RGW对外是一套HTTP服务&#xff0c;对内要…

作者头像 李华
网站建设 2026/10/15 15:21:18

天然气阀门选型与进口阀门核心技术解析

1. 天然气阀门选型核心要素解析在天然气输配系统中&#xff0c;阀门如同人体的"心脏瓣膜"&#xff0c;控制着介质流动的方向、压力和流量。2026年最新行业数据显示&#xff0c;全球天然气阀门市场规模已达187亿美元&#xff0c;其中进口阀门在高压、超低温等特殊工况…

作者头像 李华