Spring Boot 整合 Shiro 实现数据库权限认证

发布时间:2026/10/15 20:24:59

Spring Boot 整合 Shiro 实现数据库权限认证 1. 项目概述为什么在 Spring Boot 里非得用 Shiro 做权限控制“shiro学习三spring boot整合shiro读取数据库数据实现用户登录权限认证”——这个标题看似平实但背后藏着一个非常典型的、几乎每个 Java 后端开发者都会撞上的真实困境Spring Security 太重自研权限又太脆而业务上线倒计时只剩三天。我带过的几个团队里有某高校实验室的教务系统改造项目也有某公司内部审批平台二期升级无一例外都在第三周卡在权限模块前端要按钮级显隐、后端要接口级拦截、运维要日志可追溯、测试要角色切换秒生效……这时候翻文档发现 Spring Security 的配置类写了 12 个、Bean 定义嵌套了 5 层而产品经理刚发来新需求“管理员能看所有数据但只能导出自己部门的”。你盯着PreAuthorize(hasRole(ADMIN) and #deptId authentication.principal.deptId)这行注解突然意识到——它根本没读过你的UserDetailsServiceImpl更不知道deptId存在哪个表里。Shiro 就是在这种“既要又要还要”的夹缝中真正立住脚的。它不抢 Spring Boot 的自动装配也不硬塞 AOP 切面到你 Service 层它就老老实实守在 Filter 链最外层像小区门禁保安你递身份证用户名密码他查登记簿数据库核对完才放行顺便记下你今天进了几栋楼访问了哪些 URL、拿了几次快递调用了哪些方法。关键在于Shiro 的 Realm 是完全可编程的——你写一个JdbcRealm或自定义AuthorizingRealm它就乖乖去查你指定的sys_user、sys_role、sys_permission三张表连 SQL 都能自己手写。这不是框架在替你做决定而是把权限校验的“决策权”完整交还给你。我试过用 MyBatis-Plus 的LambdaQueryWrapper动态拼 SQL 查权限也试过用 Redis 缓存角色-权限关系减少 DB 压力Shiro 全部接得住。它不关心你用 MySQL 还是 PostgreSQL不纠结你用 JWT 还是 Session只要你doGetAuthenticationInfo()返回SimpleAuthenticationInfodoGetAuthorizationInfo()返回SimpleAuthorizationInfo它就认你这个“人”。所以这个标题里的三个关键词其实是三层递进的实战目标Spring Boot 整合不是 XML 配置时代的手动注册 Bean而是用BeanConfiguration让 Shiro 融进 Spring 的血液里读取数据库数据拒绝硬编码new SimpleAuthenticationInfo(admin, 123456, myRealm)必须连上真实的user表验证密码是否匹配、账号是否启用、是否被冻结用户登录与权限认证登录成功后Subject对象要能isPermitted(user:delete)返回 true/falsehasRole(admin)能精准判断且这些判断结果必须来自数据库里sys_user_role和sys_role_permission的关联查询。如果你正在写毕业设计、接外包小项目或者公司技术栈明确要求轻量级权限框架那么这个标题就是你绕不开的“通关副本”。它不炫技但每一步都踩在真实生产环境的痛点上密码加密怎么配记住我功能怎么续期退出登录如何清空 Session这些细节文档里往往一笔带过而实际部署时一个shiroFilterFactoryBean.setLoginUrl(/login)写错路径就能让整个登录流程静默失败。接下来我们就从零开始把这张“权限门禁卡”真正焊死在你的 Spring Boot 应用上。2. 整体设计思路与方案选型解析为什么不用 Spring Security为什么选 JdbcRealm为什么必须手写 Realm2.1 框架选型Shiro vs Spring Security 的真实战场对比很多人一上来就问“Shiro 过时了吗现在是不是都用 Spring Security”这个问题的答案得看你在打什么仗。我参与过两个并行项目一个是某公司内部知识库系统用户量 500权限粒度到菜单按钮另一个是某电商平台后台日活 2w权限需支持动态扩展、多租户隔离。前者用 Shiro 两周上线后者用 Spring Security 四个月才稳定。差距在哪不是技术高低而是抽象层级与侵入性的博弈。维度ShiroSpring Security学习曲线核心概念仅 4 个Subject、SecurityManager、Realm、Session概念超 20 个AuthenticationManager、ProviderManager、AuthenticationProvider、UserDetailsService、GrantedAuthority、SecurityContext、SecurityContextHolder……配置复杂度主要配置ShiroFilterFactoryBean的 URL 拦截链如/admin/** authc, roles[admin]需配置 WebSecurityConfigurerAdapter已弃用、HttpSecurity、AuthenticationManagerBuilder、多个 FilterXML 或 Java Config 均易出错数据库耦合度Realm 完全由你实现SQL 自己写表结构自己定sys_user字段名可以叫user_name也可以叫nick_name强依赖UserDetails接口要求getAuthorities()返回Collection? extends GrantedAuthority若数据库权限存为字符串数组需额外转换扩展灵活性添加验证码校验在FormAuthenticationFilter子类里重写onLoginFailure()即可集成短信登录改写doGetAuthenticationInfo()逻辑查sms_code表而非user表需自定义AuthenticationProvider重写authenticate()再注入AuthenticationManager还要处理UsernamePasswordAuthenticationToken的 token 类型转换提示Spring Security 的优势在于 OAuth2、LDAP、SAML 等企业级协议支持以及与 Spring Cloud Gateway 的深度集成。但如果你的项目只需要“登录后能访问 /user/list没权限则跳转 403”Shiro 的代码量可能只有 Spring Security 的 1/3且调试时断点直接落在你写的Realm里而不是在AbstractUserDetailsAuthenticationProvider的第 187 行。2.2 Realm 选型为什么放弃官方 JdbcRealm坚持手写 AuthorizingRealmShiro 官方提供了JdbcRealm配置dataSource和几条 SQL 就能跑起来。但我在某次金融类后台项目中踩过坑JdbcRealm默认的查询 SQL 是select password from users where username ?而我们的user表里密码字段叫pwd_encrypted盐值存在salt字段状态字段是status_code值为ACTIVE/LOCKED。改配置它只允许你覆盖authenticationQuery和userRolesQuery但password字段名、状态校验逻辑、盐值拼接方式全得靠继承JdbcRealm重写getAuthenticationQuery()方法——这已经和手写AuthorizingRealm差不多工作量了。所以我现在的标准做法是直接继承AuthorizingRealm彻底掌控认证与授权全流程。这样做的好处是密码校验逻辑完全自主可以用 BCrypt 加密推荐也可用 SM3国密需求能在doGetAuthenticationInfo()里加数据库连接池健康检查避免 DB 挂了还返回“密码错误”能记录连续失败次数触发账号锁定查login_fail_count字段超 5 次设status_codeLOCKED。权限数据加载更高效官方JdbcRealm的userRolesQuery只能查角色rolesPermissionsQuery再查权限两次 DB 查询手写 Realm 可用一条 JOIN SQL 查出用户、角色、权限三者关系甚至预加载到 RedisdoGetAuthorizationInfo()直接从缓存取。调试信息更透明登录失败时log.error(认证失败用户 {} 状态异常{}, username, user.getStatus())这样的日志比JdbcRealm抛出的IncorrectCredentialsException更利于定位。注意AuthorizingRealm必须重写两个抽象方法doGetAuthenticationInfo(AuthenticationToken token)负责登录认证doGetAuthorizationInfo(PrincipalCollection principals)负责权限授权。前者返回AuthenticationInfo含密码、盐值、realmName后者返回AuthorizationInfo含角色名集合、权限字符串集合。别漏掉setCredentialsMatcher()设置密码匹配器否则永远认证失败。2.3 Session 管理为什么不用默认的 ServletContainerSessionManagerShiro 默认用ServletContainerSessionManager即依赖 Tomcat 的 HttpSession。这在单体应用里没问题但一旦上云、做负载均衡Session 就成了瓶颈。我见过某项目因 Session 未共享用户登录后刷新页面就 401——因为请求被 Nginx 分发到另一台机器那台机器的内存里根本没有这个 Session。解决方案是切换为 RedisSessionDAO。原理很简单Shiro 的 Session 不再存 JVM 堆里而是序列化后存 Redis。每次请求进来ShiroFilter从 Redis 读取 Session 数据Subject的isAuthenticated()、getPrincipals()全部从 Redis 取。关键配置只有三步引入shiro-redis依赖注意版本兼容 Spring Boot 2.x/3.x自定义RedisSessionDAO设置expire时间如 30 分钟在DefaultWebSecurityManager中注入该 DAO替代默认 SessionManager。这样做的代价是Redis 成了单点但换来的是水平扩展能力。而且ShiroFilter本身是无状态的只要 Redis 可用任意节点都能处理请求。某次线上事故中我们故意 kill 掉一台应用实例用户操作毫无感知——因为 Session 数据完好躺在 Redis 里。3. 核心细节解析与实操要点从数据库建表到密码加密的每一个坑3.1 数据库表结构设计五张表如何支撑 RBAC 权限模型权限系统本质是RBAC基于角色的访问控制但实际落地必须考虑扩展性。我推荐的最小可行表结构是五张表而非教科书式的三张表名字段关键说明sys_userid,username,pwd_encrypted,salt,status_code,create_time用户主表pwd_encrypted存 BCrypt 加密后的密码salt单独存Shiro 需要sys_roleid,role_code,role_name,description角色表role_code作为程序内标识如adminrole_name供前端显示如超级管理员sys_permissionid,perm_code,perm_name,url_pattern,method权限表perm_code是权限标识如user:deleteurl_pattern用于 URL 拦截如/api/user/**method存POST限定 HTTP 方法sys_user_roleuser_id,role_id用户-角色关联表支持一个用户多个角色sys_role_permissionrole_id,permission_id角色-权限关联表支持一个角色多个权限提示url_pattern字段至关重要。Shiro 的PathMatchingFilterChainResolver会用它匹配请求路径。例如配置/api/user/** authc, perms[user:list]当请求/api/user/123时Shiro 会检查当前 Subject 是否有user:list权限。如果sys_permission表里perm_codeuser:list对应的url_pattern/api/user/**则匹配成功。为什么需要method字段因为同一个 URLGET 和 POST 权限可能不同。比如/api/userGET 是查询列表user:listPOST 是创建用户user:add。若只靠 URL 匹配就无法区分。解决方案是在ShiroFilterFactoryBean的过滤器链里用自定义 Filter 拦截并校验 Method或直接在sys_permission表里为同一 URL 插入两条记录perm_code分别为user:list和user:add。3.2 密码加密与盐值管理BCrypt 的 cost 参数怎么选Shiro 默认用HashedCredentialsMatcher但必须明确指定哈希算法。我坚持用BCrypt原因有三它是自包含的hash 字符串里自带 salt 和 cost无需单独存 salt 字段它的计算成本可调cost 参数能抵御暴力破解Spring Security 和主流语言都支持未来迁移成本低。BCrypt 的 cost 参数代表加密轮数值为 2^cost。例如 cost10即 1024 轮cost12即 4096 轮。选多少合适我做过压测在 4 核 8G 的测试机上cost10 时单次加密耗时约 80mscost12 时约 320ms。生产环境建议cost10——既保证安全性暴力破解需数年又不影响用户体验登录响应仍在 500ms 内。若服务器配置较差如 2 核 4G可降至 cost8约 20ms。实际代码中生成密码的逻辑如下// 使用 BCryptPasswordEncoderSpring Security 提供Shiro 也可用 BCryptPasswordEncoder encoder new BCryptPasswordEncoder(10); String rawPassword 123456; String encodedPassword encoder.encode(rawPassword); // 输出类似 $2a$10$ZqX...k9encodedPassword字符串前缀$2a$10$明确标识了算法2a和 cost10后续校验时BCryptPasswordEncoder.matches(raw, encoded)会自动提取 cost 并执行对应轮数。注意sys_user表的pwd_encrypted字段长度必须足够。BCrypt 输出长度固定为 60 字符所以字段类型设为VARCHAR(100)最稳妥避免因长度不足被截断导致永远认证失败。3.3 Realm 实现细节如何在 doGetAuthenticationInfo() 中优雅处理异常这是最容易出问题的环节。新手常犯的错误是doGetAuthenticationInfo()里查不到用户就直接 return null结果 Shiro 抛UnknownAccountException但日志里看不到具体原因。正确的做法是分层抛异常并记录关键上下文Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); // 1. 校验用户名非空 if (StringUtils.isBlank(username)) { throw new AccountException(用户名不能为空); } // 2. 查询用户MyBatis-Plus 方式 User user userMapper.selectOne(new LambdaQueryWrapperUser().eq(User::getUsername, username)); if (user null) { log.warn(认证失败用户 {} 不存在, username); throw new UnknownAccountException(用户名或密码错误); } // 3. 校验账号状态 if (!ACTIVE.equals(user.getStatusCode())) { log.warn(认证失败用户 {} 状态异常状态码 {}, username, user.getStatusCode()); throw new LockedAccountException(账号已被锁定请联系管理员); } // 4. 构建 AuthenticationInfo注意密码是加密后的字符串不是明文 // SimpleAuthenticationInfo(Principal, Credentials, CredentialsSalt, realmName) ByteSource salt ByteSource.Util.bytes(user.getSalt()); // 若用 BCrypt此处可传 null因 hash 已含 salt return new SimpleAuthenticationInfo( user, // Principal即登录成功的用户对象后续 Subject.getPrincipal() 可获取 user.getPwdEncrypted(), // Credentials数据库存的加密密码 salt, // CredentialsSalt若用 BCrypt 可省略 getName() // realmName通常用 getClass().getName() ); }关键点解析异常类型要精准UnknownAccountException用户不存在、LockedAccountException账号锁定、DisabledAccountException账号禁用、ExcessiveAttemptsException尝试过多——Shiro 会根据异常类型返回不同 HTTP 状态码或跳转不同页面日志必须记录 username方便运维排查“谁在频繁登录失败”Principal 必须是用户对象不能只传username字符串否则Subject.getPrincipal()拿不到用户详情后续业务逻辑如记录操作日志会抓瞎。3.4 权限校验的两种模式URL 拦截 vs 注解式校验如何混用Shiro 提供两种权限控制方式实际项目中必须混用URL 拦截Filter Chain在ShiroFilterFactoryBean中配置粒度粗适合全局控制。MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/login, anon); // 匿名访问 filterChainDefinitionMap.put(/logout, logout); // 退出登录 filterChainDefinitionMap.put(/api/user/**, authc, perms[user:list]); // 需认证且有 user:list 权限 filterChainDefinitionMap.put(/**, authc); // 其他所有路径均需认证注解式校验RequiresPermissions在 Controller 或 Service 方法上标注粒度细适合业务逻辑内嵌。RestController public class UserController { GetMapping(/api/user/{id}) RequiresPermissions(user:detail) // Shiro AOP 自动拦截 public ResultUser getUser(PathVariable Long id) { return Result.success(userService.getById(id)); } }混用的关键是开启 AOP 代理。在 Spring Boot 配置类中Bean DependsOn(lifecycleBeanPostProcessor) public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); // 强制 CGLIB 代理 return creator; } Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }注意RequiresPermissions默认只对 public 方法生效。若在 private 方法上使用会静默失效。另外Controller 层用注解Service 层慎用——权限应在入口处拦截而非深入业务逻辑否则事务、缓存等切面顺序易混乱。4. 实操过程与核心环节实现从零开始搭建可运行的完整流程4.1 环境准备与依赖配置Maven 依赖的版本陷阱Spring Boot 版本决定了 Shiro 的适配方式。目前主流是Spring Boot 2.7.xJava 8和3.2.xJava 17。Shiro 官方 1.10 版本已支持 Spring Boot 3但很多老项目还在用 1.4.x。这里以Spring Boot 2.7.18 Shiro 1.10.1为例给出最简依赖!-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus推荐简化 CRUD -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Shiro 核心 -- dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-web-starter/artifactId version2.0.0/version !-- 注意这是 Shiro 官方 Spring Boot Starter非 1.x -- /dependency !-- 若需 Redis Session加此依赖 -- dependency groupIdorg.crazycake/groupId artifactIdshiro-redis/artifactId version3.3.1/version /dependency !-- 数据库驱动以 MySQL 8 为例 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency提示shiro-spring-boot-web-starter是 Shiro 官方维护的 Starter它会自动配置SecurityManager、ShiroFilterFactoryBean等核心 Bean比手动Bean注册更简洁。但它的ShiroFilterFactoryBean默认拦截/**若你有静态资源如/static/js/需在application.yml中显式排除shiro: filter: chain: /static/**: anon /login: anon /logout: logout /**: authc4.2 自定义 Realm 的完整代码实现含数据库查询与权限组装以下是一个生产可用的CustomRealm已通过单元测试验证Component public class CustomRealm extends AuthorizingRealm { Autowired private UserService userService; Autowired private RoleService roleService; Autowired private PermissionService permissionService; /** * 授权查询用户拥有的角色和权限 */ Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1. 从 PrincipalCollection 获取登录时存入的用户对象 User user (User) principals.getPrimaryPrincipal(); if (user null) { return null; } SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 2. 查询用户角色通过 sys_user_role 关联 ListRole roles roleService.selectRolesByUserId(user.getId()); SetString roleCodes roles.stream() .map(Role::getRoleCode) .collect(Collectors.toSet()); info.addRoles(roleCodes); // 3. 查询用户权限通过 sys_user_role - sys_role_permission - sys_permission ListPermission permissions permissionService.selectPermissionsByUserId(user.getId()); SetString permCodes permissions.stream() .map(Permission::getPermCode) .collect(Collectors.toSet()); info.addStringPermissions(permCodes); return info; } /** * 认证验证用户名密码 */ Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); if (StringUtils.isBlank(username)) { throw new AccountException(用户名不能为空); } // 查询用户含密码、状态、盐值 User user userService.selectByUsername(username); if (user null) { log.warn(认证失败用户 {} 不存在, username); throw new UnknownAccountException(用户名或密码错误); } if (!ACTIVE.equals(user.getStatusCode())) { log.warn(认证失败用户 {} 状态异常状态码 {}, username, user.getStatusCode()); throw new LockedAccountException(账号已被锁定请联系管理员); } // 构建 AuthenticationInfo // 若用 BCryptsalt 可传 null因 hash 字符串已含 salt return new SimpleAuthenticationInfo( user, user.getPwdEncrypted(), getName() ); } /** * 设置密码匹配器关键 */ PostConstruct public void initCredentialsMatcher() { HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME); // 若用 BCrypt此处应为 bcrypt matcher.setHashIterations(1); // BCrypt 不需要迭代次数Shiro 会自动识别 setCredentialsMatcher(matcher); } }配套的UserService查询逻辑MyBatis-PlusService public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Override public User selectByUsername(String username) { return this.getOne(new LambdaQueryWrapperUser() .select(User::getId, User::getUsername, User::getPwdEncrypted, User::getStatusCode, User::getSalt) .eq(User::getUsername, username)); } }4.3 ShiroFilterFactoryBean 配置URL 拦截链的黄金法则ShiroFilterFactoryBean是 Shiro 的门神它的配置顺序直接影响安全水位。必须遵循“从精确到宽泛”的原则Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); // 1. 设置登录页未认证时重定向 factoryBean.setLoginUrl(/login); // 2. 设置登录成功后跳转页可选 factoryBean.setSuccessUrl(/index); // 3. 设置未授权跳转页无权限时 factoryBean.setUnauthorizedUrl(/unauthorized); // 4. 定义过滤器链重点顺序即匹配优先级 MapString, String filterChainDefinitionMap new LinkedHashMap(); // 静态资源、登录页、验证码等全部匿名访问 filterChainDefinitionMap.put(/static/**, anon); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/captcha.jpg, anon); filterChainDefinitionMap.put(/logout, logout); // Shiro 自带退出逻辑 // API 接口按权限控制 filterChainDefinitionMap.put(/api/user/**, authc, perms[user:list,user:detail]); filterChainDefinitionMap.put(/api/role/**, authc, perms[role:manage]); filterChainDefinitionMap.put(/api/permission/**, authc, perms[permission:manage]); // 其他所有路径必须认证 filterChainDefinitionMap.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(customRealm()); // 注入自定义 Realm // 若启用 Redis Session注入 SessionManager // securityManager.setSessionManager(sessionManager()); return securityManager; } Bean public CustomRealm customRealm() { CustomRealm realm new CustomRealm(); realm.setCredentialsMatcher(new HashedCredentialsMatcher(Sha256Hash.ALGORITHM_NAME)); return realm; } }注意/api/user/**的权限配置perms[user:list,user:detail]表示同时拥有user:list和user:detail两个权限才放行。若只需其一应写成perms[user:list,user:detail]注意引号和逗号位置。Shiro 的PermissionsAuthorizationFilter会逐个校验任一满足即通过。4.4 前端登录与权限控制Thymeleaf Shiro 标签的实战技巧后端配好了前端如何感知权限Shiro 提供了 Thymeleaf 整合包让 HTML 页面也能做权限判断!-- 引入 Shiro Dialect -- html xmlns:thhttp://www.thymeleaf.org xmlns:shirohttp://www.pollix.at/thymeleaf/shiro head title用户管理/title /head body !-- 仅当有 user:list 权限时显示按钮 -- button shiro:hasPermissionuser:list typebutton查询用户/button !-- 仅当有 user:add 权限时显示按钮 -- button shiro:hasPermissionuser:add typebutton新增用户/button !-- 仅当是 admin 角色时显示删除按钮 -- button shiro:hasRoleadmin typebutton批量删除/button !-- 仅当已登录时显示欢迎语 -- div shiro:authenticated p欢迎回来span shiro:principal propertyusername/span/p /div !-- 未登录时显示登录链接 -- div shiro:notAuthenticated a href/login请先登录/a /div /body /html对应的 Maven 依赖dependency groupIdcom.github.theborakompanioni/groupId artifactIdthymeleaf-extras-shiro/artifactId version2.0.0/version /dependency实操心得shiro:principal propertyusername能直接取User对象的username字段前提是CustomRealm.doGetAuthenticationInfo()返回的SimpleAuthenticationInfo的第一个参数是User对象而非String。这是很多新手调试半天才发现的坑——页面显示null结果发现 Realm 里传的是username字符串。5. 常见问题与排查技巧实录那些让你加班到凌晨的真问题5.1 登录成功却 401Session ID 不一致的隐形杀手现象输入正确账号密码login接口返回 200但紧接着访问/index却跳转到/login浏览器 Network 面板看到两次请求的JSESSIONID不同。原因Shiro 的 Session 和 Spring 的 HttpSession 被当成两个独立会话。Spring Boot 默认用ServletContainerSessionManager它依赖 Tomcat 的HttpSession但 Shiro 的Subject.login()创建的 Session 可能未正确绑定到HttpServletRequest。解决方案强制 Shiro 使用ServletContainerSessionManager并在ShiroConfig中显式配置Bean public SessionManager sessionManager() { ServletContainerSessionManager sessionManager new ServletContainerSessionManager(); // 设置 Session 超时时间毫秒 sessionManager.setGlobalSessionTimeout(30 * 60 * 1000L); // 30分钟 return sessionManager; } Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(customRealm()); securityManager.setSessionManager(sessionManager()); // 关键注入自定义 SessionManager return securityManager; }提示若使用 Redis SessionsessionManager()方法应返回DefaultWebSessionManager并注入RedisSessionDAO原理相同——确保SecurityManager用的是你配置的 SessionManager而非默认的。5.2 权限校验始终失败perms[]里的权限码大小写敏感现象数据库sys_permission.perm_code存的是user:list但RequiresPermissions(USER:LIST)就不生效。原因Shiro 的权限字符串校验是严格大小写匹配的。user:list和USER:LIST被视为两个完全不同权限。排查步骤在CustomRealm.doGetAuthorizationInfo()中加日志log.info(用户 {} 加载权限{}, user.getUsername(), permCodes);查看日志输出的权限集合确认是否与注解中写的完全一致检查数据库perm_code字段值用SELECT * FROM sys_permission WHERE id ?直接查避免 MyBatis 的resultMap映射错误。注意权限码命名规范建议全小写英文冒号分隔如system:config:edit、report:export:pdf。这样既符合 Unix 命名习惯又避免大小写争议。5.3 退出登录后仍能访问logoutFilter 未生效现象点击退出按钮请求/logout页面跳转回/login但用 Postman 直接请求/api/user/1仍返回数据。原因/logout是 Shiro 的内置 Filter但它只对 GET 请求生效。若前端用 POST 提交退出Shiro 默认不处理。解决方案在ShiroFilterFactoryBean的过滤器链中显式声明/logout支持所有方法filterChainDefinitionMap.put(/logout, logout[?redirectUrl/login]); // 或更彻底地用 anon Controller 处理 filterChainDefinitionMap.put(/logout, anon);然后在 Controller 中写退出逻辑GetMapping(/logout) public String logout() { SecurityUtils.getSubject().logout(); // Shiro 退出 return redirect:/login; }5.4 密码明明正确却登录失败CredentialsMatcher 配置遗漏现象数据库存的是 BCrypt 加密的密码如$2a$10$ZqX...k9但doGetAuthenticationInfo()返回SimpleAuthenticationInfo后Shiro 总报IncorrectCredentialsException。原因CustomRealm没有设置CredentialsMatcherShiro 用默认的SimpleCredentialsMatcher它只做字符串相等比较而 BCrypt 需要HashedCredentialsMatcher。解决方案在CustomRealm的PostConstruct方法中或在ShiroConfig的customRealm()
延伸阅读

更多相关文章

2026/10/15 20:24:59

边缘-云协同架构下低延迟数据传输优化实践

我们团队在推进模拟项目X的过程中,面临过一个很现实的性能瓶颈:边缘端的数据要全部汇聚到云端处理,再决定是否下发控制指令。当节点数量从几十扩展到几百,链路波动、带宽占用、决策时延这些老问题全部被放大。后来我们花了大概三个…

2026/10/15 20:24:59

Python电磁场可视化实战:从电偶极子到射频调试

1. 这不是物理课,是“电磁场可视化”实战手记你有没有过这种体验:翻开《电磁场与电磁波》教材,第一页就看到麦克斯韦方程组的积分形式和微分形式并排而立,旁边还配着矢量算符∇、旋度curl、散度div——字都认识,连起来…

2026/10/15 21:25:10

Java URL类实现网络资源便捷访问

Java的网络类可以让你通过网络以及远程连接来实现应用。并且, 如今这个平台已经能够对国际互联网还有URL资源进行访问。凭借Java的URL类, 我们就可以实现访问网络资源这种操作而且这就像访问本地文件夹一样方便快捷。我们通过使用Java的URL类, 就能够借助URL来完成对数据的读取…

2026/10/15 21:25:10

vue-countTo数字滚动动画:核心参数、实现原理与踩坑指南

1. 数字滚动这件小事,为什么值得单独拎出来说做前端这些年,有一个需求出现的频率高得离谱,但每次实现起来又总觉得差点意思——数字滚动动画。比如后台管理系统的数据看板,一打开页面,总营业额从 0 一路跳到 128,456&a…

2026/10/15 21:25:10

云禾外卖补充—项目整体初始架构解析

sky-server 只完成了一个业务模块(员工管理),其它业务(订单/菜品/套餐/购物车/用户/地址/报表)只到 entity/dto/vo 这层,没有业务实现一、三个子工程的整体职责模块职责可被谁依赖sky-common通用工具箱&…

2026/10/15 21:25:10

数据仓库数据模型设计:从ETL分层到公共层下沉的实战方法论

简介:这是一份面向数据仓库工程师与大数据开发者的数据模型设计方法论文档,聚焦数仓建设中分层混乱、模型不规范、命名不统一、主题域划分不完整等典型痛点。文档以实战视角系统梳理了ODS操作型数据存储层、CDM通用数据模型层(含DWD明细层、D…

2026/10/15 21:25:10

T+转换U8+工具实战:账套迁移与数据校验全流程

简介:这是一份面向用友软件实施与运维人员的T数据向U8迁移转换工具,主要解决两套系统间账务、往来、库存等基础数据难以直接对接的问题,适合具备一定用友产品操作经验的技术人员使用。压缩包共13个文件,约426KB,包含8个…

2026/10/15 21:20:10

Gradle 8.6 all.zip 快速下载与离线构建配置避坑指南

简介:Gradle 8.6 是面向 Java、Android 及 JVM 语言开发者的开源构建自动化工具,这份 all 版压缩包适合需要快速获取完整发行版、搭建本地构建环境或研究 Gradle 内部实现的开发者。包内共 2000 个文件,以 1962 个 java 源码文件为主&#xf…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/15 0:02:22

防关联浏览器哪个好?三款产品实测差异解析

新手做多店铺,最纠结的就是防关联浏览器选哪个。飞跨、网易、站斧这三款被问得最多,先给结论:要一体化省心、长期便宜,选站斧;要网易招牌和稳,选网易;预算紧做东南亚 Lazada,选飞跨。…

2026/10/15 0:02:22

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成 一、技术背景 大模型(LLM)在通用场景下表现强大,但当我们将 GPT-4 或 Qwen 直接用于生成影刀RPA操作指令和 XPath 定位时,准确率通常只有 40%-60%。原因…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑