SpringBoot+Layui+Shiro+Ehcache学生管理系统实践
2026/9/16 10:47:32 网站建设 项目流程

简介:这是一套基于Spring Boot和Layui搭建的学生管理系统,同时整合Shiro安全框架与Ehcache缓存框架,主要面向全栈初学者、课程设计及毕业设计人群。项目源码已通过本地编译测试,配套环境配置文档齐全,整体难度适中,可作为学生信息管理场景的完整入门范例。压缩包共310个文件,大小约30.39MB,内含Java源码、Layui页面、数据库SQL脚本以及XML、YML等配置文件,并配有大量图片与样式资源,便于查看界面效果和前后端联调方式,整体目录结构清晰。系统覆盖登录认证、权限分配、缓存处理等关键功能,能够帮助学习者理解安全框架与缓存机制在真实项目中的整合用法,具有较高的参考价值。目前已有94人学习,适合需要快速搭建毕设项目或提升全栈开发能力的学习者参考与二次开发。

1. 这套学生管理系统,为什么值得从零搭一遍

一个看起来“遍地都是”的学生管理系统,其实是把 Java Web 开发里最常踩的坑集中到了一起:表结构设计、权限控制、缓存穿透、前端渲染、会话管理。SpringBoot 负责把后端装配成本地可运行的服务,Layui 承担后台管理页面的表格、表单和弹窗交互,Shiro 解决“谁能访问哪个接口”的认证与授权问题,Ehcache 则用来缓存热点数据——比如学生列表、字典项、权限数据,避免每次请求都打数据库。

这套组合的技术选型非常典型:SpringBoot 2.x 时代,Shiro 和 Ehcache 的整合方式已经非常成熟,不依赖微服务体系,也没有引入消息队列和分布式缓存,单机部署即可支撑一个院系级别的日常使用。对初中级开发者来说,把这条链路完整跑通,比单纯背 Spring Security 的过滤器链要直观得多。对五年以上经验的人来说,这套系统的价值在于排查一个问题:Shiro 的 Session 和 Ehcache 的缓存边界在哪里,写不好就会遇到“权限改了不生效”和“列表缓存和数据不一致”的经典故障。

本文按实际开发顺序推进:先搭建项目骨架和数据库层,再做 Layui 管理界面与后端接口的对接,然后集成 Shiro 做登录认证和权限拦截,再配置 Ehcache 做二级缓存,最后讲部署和排错。全程使用可复现的代码片段,版本以 SpringBoot 2.5.x 为例。

2. 从零搭建SpringBoot+Layui的项目骨架,先跑通登录页

2.1 创建一个不带前端构建工具的SpringBoot工程

很多项目组会把 Layui 直接放在src/main/resources/static下,不走 Node 构建链路。这是最省事的做法,适合内部管理系统——不需要 webpack 打包、不需要 npm 安装、不需要处理跨域,SpringBoot 的静态资源映射天然支持这个目录。

使用 IDEA 创建项目时,Spring Initializr 选择 Java 8、SpringBoot 2.5.x,依赖勾选 Spring Web、Thymeleaf(如果要用模板渲染)、MyBatis(或 MyBatis-Plus)、MySQL Driver。创建完成后,手动往 pom.xml 里补充 Shiro 和 Ehcache 的依赖:

<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.9.1</version> </dependency>

这里有个容易踩的版本坑:shiro-ehcache依赖的 Ehcache 版本是 2.10.x,和 Spring Boot 2.5 自带的缓存抽象兼容性尚可,但需要排除传递依赖中的ehcache-core,改为显式引入net.sf.ehcache:ehcache:2.10.9.2,避免和 Spring Boot 的缓存管理冲突。如果使用 SpringBoot 3.x,Shiro 的 javax.servlet 依赖会直接启动失败,建议本项目锁定在 2.x 系列。

依赖就绪后,application.yml 配置数据源和 MyBatis 映射:

spring: datasource: url: jdbc:mysql://localhost:3306/student_ms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver cache: type: ehcache mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.studentms.entity server: port: 8080 servlet: context-path: /

spring.cache.type=ehcache会让 Spring 的@Cacheable注解走 Ehcache 实现,这个后面讲缓存时会用到。Mapper 接口和 XML 放到对应位置,写一个最简单的StudentMapper.selectPage查询验证数据库连通性,然后搭建后端返回格式的基类,统一codemsgdata三字段结构,这也正是 Layui 表格组件要求的响应格式。

2.2 Layui 页面引入方式和登录接口的第一版

static目录下创建layui/文件夹,把下载的 Layui 2.8.x 文件拷贝进去,然后在templates/login.html中引入:

<link rel="stylesheet" href="/layui/css/layui.css"> <script src="/layui/layui.js"></script>

登录页面用 Layui 的表单模块渲染,关键逻辑是监听提交按钮,用$.ajax把用户名密码发送到后端。这部分的前端代码不复杂,真正的核心在后端登录接口如何处理 Shiro 的 Subject:

@PostMapping("/login") @ResponseBody public Result doLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); return Result.success("登录成功"); } catch (UnknownAccountException e) { return Result.error("用户不存在"); } catch (IncorrectCredentialsException e) { return Result.error("密码错误"); } catch (LockedAccountException e) { return Result.error("账号被锁定"); } }

SecurityUtils.getSubject()拿到当前访问用户,调用login()后,Shiro 会走Realm的认证流程。这里不能直接操作 Session 存储用户信息,Shiro 会把认证状态写入自身管理的 Session,后续从subject.getPrincipal()获取当前登录用户。登录接口成功后前端跳转到/index,首页再做一次权限校验,渲染当前用户名和菜单。

2.3 ShiroConfig 配置类:过滤链和登录跳转的最小写法

集成 Shiro 到 SpringBoot,核心是一个ShiroFilterFactoryBean的配置类。初次搭建时,过滤链规则越简单越好,先只保护/student/**/index,放开/login/layui/**/css/**等静态资源:

@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl("/login"); factoryBean.setUnauthorizedUrl("/403"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/layui/**", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/index", "authc"); filterChainDefinitionMap.put("/student/**", "authc"); filterChainDefinitionMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }

anon表示匿名可访问,authc表示必须登录,logout是 Shiro 内置的退出过滤器,会清除 Session 后跳转到登录页。注意过滤链是有序的,LinkedHashMap保证顺序,/student/**必须在/**前面声明,否则被通配规则提前拦截变成authc。静态资源的anon规则必须写全,否则登录页引用的 Layui 脚本会被拦截,页面裸奔且难以排查。

配完 ShiroConfig 后,还需要一个自定义 Realm。初学者最容易漏掉的是:Realm 里不仅要实现认证,还要实现授权。如果只做认证,登录能成功,但一访问需要权限的接口就会 403。第一版可以先让doGetAuthorizationInfo返回空权限集,把登录链路验证通,权限控制的细节放到下一章细讲。

3. 深入Shiro认证授权模型,把权限控制做到按钮级

3.1 Realm 中认证与授权分离的设计边界

Shiro 的设计理念里,认证解决“你是不是合法用户”,授权解决“你能干什么”。这两个动作分别对应doGetAuthenticationInfodoGetAuthorizationInfo,在 Realm 中分开实现。认证方法里查一次用户表,授权方法里查角色和权限表,两者不要混在一起做——授权的查询频率远高于认证,混在一起会造成不必要的数据库开销。

@Component public class UserRealm extends AuthorizingRealm { @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user = (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List<String> roles = permissionMapper.selectRolesByUserId(user.getId()); List<String> perms = permissionMapper.selectPermissionsByUserId(user.getId()); info.addRoles(roles); info.addStringPermissions(perms); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userMapper.selectByUsername(username); if (user == null) { return null; } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } }

授权方法最终返回的是角色标识和权限标识的集合。addStringPermissions里的字符串形如student:addstudent:delete,这是配合@RequiresPermissions("student:add")注解使用的。注意 Shiro 的权限字符串是“资源:操作”的约定,不是固定语法,自己定义清晰即可,但全项目要保持一致。

doGetAuthenticationInfo返回null表示用户不存在,Shiro 自动抛出UnknownAccountException;返回SimpleAuthenticationInfo时,Shiro 会拿传入的token.getPassword()和 user 的密码做比对,密码错误时抛IncorrectCredentialsException。密码比对默认使用明文,生产环境必须使用 MD5 加盐或 BCrypt 加密,这个后面单独说。

3.2 在 Service 层还是 Controller 层做权限校验

很多管理系统的权限校验写在 Controller 方法上,通过注解声明所需权限:

@Controller @RequestMapping("/student") public class StudentController { @RequiresPermissions("student:add") @PostMapping("/add") @ResponseBody public Result addStudent(Student student) { studentService.insert(student); return Result.success("新增成功"); } @RequiresPermissions("student:delete") @PostMapping("/delete") @ResponseBody public Result deleteStudent(Integer id) { studentService.deleteById(id); return Result.success("删除成功"); } }

注解生效的前提是 Shiro 的 Spring AOP 支持已开启。在 ShiroConfig 中补充AuthorizationAttributeSourceAdvisorDefaultAdvisorAutoProxyCreator两个 Bean,缺一不可。DefaultAdvisorAutoProxyCreator负责扫描带 Shiro 注解的 Bean 生成代理,AuthorizationAttributeSourceAdvisor把注解与 Shiro 的拦截逻辑绑定。

这里有一个常见误用:把@RequiresPermissions加到 Service 层方法上。虽然 AOP 也能拦截,但 Service 层往往被多个 Controller 调用,权限校验粒度不好控制。建议权限注解统一放在 Controller 层,Service 层专注业务逻辑。如果某些接口需要“登录即可访问”,不加权限注解即可,Shiro 的authc过滤器已经保证登录态。

权限粒度再细致一层是按钮级控制。后端接口已经做了权限拦截,前端菜单和按钮的显隐规则需要和后端保持一致。Layui 渲染表格操作列时,根据当前用户权限决定是否渲染“删除”按钮:

function getOperationColumn() { var opHtml = ''; if (hasPermission('student:edit')) { opHtml += '<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>'; } if (hasPermission('student:delete')) { opHtml += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>'; } return { title: '操作', toolbar: opHtml }; }

hasPermission里的权限集合从哪儿来?常见做法是登录成功后,后端把当前用户的所有权限标识通过接口返回,前端存到全局变量或 localStorage 中。这里的权限查询接口建议加一层缓存,因为每个页面加载都会调用,频繁查数据库完全没有必要——这正是 Ehrcache 切入的场景。

3.3 用户、角色、权限三张表的设计与关联查询

支撑上面权限模型的是经典的五张表:用户表sys_user、角色表sys_role、权限表sys_permission、用户角色关联表sys_user_role、角色权限关联表sys_role_permission。权限表用树形结构存储,父节点是菜单,子节点是按钮操作:

字段类型说明
idbigint主键
namevarchar(50)权限名称,如“学生新增”
permsvarchar(100)权限标识,如 student:add
typetinyint1菜单 2按钮
parent_idbigint父级ID,顶级为0
sortint排序号

查询用户权限的 XML 核心是一段拼接了两次关联表的 SQL:

<select id="selectPermissionsByUserId" resultType="string"> SELECT DISTINCT p.perms FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id INNER JOIN sys_role r ON ur.role_id = r.id INNER JOIN sys_role_permission rp ON r.id = rp.role_id INNER JOIN sys_permission p ON rp.permission_id = p.id WHERE u.id = #{userId} AND p.perms IS NOT NULL AND p.perms != '' </select>

DISTINCT是必要的,一个用户有多个角色、多个角色有重叠权限时,不去重会导致SimpleAuthorizationInfo里的权限集合出现重复项。Shiro 的权限匹配不排斥重复,但会加大内存占用。另一个细节是p.perms IS NOT NULL过滤掉菜单节点——菜单节点本身的 perms 为空,只有按钮节点才写权限标识。

角色变更后权限实时生效的问题需要解释清楚。由于doGetAuthorizationInfo在每次权限校验时都会调用(Shiro 会缓存授权信息,默认缓存策略见下一章),如果 Shiro 缓存了授权结果,修改用户角色后必须清除该用户的授权缓存,否则权限要等缓存过期才生效。缓存粒度控制得当的话,这个问题的处理方式在 Ehcache 配置中解决。

4. 集成Ehcache缓存框架,缓存权限数据和热点查询

4.1 为什么是Ehcache而不是Redis——单机系统的务实选择

在这个学生管理系统的场景中,用户量是千级而非百万级,并发是几十而非几千,引入 Redis 意味着多一套服务部署、多一份序列化配置、多一个网络延迟。Ehcache 是进程内缓存,直接嵌在 JVM 里,读取零网络开销,配置只要一份 XML 文件。对单机部署的管理系统来说,这是性价比最高的方案。

Shiro 本身对 Ehcache 有原生支持,shiro-ehcache依赖里就包含了EhCacheManager实现。Shiro 的认证和授权信息可以通过这个 CacheManager 做缓存,这样用户第一次登录后,权限数据不会每次都查数据库。同时 Spring 的@Cacheable注解也能使用同一个 Ehcache 实例,实现业务数据的缓存。

需要特别说明的是进程内缓存的边界:Ehcache 的数据存在当前 JVM 中,如果系统做了多实例部署,每个实例的缓存是独立的,数据一致性无法保证。学生管理系统通常单实例部署,这个问题不存在。如果要向分布式演进,再考虑把 Shiro 的 Session 和缓存迁移到 Redis,但那是另一个改造工程了。

4.2 ehcache.xml 参数配置和三大缓存区域的划分

src/main/resources下创建ehcache.xml,配置三个缓存区域:

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="java.io.tmpdir/studentms-ehcache"/> <defaultCache maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" memoryStoreEvictionPolicy="LRU"/> <cache name="authorizationCache" maxElementsInMemory="2000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" overflowToDisk="false"/> <cache name="authenticationCache" maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" overflowToDisk="false"/> <cache name="studentPageCache" maxElementsInMemory="500" eternal="false" timeToIdleSeconds="60" timeToLiveSeconds="60" overflowToDisk="false"/> </ehcache>

timeToIdleSeconds是元素闲置多少秒后被移除,timeToLiveSeconds是元素从创建到过期的最长存活时间。两者都设置时,先到者生效。授权缓存设置 30 分钟,意味着管理员改完角色权限后,最长 30 分钟缓存过期,用户权限才刷新。如果这不可接受,就需要在修改权限的接口里主动清缓存。

memoryStoreEvictionPolicy="LRU"表示内存满了之后淘汰最久未使用的元素。对授权缓存这种“总量可控、单次读取频繁”的数据,LRU 是合理的。注意overflowToDisk="false"—— 权限数据放磁盘没有意义,反而引入 IO 延迟。

配置类中让 Shiro 的 CacheManager 指向这个 Ehcache 实例:

@Bean public EhCacheManager ehCacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return cacheManager; } @Bean public SecurityManager securityManager(UserRealm userRealm, EhCacheManager ehCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); userRealm.setCacheManager(ehCacheManager); return securityManager; }

setCacheManagerAuthorizingRealm父类的方法,必须等 SecurityManager 创建之前调用。Shiro 内部有默认的缓存命名约定:授权缓存默认叫authorizationCache,认证缓存默认叫authenticationCache,这正好对应 ehcache.xml 中前两个 cache 的 name。如果自定义缓存名称,需要在 realm 上手动指定setAuthenticationCacheNamesetAuthorizationCacheName

4.3 @Cacheable 注解缓存学生列表,注意 key 和失效时机

对于学生分页列表这类查询频率高、数据变化不频繁的接口,直接在 Service 层加缓存注解:

@Override @Cacheable(cacheNames = "studentPageCache", key = "#pageNum + '_' + #pageSize + '_' + (#keyword == null ? '' : #keyword)") public PageResult<Student> selectPage(int pageNum, int pageSize, String keyword) { return studentMapper.selectPage(pageNum, pageSize, keyword); }

cacheNames对应 ehcache.xml 里的studentPageCachekey用 SpEL 表达式拼接分页参数和查询关键字,不同查询条件缓存互不干扰。这里有个隐患:#keyword为 null 时直接拼接字符串会得到"1_10_null",下次传入空字符串时 key 变成"1_10_",导致同一份数据被缓存两份。解决方式是像上面代码一样做一次空值归一化。

缓存失效的处理比缓存本身更关键。新增、修改、删除学生后,列表缓存已经过期,必须主动清除:

@Override @CacheEvict(cacheNames = "studentPageCache", allEntries = true) public void updateStudent(Student student) { studentMapper.updateById(student); }

allEntries = true表示清空整个studentPageCache缓存区域,而不是删除单个 key。因为分页列表的 key 是分页参数组合出来的,你不知道这次修改会影响哪几页的数据,全部清空是最简单可靠的做法。代价是清空后的第一次查询会重新走数据库,但学生管理系统的写操作频率低,这个代价完全可以接受。

@CacheEvict@Cacheable同时出现在 Service 实现类中时,必须注意 Spring AOP 的自调用问题。如果同一个类内部方法调用updateStudent,缓存拦截器不会生效。常见做法是 Controller 调 Service 接口,或者内部调用时通过AopContext.currentProxy()获取代理对象。新手最容易在这个地方困惑:注解写了但缓存完全没起作用。

5. 生产环境部署与线上问题排查的五个关键点

5.1 打包方式和外部配置文件分离

开发环境用spring-boot-maven-plugin直接运行 main 方法即可。生产环境需要打可执行 JAR 包,但数据库密码、缓存路径这些配置不应该打进 JAR 中。使用 Spring Boot 的配置外部化机制,在 JAR 同级目录放一个application-prod.yml,启动命令显式指定:

mvn clean package -DskipTests java -jar studentms.jar --spring.profiles.active=prod --spring.config.location=./application-prod.yml

spring.config.location指定外部配置文件路径,覆盖 JAR 内部的配置。这样换环境部署时不用重新打包,运维直接改外部配置里的数据源和端口。MySQL 连接串建议追加connectTimeout=5000&socketTimeout=60000,避免数据库暂时不可用时请求线程无限等待。

5.2 Layui 表格请求失败的三种典型表现

管理页面常见的故障是表格区域一直转圈不加载数据。打开浏览器开发者工具,Network 面板看请求状态。第一种情况:请求返回 302 且 Location 指向/login,说明当前 Session 失效或未登录,Layui 的 table 请求默认携带 Same-Origin 凭据,但 Shiro 的拦截规则没有放行/student/list接口,需要检查过滤链配置。第二种情况:请求返回 200 但表格空白,多半是响应 JSON 格式不符合 Layui 表格的约定。Layui 要求返回结构为{"code":0, "msg":"", "count":100, "data":[...]},其中 count 是总数,data 是当前页数据。如果把整个 PageResult 对象直接返回而不拆出countdata,表格就渲染不出来。第三种情况:接口报 500,查看后端日志,常见原因是 PageHelper 分页插件没有在 MyBatis 配置中启用:

@Configuration public class MybatisConfig { @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } }

如果使用 MyBatis-Plus,分页插件版本要和 SpringBoot 版本匹配。MyBatis-Plus 3.4.x 对应 SpringBoot 2.x 没问题,但 3.5.3 之后的版本在 SpringBoot 2.5 上有兼容隐患,可以把PaginationInterceptor换成MybatisPlusInterceptor并添加PaginationInnerInterceptor

5.3 Shiro 的授权缓存不刷新问题:清缓存接口和触发时机

上一章提到角色权限修改后授权缓存 30 分钟才过期,运维反馈“用户权限改了半小时还没生效”。解决思路有两种,优先推荐第二种。第一种是把授权缓存的timeToLiveSeconds改成 60 秒,代价是每个用户每分钟最多一次权限查询数据库,用户量大时数据库压力变大。第二种是做一个主动清缓存接口:

@GetMapping("/admin/clearShiroCache") @ResponseBody @RequiresRoles("admin") public Result clearShiroCache(String username) { if (StringUtils.hasText(username)) { User user = userService.selectByUsername(username); if (user != null) { userRealm.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); return Result.success("已清除用户 " + username + " 的授权缓存"); } return Result.error("用户不存在"); } return Result.success("未指定用户,缓存保留"); }

clearCachedAuthorizationInfoAuthorizingRealm提供的方法,传入PrincipalCollection即可清除该用户的授权信息。如果修改了角色的权限,但用户未重新登录,PrincipalCollection里的用户信息仍然有效,调用这个方法就能生效。注意这个接口必须加管理员角色限制,否则任何登录用户都能清除别人的缓存,虽然这不是安全事故,但会造成不必要的数据库查询。

5.4 Session 超时和 RememberMe 的配合策略

Shiro 默认 Session 超时时间是 30 分钟,用户在使用系统时如果长时间不操作,Session 过期后任意点击都会跳回登录页。对于学生管理系统来说,30 分钟偏短,建议在配置中调长:

@Bean public DefaultWebSecurityManager securityManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(2 * 60 * 60 * 1000L); DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(sessionManager); return securityManager; }

setGlobalSessionTimeout以毫秒为单位,2 小时是管理系统比较合理的值。但要注意,Session 是存在服务器内存中的,maxElementsInMemory只控制缓存数量,Session 本身没有配置 Ehcache 持久化,用户量大时内存会膨胀。学生管理系统的用户量完全不用担心这个问题。RememberMe 功能要谨慎开启,它会把用户身份序列化到 Cookie 中,如果同时在多个浏览器使用,可能出现身份混淆。建议关闭:

token.setRememberMe(false);

5.5 密码明文存储的最后整改:MD5 加盐与 Shiro 的匹配器

如果系统已经用明文密码运行了一段时间,整改分两步:数据库中的密码改为MD5(密码 + 用户名)MD5(密码 + 随机盐),Shiro 这边配置对应的凭证匹配器:

@Bean public HashedCredentialsMatcher credentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("MD5"); matcher.setHashIterations(1); return matcher; }

然后在UserRealm构造函数中传入credentialsMatcher。修改密码时,用同样的算法生成密文再存库:

String encryptedPassword = new SimpleHash("MD5", rawPassword, username, 1).toHex();

注意盐用的字段要和 Realm 中SimpleAuthenticationInfo传入的盐一致。如果 Realm 里new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), getName())没有传盐,那么匹配器在验证时会用空盐计算,密码永远匹配不上。最常见的整改报错就是这个盐参数不对齐。setHashIterations(1)表示只做一次 MD5,迭代次数越多越安全,但也要考虑登录耗时,一般 1024 次以内没问题。

5.6 用 Arthas 在线诊断权限校验卡顿

权限接口响应慢,但本地复现不了,生产环境又没有配置 SkyWalking 这类链路追踪。推荐用 Arthas 在线诊断,不需要重启服务:

curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar

启动后选择学生管理系统的进程 PID,然后执行 trace 命令跟踪权限查询链路的耗时:trace com.example.studentms.service.impl.UserServiceImpl selectPermissionsByUserId。这个命令会打印方法内部每一步的耗时分布,很快就能看出是数据库查询慢还是缓存未命中导致走了全量 SQL。如果输出的耗时集中在 MyBatis 的 Mapper 调用上,检查该 SQL 的索引是否生效:

EXPLAIN SELECT DISTINCT p.perms FROM sys_user u INNER JOIN sys_user_role ur ON u.id = ur.user_id ...

重点看key列,如果为 NULL 说明没有走索引。sys_user_rolesys_role_permission两张关联表,外键字段必须建索引,否则联表查询随着数据量增长性能下降非常明显。对学生管理系统的数据量来说,这个优化一次到位。

Ehcache 的命中率也可以通过日志观察。在application-prod.yml中加一行配置:logging.level.net.sf.ehcache=DEBUG,然后观察日志中 Cache hit 和 Cache miss 的比例。如果 miss 率长期超过 30%,说明缓存 key 设计不当或过期时间太短,需要重新审视@Cacheable的 key 组成。

部署完成后,建议把所有配置项按环境归纳成一张表:MySQL 连接、Ehcache 磁盘路径、Shiro 会话超时、日志级别。开发环境用本地 MySQL,生产环境换独立的库,仅此一项就能避免大部分因为配置混用导致的低级故障。真正把一个管理系统做稳定,靠的不是某个炫技的框架,而是把这些边角配置一项项抠干净。

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

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

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

立即咨询