Spring Boot集成Shiro:企业门户权限管理实战
发布时间:2026/9/16 22:17:42来源:尧图网络
简介一套基于Spring Boot与Apache Shiro的企业门户完整前后端系统源码面向Java开发人员及企业站开发者适合需要快速搭建带新闻发布、产品展示等功能的门户后台场景。项目在Bootdo框架基础上扩展了完整门户前端与后台管理前端包含首页、新闻列表与详情、轮播等页面后端支持新闻文章发布、产品图及基础信息维护技术栈覆盖Spring Boot、Shiro、MyBatis、Thymeleaf、Druid、Ehcache/Redis等常用组件。资源包共1521个文件以js、html、java、css、png等为主js负责页面交互html构建页面结构java承载后端逻辑css用于样式png/jpg提供产品与轮播图片素材xml/json/yml为配置信息整体压缩包约20MB同时附带jQuery、bootstrapTable、layer、jsTree等前端交互组件。已有668人学习下载目录结构清晰适合作为中小企业门户系统开发的学习范例或项目脚手架可直接参考其权限控制、内容发布与前端展示的完整实现对于想了解Spring Boot整合Shiro权限拦截、Druid连接池配置及Thymeleaf模板渲染的读者也是一份不错的参考实例。1. Shiro 在企业门户里到底管什么企业门户这类项目有个特点系统不大但页面多、角色杂、权限边界碎。Spring Boot 负责把业务接口和页面快速搭起来Shiro 则承接了登录认证、会话维持、URL 权限拦截这三件最容易被做乱的事。很多团队在门户上线半年后才意识到权限模块的代码量已经超过了业务代码而 Shiro 的意义恰恰在于用一套统一模型把这些分散的逻辑收敛住。这套模型的核心是 Subject、SecurityManager、Realm 三件套Subject 代表当前用户SecurityManager 是调度中枢Realm 是从数据源取用户和权限的桥。配置好三者登录、退出、记住我、URL 拦截、方法级权限都能在框架层面解决业务代码里不需要再到处写if (user.hasRole(admin))这类判断。整套源码要跑起来第一步不是看业务代码而是把这三个组件的职责和装配关系理顺。前面先把 Shiro 的运作机制讲透后面才能顺畅地替换数据源、加 Redis 会话、对接前后端分离的接口鉴权。2. Spring Boot 集成 Shiro 的四个核心配置2.1 依赖引入与版本选型Spring Boot 集成 Shiro 有两套常见路线一套是直接用shiro-spring-boot-starter另一套是引入shiro-core和shiro-web后手工配置。企业门户这种包含页面跳转和接口响应的项目通常选前者省去大量样板代码。依赖坐标如下dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-starter/artifactId version1.13.0/version /dependency如果是 Spring Boot 3.x 项目需要把 Shiro 换成 2.0.0 及以上版本因为 Spring Boot 3 基于 Jakarta EE旧版 Shiro 的 javax 命名空间无法兼容。需要特别留意的是Shiro 1.x 系列的最后一个稳定版本停留在 1.13.0之后的新功能都集中到 2.x所以新项目直接选 2.x 更合理。依赖引入后Spring Boot 会自动装配DefaultWebSecurityManager和ShiroFilterFactoryBean但这只是框架的默认实现Realm、过滤器链规则仍需显式声明。下面从 Realm 开始搭。2.2 Realm认证与授权的第一入口Realm 是 Shiro 连接业务数据源的桥梁。企业门户里用户信息通常存在数据库或 LDAP 中Realm 要做两件事登录时根据用户名查出用户记录并校验密码鉴权时根据用户 ID 查出角色和权限集合。继承AuthorizingRealm并实现两个方法是最典型的做法Component public class PortalRealm extends AuthorizingRealm { Autowired private UserService userService; Autowired private PermissionService permissionService; /** 授权查询当前用户的角色与权限标识集合 */ Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); SetString roles permissionService.findRolesByUsername(username); SetString perms permissionService.findPermissionsByUsername(username); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(perms); return info; } /** 认证根据用户名加载用户记录并返回身份信息 */ Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } if (user.getStatus() 0) { throw new LockedAccountException(账号已被锁定); } return new SimpleAuthenticationInfo(username, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } }doGetAuthenticationInfo中返回的SimpleAuthenticationInfo携带用户名、密文密码和盐值Shiro 拿到后会与用户提交的明文密码做哈希比对。doGetAuthorizationInfo在每次需要鉴权时触发返回值中包含角色集合和权限字符串集合供过滤器链和方法注解使用。这里的getName()是 Realm 的默认名称在配置 SecurityManager 后会自动匹配。密码加密推荐使用Sha256Hash或 BCrypt。Shiro 内置的CredentialsMatcher支持加盐哈希配合ByteSource.Util.bytes(user.getSalt())可以避免同密码同密文的问题。注意一点如果系统已经上线且密码是 MD5 明文存储不要直接改加密算法应在登录接口上做兼容处理否则所有存量用户都会无法登录。2.3 过滤器链与匿名/认证路径的坑Shiro 的访问控制依赖过滤器链。在 Spring Boot 中通过ShiroFilterFactoryBean定义 URL 与过滤器的映射关系Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); MapString, String filterChain new LinkedHashMap(); filterChain.put(/login, anon); filterChain.put(/css/**, anon); filterChain.put(/js/**, anon); filterChain.put(/images/**, anon); filterChain.put(/captcha, anon); filterChain.put(/logout, logout); filterChain.put(/api/login, anon); filterChain.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChain); factoryBean.setLoginUrl(/login); factoryBean.setUnauthorizedUrl(/403); return factoryBean; } }这里的LinkedHashMap是有序的Shiro 按声明顺序从上到下匹配一旦命中就停止匹配。常见错误是把/**放在最前面导致所有请求都被authc拦住登录页和静态资源全部 302。另一个容易翻车的地方是/logout过滤器如果登录页本身需要匿名访问而退出后重定向到登录页logout过滤器会清掉会话再跳转此时应确保/login允许匿名否则退出后会陷入重定向循环。如果门户系统拆成了前后端分离结构过滤器链要相应调整。/api/**路径不能直接返回 302 跳转登录页而是返回 JSON 状态码由前端统一处理 401 跳转。这个场景下需要自定义UserFilter覆盖onAccessDenied方法public class JsonAuthFilter extends UserFilter { Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { response.setContentType(application/json;charsetUTF-8); response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\未登录或会话已过期\}); return false; } }过滤器链配置完成后务必在浏览器开发者工具里观察状态码登录前的静态资源应是 200未登录访问受保护页面应是 302传统 MVC或 401前后端分离这两种行为分别要配到不同类型的过滤器链中。2.4 Session 管理与 Redis 共享Shiro 默认使用DefaultWebSessionManager会话数据存在 JVM 内存中。单机部署没问题但门户系统一旦部署多实例或者应用重启后用户被迫重新登录就需要把 Session 放到 Redis。常见做法是引入shiro-redis插件dependency groupIdorg.crazycake/groupId artifactIdshiro-redis/artifactId version3.3.1/version /dependencyBean public RedisSessionDAO sessionDAO(RedisManager redisManager) { RedisSessionDAO sessionDAO new RedisSessionDAO(); sessionDAO.setRedisManager(redisManager); sessionDAO.setKeyPrefix(portal:session:); sessionDAO.setExpire(1800); // 秒 return sessionDAO; }RedisSessionDAO的keyPrefix避免与其他业务 Redis key 冲突expire控制会话存活时长。配置完成后需要在SecurityManager中设置DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(portalRealm()); securityManager.setSessionManager(sessionManager());从两侧对应关系看本地内存会话适用于开发环境Redis 会话适用于多实例部署。建议在配置文件里用spring.redis.host驱动环境切换而不是改代码。Session 失效时间、Redis key 过期时间、Cookie 有效期这三者要统一设置否则会出现 Cookie 还在但 Session 已失效的边界情况。配置项本地 SessionRedis Session存储位置JVM 堆内存Redis持久化重启丢失重启保留多实例共享不支持支持适用环境开发、单机生产、集群3. 登录流程与密码处理的完整实现3.1 登录接口与认证过程拆解门户系统的登录接口是 Shiro 唯一允许匿名访问的业务接口。该接口一般接收用户名、密码和验证码验证码校验通过后调用Subject.login()RestController RequestMapping(/api) public class LoginController { Autowired private UserService userService; PostMapping(/login) public Result? login(RequestBody LoginRequest request, HttpServletRequest httpRequest) { String captchaCode (String) httpRequest.getSession() .getAttribute(Constants.CAPTCHA_KEY); if (request.getCaptcha() null || !captchaCode.equalsIgnoreCase(request.getCaptcha())) { return Result.error(500, 验证码错误); } Subject subject SecurityUtils.getSubject(); UsernamePasswordToken token new UsernamePasswordToken( request.getUsername(), request.getPassword()); token.setRememberMe(request.isRememberMe()); try { subject.login(token); } catch (UnknownAccountException e) { return Result.error(500, 用户名不存在); } catch (IncorrectCredentialsException e) { return Result.error(500, 密码错误); } catch (LockedAccountException e) { return Result.error(500, 账号被锁定); } catch (AuthenticationException e) { return Result.error(500, 认证失败); } return Result.success(null); } }代码里的验证码从 Session 中取出因此登录接口必须允许匿名且携带 Session。UsernamePasswordToken的setRememberMe(true)只在浏览器关闭后仍保持登录时使用它不影响当前会话的创建。如果门户对安全性要求高可再叠加登录失败次数限制Shiro 本身不直接提供需要自行在 Redis 中维护失败计数器超过阈值后锁定账号一段时间。3.2 密码加密与盐值策略企业门户的密码绝不能明文存储。Shiro 的HashedCredentialsMatcher支持 MD5、SHA-256 等算法推荐做法是每个用户独立盐值Bean public HashedCredentialsMatcher credentialsMatcher() { HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(SHA-256); matcher.setHashIterations(1024); matcher.setStoredCredentialsHexEncoded(true); return matcher; }注册用户时生成随机盐值并计算密文String salt RandomStringUtils.randomAlphanumeric(16); String hashedPassword new Sha256Hash(rawPassword, salt, 1024).toHex(); user.setPassword(hashedPassword); user.setSalt(salt);需要注意HashIterations必须与注册时一致否则登录时算出的哈希对不上。修改加密参数后所有旧密码都会失效所以生产环境改哈希算法前先确认数据库里存量密码的生成逻辑。3.3 记住我与会话保持的边界Shiro 的 RememberMe 机制通过 Cookie 保存用户身份Cookie 值经过序列化与加密。默认情况下SimpleCookie使用HttpOnly标志这对降低 XSS 风险有帮助。但 RememberMe 不等于 Session它只负责记住身份权限信息在下次访问时仍需重新获取。常见误区是勾选记住我后用户直接访问受保护资源仍被重定向到登录页。原因在于authc过滤器要求必须登录而 RememberMe 只是让user过滤器放行。企业门户建议路径区分敏感操作订单、用户管理用authc严格拦截普通浏览首页、公告用user过滤器允许 RememberMe 通过4. 权限模型与动态授权实现4.1 用户、角色、权限的表结构设计企业门户权限模型通常采用三张核心表加两张关联表的设计。SQL 结构如下CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(128) NOT NULL, salt VARCHAR(32) NOT NULL, status TINYINT DEFAULT 1, create_time DATETIME ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(50) NOT NULL, role_code VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, perm_name VARCHAR(50) NOT NULL, perm_code VARCHAR(100) NOT NULL UNIQUE, url_pattern VARCHAR(200) ); CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ); CREATE TABLE sys_role_permission ( role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL, PRIMARY KEY (role_id, permission_id) );sys_permission里perm_code采用user:add、order:delete这样的冒号分隔约定url_pattern用于 URL 粒度控制。设计阶段最容易犯的错是把菜单和权限混为一谈。菜单是前端导航项权限是后端接口访问规则两者应分开维护通过权限码关联否则新增一个菜单就要改一遍表结构。4.2 动态权限加载让授权不写死在代码里权限数据放在数据库里的意义就是可以动态调整。Realm 的doGetAuthorizationInfo每次鉴权时被调用每次都查库会带来性能压力因此需要缓存Bean public CacheManager cacheManager() { return new RedisCacheManager(); }权限变更后要主动清掉缓存否则用户改动角色后要等缓存过期才能生效。常见做法是在角色分配接口中调用如下逻辑Subject subject SecurityUtils.getSubject(); String username (String) subject.getPrincipal(); subject.getSession().removeAttribute(username :perms);如果是 Redis 缓存直接删除对应 key 更彻底。另一条路是在 Realm 里做授权信息的细粒度控制按用户维度在缓存中保留角色和权限集合而不是全量缓存整个 AuthorizationInfo。4.3 注解鉴权与 URL 鉴权的混合使用Shiro 提供RequiresRoles和RequiresPermissions注解用在 Controller 方法上。开启方式是在配置类加EnableShiroAnnotations或在 SecurityManager 中设置DefaultAdvisorAutoProxyCreatorBean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisor new DefaultAdvisorAutoProxyCreator(); advisor.setProxyTargetClass(true); return advisor; }注解方式适合细粒度控制URL 方式适合粗粒度拦截。两者的定位差异如下控制粒度实现方式适用场景URL 级别过滤器链模块级入口控制维护成本低方法级别注解单个操作级控制灵活但注解分散生产环境中推荐搭配URL 过滤器做第一道闸门注解做第二道精确控制。但要注意URL 过滤器只能拦截配置的路径若接口路径是/api/user/1这种 RESTful 风格/**能匹配但RequiresPermissions(user:view)仍需显式标注在对应方法上。4.4 页面按钮级权限的落地方式后端控制接口还不够门户系统通常还要控制前端页面的按钮显示。Shiro 标签库shiro:hasPermission可以在 JSP/Thymeleaf 中直接使用但前后端分离后这种方式就不再适用。替代方案是登录成功后返回该用户的所有权限码集合由前端存储并判断{ code: 200, data: { username: zhangsan, roles: [admin, operator], permissions: [user:add, user:edit, order:view] } }前端在按钮渲染时做权限判断动作按钮的显示与否由后端下发的权限集合决定。后端在敏感接口上仍然用注解拦截双重保障。需要注意前端权限控制只影响体验真正的安全边界必须由后端守住。5. 源码部署与排错的三个关键动作5.1 先用最少配置跑通登录链路拿到完整前后端源码后不要急着配数据库和 Redis。先做最小化启动注释掉 Redis 相关配置改用本地 Session把数据库连接切到本地 MySQL然后启动后端、打开登录页验证从输入账号到跳转首页的完整链路。这一步能排除绝大多数环境问题。常见报错及处理方向抛错信息排查方向UnknownAccountException用户表无记录或 Realm 中findByUsername逻辑有误IncorrectCredentialsException密码明文与密文不匹配检查盐值和加密轮数是否一致Circular view pathShiro 拦截了静态资源转发确认/login与静态资源的 anon 配置ClassNotFoundException: javax.*Spring Boot 3 下用了 Shiro 1.x升级 Shiro 依赖请求重定向过多次过滤器链中/login被authc拦截或退出后跳转地址被拦截5.2 验证权限配置是否生效系统启动后至少验证三个动作未登录访问受保护路径被拦截使用低权限账号访问高权限接口返回无权限修改角色后权限实时或按预期延迟生效。第三个动作需要确认缓存清理逻辑是否触发如果修改了数据库权限但依旧能访问优先查 Redis 中是否有残留的 Session 和权限缓存。5.3 用日志定位 Shiro 内部流程调试 Shiro 问题时将 logback 中org.apache.shiro的日志级别调到 DEBUG过滤器链的匹配过程、Realm 的调用顺序、权限校验的命中情况都会输出到日志中。看到No subject found - creating subject说明访问时未关联 Session检查 Cookie 是否正常携带看到Authorization successful说明授权逻辑通过看到No permission则检查权限码是否匹配、角色是否绑定。日志配合断点大部分 Shiro 问题都能在半小时内定位到具体环节。本文还有配套的精品资源点击获取
网站建设高端定制企业官网