资讯动态

SpringBoot整合Shiro与Thymeleaf实现RBAC权限控制

发布时间:2026/10/12 21:31:38 来源:尧图企业网站定制
1. 项目概述为什么一个权限控制 demo 值得花三小时细读“SpringBoot整合shiro整合thymeleaf详细示例”——这个标题看起来平平无奇甚至有点老派。但如果你正在维护一个后台管理系统、开发一个企业级内部平台或者刚接手一个遗留的 Spring Boot 项目发现登录后所有菜单都裸奔、角色权限形同虚设、页面上该隐藏的按钮明晃晃挂着……那你大概率会在这个标题里找到救命稻草。我做过不下二十个基于 Spring Boot 的管理后台从百人规模的 SaaS 工具到某高校实验室的设备预约系统凡是涉及“用户-角色-资源”三级管控的Shiro 几乎是绕不开的选项。它不像 Spring Security 那样自带全家桶和学习曲线也不像自研 RBAC 框架那样容易漏掉 session 并发校验或 rememberMe 的加密陷阱。Shiro 的核心优势就四个字可控、轻量、可插拔、不绑架。它不强制你用它的认证流程也不要求你改写整个 MVC 结构它只在你需要它的地方亮个相在你不想要它的时候默默退场。而 Thymeleaf则是这套权限体系落地到前端最自然的搭档。它不是 Vue 那种运行时渲染也不是 JSP 那种被时代淘汰的模板它是服务端预编译、支持 HTML 原生语法、能直接双击打开看效果的“所见即所得”模板引擎。更重要的是——它原生支持 Shiro 的方言shiro:xxx 标签不需要写一行 JavaScript 就能在div shiro:hasRoleadmin里精准控制 DOM 显示逻辑。这种服务端粒度的权限裁剪比前端 v-if 安全得多恶意用户删掉 DOM 或篡改 JS也看不到被拦截的按钮背后的接口地址和数据结构。所以这个“详细示例”本质不是教你怎么敲几行代码而是帮你建立一套权限控制的工程化思维闭环从认证流程怎么设计用户名密码LDAPJWT、会话怎么管理单点登录超时踢出并发登录限制、授权规则怎么定义URL 级方法级数据级、再到前端如何安全地呈现菜单动态加载按钮条件渲染403 页面友好提示。每一个环节Shiro 都留了钩子每一个钩子Thymeleaf 都配了语法糖。接下来的内容我会带你从零开始把这套组合拳拆成可复现、可调试、可扩展的每一步不跳过任何关键配置不省略任何踩坑细节——包括那个让 73% 新手卡住的 thymeleaf-extras-shiro 版本兼容问题以及 Shiro Filter 链顺序错一位导致整个权限失效的血泪教训。2. 整体架构设计与技术选型逻辑2.1 为什么是 Shiro 而不是 Spring Security这个问题我被问过太多次。答案不是“哪个更好”而是“哪个更合适”。Spring Security 功能强大OAuth2、OIDC、WebFlux 支持一应俱全但它像一辆豪华 SUV配置项多、启动慢、调试难一个EnableWebSecurity就可能让你掉进自动配置的迷宫。而 Shiro 更像一辆经过改装的皮卡——底盘扎实、改装自由、维修方便。具体到本项目我们选择 Shiro 的三个硬性理由第一轻量启动与低侵入性。Spring Boot 项目启动时Spring Security 会默认注册大量 BeanFilterChainProxy、SecurityContextPersistenceFilter等而 Shiro 只需一个ShiroFilterFactoryBean和几个自定义 Realm启动时间平均快 1.8 秒实测 2.4s vs 4.2s。这对本地开发调试、CI/CD 构建速度影响显著。更重要的是Shiro 不强制你继承它的 Controller 或 Service你的业务代码可以完全保持原样只需在需要权限的地方加注解或标签。第二会话管理更贴近传统 Web 场景。Spring Security 默认依赖HttpSession但在分布式环境下需要集成 Redis 或 Hazelcast配置复杂且容易因序列化问题导致NotSerializableException。Shiro 的Session是抽象层它天然支持EnterpriseCacheSessionDAO只要配置好缓存管理器就能无缝对接 Redis。我们实测过在某跨校区实验室系统中用 Shiro Redis 实现 session 共享仅需 3 行配置sessionDAO.setCacheManager(cacheManager)而 Spring Security 同等效果需引入spring-session-data-redis 自定义SessionRepositoryFilter 多个Bean注册出错概率高 3 倍。第三权限表达式更直观、更易调试。Shiro 的RequiresPermissions(user:delete)和 Thymeleaf 的shiro:hasPermissionuser:delete是一一映射的字符串你可以直接在数据库里查sys_permission表看到permission_code字段就是user:delete。而 Spring Security 的PreAuthorize(hasAuthority(ROLE_ADMIN))依赖GrantedAuthority接口实现调试时要翻源码才能确认 authority 字符串格式是否带ROLE_前缀。我们曾遇到一个线上事故前端传来的权限码是user:edit后端却在UserDetails.getAuthorities()里硬编码返回SimpleGrantedAuthority(USER_EDIT)大小写下划线全错结果权限永远不生效——这种错误在 Shiro 里一眼就能看出不匹配。提示这不是贬低 Spring Security。如果你的项目需要 OAuth2 第三方登录、微服务间 token 透传、或细粒度的 method-level AOP 切面Spring Security 是更优解。但本项目定位是“内部管理后台”目标明确快速上线、稳定可靠、运维简单。Shiro 在这个场景下就是一把恰到好处的瑞士军刀。2.2 为什么选 Thymeleaf 而非 Freemarker 或 VueThymeleaf 的胜出源于它对“服务端权限控制”的原生支持。Freemarker 虽然也能通过#if判断角色但它没有官方 Shiro 插件你需要自己写TemplateDirective或TemplateMethod每次升级版本都要重适配。Vue 则完全脱离服务端上下文——它只能靠 API 返回的权限列表做前端判断一旦用户禁用 JS 或抓包伪造响应权限就形同虚设。Thymeleaf 的thymeleaf-extras-shiro插件是 Shiro 官方团队维护的注意不是社区第三方它把 Shiro 的 Subject 对象直接注入模板上下文提供 7 个开箱即用的方言标签shiro:guest—— 匿名用户可见shiro:user—— 已登录用户可见含 rememberMeshiro:authenticated—— 仅密码登录用户可见排除 rememberMeshiro:notAuthenticated—— 未认证用户可见shiro:hasRole/shiro:lacksRole—— 角色判断shiro:hasPermission/shiro:lacksPermission—— 权限码判断这些标签不是简单的 if-else它们会调用SecurityUtils.getSubject().isPermitted(xxx)走的是完整的 Shiro 授权链路包括 Realm 查询、缓存命中、权限树递归计算。这意味着你在 Thymeleaf 里写的shiro:hasPermissionorder:export和你在 Controller 里写的subject.isPermitted(order:export)执行的是同一套逻辑返回的是同一个布尔值。这种一致性是其他模板引擎无法提供的。注意thymeleaf-extras-shiro的版本必须严格匹配 Shiro 版本。常见坑是Shiro 1.10.x 需要thymeleaf-extras-shiro2.3.0而旧版 1.2.x 只能用 1.2.1。版本不匹配会导致shiro:xxx标签完全不解析页面静默失败——连报错日志都没有。这个细节我会在后续实操环节重点演示如何验证。2.3 整体分层与数据流向设计本项目的分层不是教科书式的“Controller-Service-Dao”而是围绕权限生命周期构建的四层结构第一层认证入口Authentication Entry对应/login接口接收用户名密码调用Subject.login(token)。这里的关键是UsernamePasswordToken的封装——我们不直接 new而是用工厂类统一处理 rememberMe、验证码、IP 限制等扩展字段避免 Controller 里堆砌 if-else。第二层授权中心Authorization Core由自定义AuthorizingRealm实现重写doGetAuthenticationInfo()和doGetAuthorizationInfo()。前者查用户基础信息密码、状态、盐值后者查角色列表和权限列表。重点在于权限列表不是简单查sys_user_role关联表而是通过role_permission中间表递归加载角色拥有的所有权限码并缓存到SimpleAuthorizationInfo中。这样一次查询就能支撑后续所有isPermitted()调用。第三层权限网关Permission Gateway即ShiroFilterFactoryBean配置的 Filter 链。我们采用“URL 模式匹配 方法级注解”双保险静态资源/static/**,/favicon.ico放行登录页/login,/logout匿名访问其余路径默认需要认证authc关键操作如/user/delete则额外加perms[user:delete]。Filter 链顺序至关重要anon必须在authc之前logout必须在user之前否则 logout 请求会被 authc 拦截导致登出失败。第四层视图渲染View RenderingThymeleaf 模板通过shiro:xxx标签实时读取当前 Subject 状态。菜单栏动态加载逻辑放在menu.html片段中用th:each遍历menuservice.findMenuByUserId(#authentication.principal.id)返回的菜单树再用shiro:hasPermission控制每个菜单项的href是否可点击。这样既保证了菜单数据安全后端查库又实现了界面级权限裁剪前端控制。整个数据流是单向、可追溯的用户请求 → Filter 链拦截 → Realm 认证/授权 → Controller 处理业务 → Thymeleaf 渲染视图。没有双向绑定没有状态共享每一环都职责清晰出了问题能准确定位到哪一层。3. 核心模块实现与关键配置详解3.1 项目初始化与依赖管理我们使用 Spring Boot 2.7.18JDK 8u291 兼容版这是目前企业级项目最稳定的 LTS 版本。Maven 依赖需精确控制版本避免传递依赖冲突。以下是pom.xml中最关键的三组依赖及其版本逻辑!-- 1. Shiro 核心 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-web-starter/artifactId version2.3.0/version /dependency注意这里用的是shiro-spring-boot-web-starter而非老式的shiro-spring。前者是 Shiro 官方为 Spring Boot 2.x 重构的 starter自动装配ShiroFilterFactoryBean和SecurityManager省去手动配置Bean的麻烦。2.3.0版本对应 Shiro 1.10.1修复了 JDK 17 下的Unsafe调用异常。!-- 2. Thymeleaf Shiro 插件 -- dependency groupIdcom.github.theborakompanioni/groupId artifactIdthymeleaf-extras-shiro/artifactId version2.3.0/version /dependency这是目前唯一维护活跃的 Shiro-Thymeleaf 集成库GitHub 最后更新 2023-11。2.3.0版本专为 Shiro 1.10.x 编译修复了shiro:hasRole在嵌套标签中失效的 bug。切记不要用1.2.1版本它只支持 Shiro 1.2.x与 Spring Boot 2.7 不兼容。!-- 3. Redis 会话存储 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-redis/artifactId version3.3.1/version /dependencyshiro-redis是社区维护的 Redis 会话插件3.3.1版本支持 Lettuce 6.xSpring Boot 2.7 默认并修复了RedisCacheManager在集群模式下的 key 冲突问题。它比 Shiro 官方的shiro-ehcache更适合生产环境因为 Ehcache 是本地内存无法解决分布式 session 共享。实操心得我曾在一个部署了 4 个节点的测试环境里因误用了shiro-ehcache导致用户在 A 节点登录后切换到 B 节点就变成未登录状态。排查了两天才发现是缓存没共享。后来换成shiro-redis只改了 3 行配置redisManager.setHost(127.0.0.1)等问题立刻解决。所以只要项目有集群部署计划Redis 会话就是必选项不是可选项。3.2 Shiro 核心配置类详解ShiroConfig.java是整个权限体系的中枢它定义了SecurityManager、Realm、ShiroFilterFactoryBean三大核心 Bean。我们逐行解析其设计逻辑Configuration public class ShiroConfig { Bean public SecurityManager securityManager(MyRealm myRealm, CacheManager cacheManager) { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); securityManager.setCacheManager(cacheManager); // 开启全局会话检查用于踢出并发登录用户 securityManager.setSessionManager(defaultWebSessionManager()); return securityManager; } }SecurityManager是 Shiro 的心脏它协调 Realm、SessionManager、CacheManager 等组件。这里我们显式设置CacheManager是为了启用权限缓存——否则每次isPermitted()都要查数据库QPS 上不去。defaultWebSessionManager()是自定义会话管理器稍后详解。Bean public MyRealm myRealm(CacheManager cacheManager) { MyRealm realm new MyRealm(); realm.setCredentialsMatcher(hashedCredentialsMatcher()); // 密码加密匹配器 realm.setCacheManager(cacheManager); // 启用 Realm 级缓存 return realm; }MyRealm是我们自定义的认证/授权逻辑。setCredentialsMatcher()指定密码比对方式我们用HashedCredentialsMatcher支持 SHA-256 加盐加密。setCacheManager()是关键它让doGetAuthorizationInfo()返回的权限信息被缓存下次同用户请求直接从缓存取不用再查 DB。缓存 Key 是user::usernameTTL 设为 30 分钟平衡安全与性能。Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean bean new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); bean.setLoginUrl(/login); // 未登录跳转地址 bean.setSuccessUrl(/index); // 登录成功跳转地址 bean.setUnauthorizedUrl(/403); // 无权限跳转地址 // 定义 Filter 链 MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/static/**, anon); // 静态资源放行 filterChainDefinitionMap.put(/login, anon); // 登录页放行 filterChainDefinitionMap.put(/logout, logout); // 登出请求 filterChainDefinitionMap.put(/captcha, anon); // 验证码接口 filterChainDefinitionMap.put(/**, authc,perms); // 其他路径需认证权限检查 bean.setFilterChainDefinitionMap(filterChainDefinitionMap); return bean; }Filter 链是权限控制的第一道闸门。filterChainDefinitionMap的 key 是 URL 模式value 是 Filter 名称。anon表示匿名访问authc表示认证perms表示权限检查。顺序决定一切/static/**必须在/**之前否则所有静态资源都会被authc拦截。/logout必须用logoutFilter它会自动清除 session 和 rememberMe cookie比手动写session.invalidate()更可靠。提示permsFilter 默认只检查 URL 权限不检查方法权限。如果需要RequiresPermissions注解生效必须在ShiroConfig中额外添加Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }这个 Bean 会扫描所有RequiresXXX注解并织入 AOP 代理。漏掉它注解就只是注释。3.3 自定义 Realm 实现深度解析MyRealm.java继承AuthorizingRealm重写两个核心方法。我们不贴完整代码而是聚焦三个易错点第一doGetAuthenticationInfo()中的盐值处理。很多教程直接new SimpleAuthenticationInfo(user, user.getPassword(), getName())这是大忌。密码必须加盐存储否则彩虹表一扫就破。我们的做法是用户注册时用SecureRandom生成 16 字节盐值与密码拼接后用SHA-256加密存入数据库password和salt字段。认证时从 DB 查出 salt重新计算哈希Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); // 1. 从数据库查用户含 salt 字段 User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户名不存在); } if (user.getStatus() ! 1) { throw new DisabledAccountException(账户已被禁用); } // 2. 构造 SaltedAuthenticationInfoShiro 会自动用 salt 加密比对 ByteSource salt ByteSource.Util.bytes(user.getSalt()); return new SimpleAuthenticationInfo( user, // principal user.getPassword(), // hashed password salt, // salt getName() // realm name ); }关键在ByteSource.Util.bytes(user.getSalt())。user.getSalt()返回的是 Base64 编码的字符串如qXzF8YvR2aL9mN0pByteSource会自动解码为字节数组。如果直接传字符串Shiro 会把它当普通文本加盐导致比对失败。第二doGetAuthorizationInfo()的权限缓存策略。权限数据不能每次请求都查库必须缓存。但我们不缓存整个SimpleAuthorizationInfo对象而是缓存其内部的SetString权限码集合。原因SimpleAuthorizationInfo包含roles和stringPermissions两个集合如果缓存整个对象当用户角色变更时需要手动清空缓存容易遗漏。我们的方案是Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); // 1. 先查缓存key: perms:: username CacheObject, Object cache getCache(); SetString permissions (SetString) cache.get(perms:: username); if (permissions ! null) { return buildAuthorizationInfo(permissions); } // 2. 缓存未命中查数据库 permissions permissionService.findPermissionsByUsername(username); // 3. 写入缓存TTL 30 分钟 cache.put(perms:: username, permissions, 30, TimeUnit.MINUTES); return buildAuthorizationInfo(permissions); } private AuthorizationInfo buildAuthorizationInfo(SetString permissions) { SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); info.setStringPermissions(permissions); return info; }getCache()返回的是 Shiro 的Cache接口实现底层是 Redis。buildAuthorizationInfo()是纯构造方法不涉及 DB 操作确保缓存穿透风险最小化。第三clearCachedAuthorizationInfo()的主动清理时机。当管理员修改用户权限时必须主动清空该用户的权限缓存否则用户要等 30 分钟才生效。我们在PermissionService.updateUserPermissions()方法末尾加// 清理 Shiro 缓存 CacheObject, Object cache myRealm.getCache(); cache.remove(perms:: username);注意myRealm.getCache()获取的是 Realm 级缓存与SecurityManager的全局缓存隔离不会误删其他用户数据。3.4 Thymeleaf-Shiro 集成与模板实战Thymeleaf 的集成只需两步添加依赖已做和注册方言。在ShiroConfig.java中添加Bean public SpringTemplateEngine templateEngine() { SpringTemplateEngine templateEngine new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver()); // 注册 Shiro 方言 templateEngine.addDialect(new ShiroDialect()); return templateEngine; }ShiroDialect()是thymeleaf-extras-shiro提供的方言类它让shiro:xxx标签能被 Thymeleaf 解析。现在看一个真实可用的菜单片段menu.html!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org xmlns:shirohttp://www.pollix.at/thymeleaf/shiro head meta charsetUTF-8/ /head body div classsidebar-menu th:fragmentmenu ul !-- 首页 -- li a href/index th:classappend${#request.getRequestURI() /index ? active : } i classicon-home/i span th:text#{menu.home}首页/span /a /li !-- 用户管理 -- li shiro:hasRoleadmin a href# th:classappend${#request.getRequestURI().startsWith(/user) ? active : } i classicon-user/i span th:text#{menu.user}用户管理/span span classarrow/span /a ul classsub li shiro:hasPermissionuser:list a href/user/list th:text#{menu.user.list}用户列表/a /li li shiro:hasPermissionuser:add a href/user/add th:text#{menu.user.add}新增用户/a /li /ul /li !-- 订单管理所有角色可见但按钮受权限控制-- li a href# th:classappend${#request.getRequestURI().startsWith(/order) ? active : } i classicon-file-text/i span th:text#{menu.order}订单管理/span span classarrow/span /a ul classsub li a href/order/list th:text#{menu.order.list}订单列表/a /li li shiro:hasPermissionorder:export a href/order/export th:text#{menu.order.export}导出订单/a /li /ul /li /ul /div /body /html这个片段体现了三个最佳实践角色与权限分离shiro:hasRoleadmin控制整个菜单模块是否显示shiro:hasPermissionuser:add控制具体按钮是否可点击。这样普通用户看不到“用户管理”菜单而客服角色能看到菜单但点不了“新增用户”按钮。URL 活动状态高亮th:classappend${#request.getRequestURI().startsWith(/user) ? active : }用 Thymeleaf 表达式判断当前 URL 是否属于该模块动态添加active类实现菜单高亮。这比前端 JS 判断更可靠因为服务端知道确切的请求路径。国际化与权限解耦菜单文字用th:text#{menu.user.list}从messages.properties文件读取而权限码user:list存在数据库里。两者完全独立改文字不影响权限逻辑改权限码也不影响界面展示。注意shiro:hasRole和shiro:hasPermission标签必须在th:fragment内部使用不能放在th:replace或th:include的外部容器上。我曾在一个项目里把shiro:hasRole放在div th:includemenu :: menu的外层 div 上结果整个菜单都不显示——因为th:include是先执行模板包含再执行 shiro 标签而外层 div 没有 Subject 上下文。正确做法是权限控制逻辑全部下沉到menu.html片段内部。4. 实操全流程与关键参数配置4.1 数据库表结构设计与初始化权限系统的基础是四张核心表sys_user用户、sys_role角色、sys_permission权限、sys_user_role用户-角色关联、sys_role_permission角色-权限关联。我们采用“角色-权限”二级授权模型而非“用户-权限”直连便于后期扩展。-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- SHA-256 加盐后存储 salt VARCHAR(50) NOT NULL, -- Base64 编码的盐值 status TINYINT DEFAULT 1, -- 1:启用, 0:禁用 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(50) NOT NULL UNIQUE, -- 如 admin, user role_name VARCHAR(50) NOT NULL, description VARCHAR(200) ); -- 权限表细粒度控制 CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, permission_code VARCHAR(100) NOT NULL UNIQUE, -- 如 user:list, order:export permission_name VARCHAR(100) NOT NULL, url_pattern VARCHAR(200), -- 对应的 URL 模式用于 Filter 链 parent_id BIGINT DEFAULT 0, -- 用于构建菜单树 sort_order INT DEFAULT 0 ); -- 用户-角色关联表 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id), FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE, FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE ); -- 角色-权限关联表 CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id), FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE, FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE );初始化 SQL 示例插入超级管理员-- 插入 admin 角色 INSERT INTO sys_role (role_code, role_name, description) VALUES (admin, 系统管理员, 拥有所有权限); -- 插入用户权限粗粒度 INSERT INTO sys_permission (permission_code, permission_name, url_pattern) VALUES (user:*, 用户管理, /user/**), (order:*, 订单管理, /order/**), (menu:*, 菜单管理, /menu/**); -- 插入细粒度权限用于按钮控制 INSERT INTO sys_permission (permission_code, permission_name, url_pattern, parent_id) VALUES (user:list, 查看用户, , 1), (user:add, 新增用户, , 1), (order:export, 导出订单, , 2); -- 关联 admin 角色与所有权限 INSERT INTO sys_role_permission (role_id, permission_id) SELECT r.id, p.id FROM sys_role r, sys_permission p WHERE r.role_code admin;实操心得权限码设计要遵循模块:操作命名规范如user:list、order:export。避免用userList或user_list因为 Shiro 的isPermitted()方法内部会做字符串匹配:是标准分隔符。我们曾用user_list结果shiro:hasPermissionuser:list总是返回 false查了三小时才发现是命名不一致。4.2 登录认证全流程调试登录流程看似简单但每一步都可能出错。我们用一个真实调试案例说明场景用户输入正确用户名密码点击登录页面跳转到/login?error控制台无报错。排查步骤检查ShiroFilterFactoryBean的loginUrl配置确认bean.setLoginUrl(/login)与 Controller 的PostMapping(/login)路径一致。如果 Controller 是PostMapping(/doLogin)而loginUrl是/loginShiro 会重定向到/login但该路径没有 POST 处理器导致 405 错误。验证UsernamePasswordToken构造在 Controller 的 login 方法里打日志PostMapping(/login) public String login(String username, String password, Model model) { log.info(Login attempt: username{}, password length{}, username, password.length()); UsernamePasswordToken token new UsernamePasswordToken(username, password); log.info(Token created: {}, token); }如果password.length()是 0说明前端没传 password如果token的password字段是null说明UsernamePasswordToken构造时传了null而不是password.toCharArray()。跟踪MyRealm.doGetAuthenticationInfo()执行在该方法第一行加断点Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { log.info(Realm authentication started for: {}, ((UsernamePasswordToken) token).getUsername()); // ... rest of code }如果断点没触发说明请求没走到 Realm问题在 Filter 链配置如/login被authc拦截了如果触发了但返回null说明userService.findByUsername()没查到用户检查数据库连接和 SQL。检查密码加密匹配在doGetAuthenticationInfo()返回前打印log.info(DB password: {}, salt: {}, user.getPassword(), user.getSalt());然后用在线工具如 https://emn178.github.io/online-tools/sha256.html输入password salt看哈希值是否与user.getPassword()一致。不一致则说明注册时加密逻辑有 bug。最终我们发现是UsernamePasswordToken构造时用了new UsernamePasswordToken(username, password)而password是String类型Shiro 要求char[]。改为new UsernamePasswordToken(username, password.toCharArray())后问题解决。提示Shiro 的Subject.login(token)方法抛出的异常类型直接告诉你问题在哪UnknownAccountException用户名不存在IncorrectCredentialsException密码错误DisabledAccountException账户禁用LockedAccountException账户锁定需开启RetryLimitCredentialsMatcher在 Controller 里 catch 这些异常返回不同提示用户体验更好。4.3 权限控制效果验证与测试用例光写代码不够必须有一套验证方案。我们设计了 5 个核心测试用例覆盖典型场景测试用例操作步骤预期结果实际结果问题定位TC-01 登录成功输入 admin/123456跳转/index显示完整菜单✅-TC-02 权限不足用普通用户登录点击“新增用户”按钮页面 403不执行 Controller 方法✅permsFilter 生效TC-03 角色隐藏普通用户访问/user/listURL 可访问因user:list权限存在但菜单不显示“用户管理”模块✅shiro:hasRole生效TC-04 缓存验证登录后修改 DB 权

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑