SpringBoot整合Shiro与Activiti:OA系统权限与流程引擎实战

发布时间:2026/9/14 12:31:45
SpringBoot整合Shiro与Activiti:OA系统权限与流程引擎实战 简介Spring Boot MyBatis Shiro Activiti 企业办公 OA 系统是一套经过测试运行成功的毕业设计项目面向计算机相关专业学生、企业员工及 Java 初级开发者适用于课程设计、大作业、毕业设计或初期项目立项演示。项目将 Shiro 权限控制与 Activiti 工作流审批整合到企业办公场景中能够帮助学习者理解用户认证、角色授权、请假/报销等业务流程的完整实现。压缩包共 497 个文件、约 3.82MB包含 177 个 Java 源码、72 个 HTML 页面、62 个 JS 脚本、35 个 CSS 样式、32 个 XML 配置、19 个 GIF 动态图以及 Activiti 流程定义文件、SQL 数据库脚本、JAR 依赖等覆盖后端逻辑、前端页面、流程定义和数据结构。资源包还附有 Maven 包装器、yml/properties 配置和启动脚本目录结构清晰便于直接导入调试、二次开发和学习拆解。目前已有 94 人学习/下载代码功能完整注释与配置较齐全功能模块划分与代码组织方式清晰适合作为参考项目、练习实战或毕业答辩展示。1. 为什么要用 Shiro 管权限、Activiti 管流程而不是全塞进业务代码里企业 OA 系统看起来是增删改查真正拉开差距的是两件事谁能点这个按钮权限请假单该往谁那儿走流程。这个基于 SpringBoot MyBatis Shiro Activiti 的课设项目把这层关系放得很清楚。实际开发里很多项目把审批状态写到业务表的 status 字段里再加一个判断逻辑做到后面每个需求都要改代码。Activiti 的价值在于把流程怎么走从 Java 代码里抽出来变成一张可以单独修改和调试的 BPMN 图Shiro 的价值在于把谁能做什么从业务代码里抽出来变成可配置的过滤器链和注解。这个项目对计科、软工、大数据方向的同学来说最大的学习点是数据流经权限层和流程引擎时每一层都不侵入业务字段而不是代码本身有多复杂。2. 认证与授权SpringBoot 整合 Shiro 时Realm 才是真正的业务入口2.1 先看 RBAC 表结构理解了它 Shiro 的过滤器配置才有意义Shiro 本身不存储用户数据它只负责问你要数据。真正干活的组件叫 Realm你的数据在 MySQL 里就要写一个 JDBC 或 MyBatis 类型的 Realm通过 Mapper 查用户、查角色、查权限。这个 OA 项目里对应的表设计和常见 RBAC 一致我按经验整理了最小可用的四张表CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, salt VARCHAR(32), status TINYINT DEFAULT 1 COMMENT 1正常 0锁定, emp_no VARCHAR(20) COMMENT 员工编号关联审批人 ); CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(30) NOT NULL COMMENT 如 admin, manager, staff, role_name VARCHAR(50) ); CREATE TABLE sys_user_role ( user_id BIGINT, role_id BIGINT, PRIMARY KEY (user_id, role_id) ); CREATE TABLE sys_permission ( id BIGINT PRIMARY KEY AUTO_INCREMENT, perm_code VARCHAR(50) NOT NULL COMMENT 如 leave:apply, leave:approve );表中salt字段对应 Shiro 的 Md5Hash 加密盐值。perm_code采用资源:操作的形式可以精确控制到某个接口比如leave:apply控制的是请假申请leave:approve控制的是审批动作这样做的好处是前端按钮显隐和后端接口权限可以共用同一套权限码。2.2 自定义 Realm 的三个关键点认证、授权、密码匹配这个项目的核心代码在一个继承AuthorizingRealm的自定义类里我拆解一下最关键的逻辑public class OaRealm extends AuthorizingRealm { Autowired private SysUserMapper userMapper; Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 1. 从 token 里取用户名 String username (String) token.getPrincipal(); // 2. 用 MyBatis 查用户表 SysUser user userMapper.findByUsername(username); if (user null) { throw new UnknownAccountException(账号不存在); } if (user.getStatus() ! 1) { throw new LockedAccountException(账号已锁定); } // 3. 注意这里返回的是数据库里的密文不是加密后的明文 return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username (String) principals.getPrimaryPrincipal(); // 查角色与权限集合 SetString roles userMapper.findRoles(username); SetString perms userMapper.findPermissions(username); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(perms); return info; } }doGetAuthenticationInfo负责回答你是谁doGetAuthorizationInfo负责回答你能干什么。这里最容易错的地方是ByteSource.Util.bytes(user.getSalt())注册用户时盐值必须和这个保持一致否则密码永远校验不过。密码加密建议使用 Shiro 的HashedCredentialsMatcher在配置类里单独声明不要在 Realm 里手工比对Bean public OaRealm oaRealm() { OaRealm realm new OaRealm(); HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(MD5); matcher.setHashIterations(1024); // 加密迭代次数 realm.setCredentialsMatcher(matcher); return realm; }迭代次数 1024 是常见做法增加了暴力破解的成本但不会显著拉慢登录响应。修改迭代次数后已注册用户需要重新生成密文否则全部登录失败。2.3 过滤链配置为什么anon和authc的顺序不能乱Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factory new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); MapString, String filterChain new LinkedHashMap(); // 注意使用 LinkedHashMap 保证顺序 filterChain.put(/login, anon); filterChain.put(/css/**, anon); filterChain.put(/js/**, anon); filterChain.put(/images/**, anon); // /** 必须放最后 filterChain.put(/**, authc); factory.setFilterChainDefinitionMap(filterChain); factory.setLoginUrl(/login); factory.setUnauthorizedUrl(/403); return factory; }anon表示匿名可访问authc表示必须登录。/**放在最后意味着全站默认拦截静态资源如 layui 的 css 和 js 文件如果没放行页面会变得完全无样式。用LinkedHashMap而不是普通HashMap是因为 HashMap 不保证迭代顺序顺序错了可能导致/login被/**拦截。提示热词里搜索量大的是shiro 最新版本注意 Shiro 1.x 的 Spring Boot 整合方式和 2.x 有差异。这个项目若使用 1.8 版本ShiroFilterFactoryBean包名是org.apache.shiro.spring.web.ShiroFilterFactoryBean2.x 里类的位置和配置方式都有调整。3. 工作流引擎leave.bpmn 与 apply.bpmn 的部署机制和状态流转原理3.1 BPMN 文件到底在描述什么先看节点类型再谈代码leave.bpmn和apply.bpmn是标准的 Activiti/BPMN 2.0 流程定义文件。不要以为它们和前端页面有关它们描述的是节点和连接线。一个最小请假流程包含startEvent开始节点、userTask用户任务对应某个人或某个角色的待办、exclusiveGateway排他网关按照条件往指定方向走、endEvent结束节点。审批链的核心在flowable:assignee或activiti:assignee属性上比如userTask idmanagerApprove name经理审批 activiti:assignee${approver} /${approver}是流程变量由发起人在启动流程时传入。如果写成固定的人名就没办法动态指定审批人。这个 OA 项目里更常见的做法是activiti:candidateUsers${approverRoles}然后由候选人列表里的用户认领任务。3.2 用 Java 代码部署与启动一个流程实例Activiti 的表自己维护不依赖业务表比如ACT_RE_PROCDEF存放流程定义ACT_RU_TASK存放运行时任务ACT_HI_TASKINST存放历史任务。这套命名规则和表结构是理解 Activiti 的关键。部署和启动的核心代码Service public class LeaveWorkflowService { Autowired private RepositoryService repositoryService; Autowired private RuntimeService runtimeService; Autowired private TaskService taskService; /** * 部署流程定义通常项目启动时只做一次 */ public void deployProcesses() { Deployment deployment repositoryService.createDeployment() .addClasspathResource(processes/leave.bpmn) .addClasspathResource(processes/apply.bpmn) .name(OA审批流程) .deploy(); System.out.println(部署ID: deployment.getId()); } /** * 发起请假返回流程实例ID */ public String startLeaveProcess(String applicant, Integer days, String reason) { MapString, Object variables new HashMap(); // 传给 org.springframework.expression 的流程变量 variables.put(applicant, applicant); variables.put(days, days); variables.put(reason, reason); // 按流程定义 key 启动 ProcessInstance instance runtimeService .startProcessInstanceByKey(leave, variables); return instance.getId(); } }startProcessInstanceByKey的 key 是 BPMN 文件里processDefinitionKey属性的值。部署后如果修改了 BPMN 文件deploy()会生成新版本的流程定义旧实例不受影响这是 Activiti 和其他状态机实现最大的区别流程中途改版是支持的历史数据有对应的版本记录。3.3 审批操作映射complete提交后网关按条件转向审批人看到的是ACT_RU_TASK里分配给自己的任务审批动作本质是告诉引擎我这个节点完成了。代码如下// 审批通过 public void approve(String taskId, Boolean approved, String comment) { MapString, Object vars new HashMap(); vars.put(approved, approved); vars.put(comment, comment); // 完成任务引擎自动根据网关条件沿连线走向下一个节点 taskService.complete(taskId, vars); }加上流程图里exclusiveGateway的 condition 表达式比如${approved true}引擎就能自动决定进入经理审批还是直接结束。这条链路理解之后Activiti 的核心就已经掌握了节点由 BPMN 定义流转条件由变量触发历史记录自动进入ACT_HI_*表。注意taskId与processInstanceId不是一回事。taskId只在当前任务存活期间有效流程结束后再查任务表中的数据会直接为空此时要查ACT_HI_TASKINST。4. 业务表单与 MyBatis 持久化把请假单和审批记录串起来的数据链路4.1 表单数据、流程实例、业务表三者的关联设计Activiti 只管流程不管表单内容。完整 OA 项目里请假天数、原因这类数据一定要落业务表。常见做法是在业务表的leave_apply里加一个process_instance_id字段把两条线扣在一起查询详情时先用流程实例 ID 查引擎历史表拿到审批轨迹再回业务表拿表单内容。CREATE TABLE leave_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_instance_id VARCHAR(64) COMMENT 关联 ACT_RU_EXECUTION, applicant VARCHAR(50) NOT NULL, days INT NOT NULL, reason VARCHAR(500), status VARCHAR(20) COMMENT pending/approving/passed/rejected, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );process_instance_id字段必须加索引。生产环境排查慢查询时最常遇到的就是拿流程实例 ID 去关联业务表由于没索引导致全表扫描。这个字段在整个生命周期内只写入一次但会被反复读取。4.2 MyBatis 多表查询和二级缓存的使用边界查询审批列表时通常需要一次查出业务数据加最新节点状态。Mapper XML 里的resultMap是这样组织的resultMap idLeaveApplyVO typecom.oa.vo.LeaveApplyVO id propertyid columnid / result propertyprocessInstanceId columnprocess_instance_id / result propertyapplicant columnapplicant / result propertydays columndays / result propertyreason columnreason / result propertycurrentNode columntask_name / /resultMap select idselectLeaveWithTask resultMapLeaveApplyVO SELECT la.id, la.process_instance_id, la.applicant, la.days, la.reason, rt.NAME_ AS task_name FROM leave_apply la LEFT JOIN ACT_RU_TASK rt ON rt.PROC_INST_ID_ la.process_instance_id WHERE la.applicant #{username} ORDER BY la.create_time DESC /selectACT_RU_TASK表在任务完成时会删除该行记录因此这个查询只能看到当前还在审批中的数据。已经走完流程的单据需要改用ACT_HI_TASKINST配合END_TIME_列来确认最终状态。不少人在课设答辩时被问到为什么流程结束后任务查不到了原因就在这张表的设计上。关于 MyBatis 缓存默认一级缓存是 SqlSession 级别的同一个 SqlSession 内相同查询会直接返回缓存结果。二级缓存默认关闭cache/标签加在 mapper XML 里就会开启。这个 OA 项目如果查询和更新走不同 mapper开启二级缓存前必须确认关联表的数据变更能够清掉对应缓存块否则用户改完邮箱后其他人仍可能读到旧数据。没有充分把握就不要开二级缓存。4.3 审批历史归档查询的常用写法流程完结后审批记录要从历史表里拼出来。SQL 并不复杂核心是取每个节点产生的历史任务SELECT h.NAME_ AS node_name, h.ASSIGNEE_ AS approver, h.START_TIME_ AS start_time, h.END_TIME_ AS end_time, h.DELETE_REASON_ AS task_comment FROM ACT_HI_TASKINST h WHERE h.PROC_INST_ID_ #{processInstanceId} ORDER BY h.START_TIME_ ASCDELETE_REASON_在流程引擎里记录的是任务消失的原因但业务上的审批意见如同意驳回通常存在独立的ACT_HI_COMMENT表中需要再拿TASK_ID_去关联。这块是流程引擎所有表里最容易混淆的地方。5. 前端页面和构建链路layui、summernote 与 mvnw.cmd 的整合现场5.1 为什么要用 layui 配 summernote 组合资源包里出现layui.css、summernote-bs3.css和animate.css说明这是一个以后台管理为主的前端方案。layui 负责整体布局和表格组件summernote 承担富文本编辑器如公告、邮件正文的编辑bootstrap 系样式负责把编辑器渲染统一。SpringBoot 的静态资源默认映射在classpath:/static/下所以这些 css 文件放在src/main/resources/static/css/即可直接访问。表单页面的写法上layui 的表单监听器和 Vue 不同需要在提交按钮上绑定lay-filter然后在全局 JS 里用form.on(submit(leaveForm))监听这种写法在老项目中非常常见layui.use([form, layer], function () { var form layui.form; form.on(submit(leaveForm), function (data) { // data.field 里是表单所有字段的值 $.ajax({ url: /leave/start, method: POST, contentType: application/json, data: JSON.stringify(data.field), success: function (res) { layer.msg(流程已发起); location.reload(); } }); // 返回 false 阻止默认的表单提交交给 ajax 处理 return false; }); });summernote 的值不在普通表单字段里初始化后要用$(#summernote).summernote(code)获取 HTML 内容再合并到提交数据里。直接$(#form).serialize()会丢掉编辑器里的内容这是消息发布类功能最常见的 bug。5.2 控制器路由如何和 Shiro 权限码联动后端写法上和普通 SpringBoot 项目的区别在于注解RestController RequestMapping(/leave) public class LeaveController { Autowired private LeaveWorkflowService workflowService; PostMapping(/start) RequiresPermissions(leave:apply) public Result start(RequestBody LeaveDTO dto) { // 当前登录用户从 Shiro 会话里取 String username (String) SecurityUtils.getSubject().getPrincipal(); String processInstanceId workflowService.startLeaveProcess( username, dto.getDays(), dto.getReason()); return Result.ok(processInstanceId); } PostMapping(/approve) RequiresPermissions(leave:approve) public Result approve(RequestBody ApproveDTO dto) { workflowService.approve(dto.getTaskId(), dto.getApproved(), dto.getComment()); return Result.ok(); } }RequiresPermissions(leave:apply)会在方法执行前由 Shiro 的 AOP 拦截器做权限判断它的生效前提是shiro.spring.web配置正确否则注解形同虚设。判断 Shiro 是否真正接入的方法是没有登录时直接请求这个接口如果返回 401 或跳转登录页说明过滤器链生效。5.3 使用 mvnw.cmd 构建项目时一个值得注意的坑资源包里有mvnw.cmd说明项目启用了 Maven Wrapper。Windows 下直接用mvnw.cmd clean package -DskipTests构建本质是自动下载 Maven 发行版来执行构建好处是不依赖本机全局 Maven 版本。构建时常见的问题是 Shiro 依赖和 SpringBoot 版本冲突尤其是用到不兼容的 Shiro Spring 模块时启动日志会给出几百行的 Bean 冲突栈但根因通常是仓库里缓存了旧版本的 jar 包。# Windows 下执行清理与打包 mvnw.cmd clean package -DskipTests # 如果 Wrapper 下载 Maven 失败先删除 .mvn 目录后重试 # 本地已安装 Maven 时也可以直接用 mvn 命令替代 mvn clean package -DskipTests提示-DskipTests会跳过测试用例执行但会编译测试代码-Dmaven.test.skiptrue则连编译都跳过打包更快但风险更高。课设交付场景下建议保留-DskipTests。6. 版本兼容与部署排错从 springboot 版本太高到 heapdump 泄露排查6.1 约定版本矩阵少走大量弯路这个项目的直观依赖是 SpringBoot、MyBatis、Shiro、Activiti。但 Activiti 与 SpringBoot 的官方整合一直是难点。Activiti 6.x 对应的 SpringBoot 版本集中在 1.5.x 和 2.0.xActiviti 7.x 才支持 SpringBoot 2.3。热词中频繁出现的springboot版本太高大多指的就是 Activiti 与 SpringBoot 自动配置不兼容的问题。如果导入项目后启动即失败先检查版本对应关系组件建议版本区间关键说明SpringBoot2.3.x ~ 2.5.x避免使用 3.xActiviti 支持不友好Activiti7.1.0.M6 或 5.22.05.x 表前缀ACT_规则简单适合教学Shiro1.8.0 或 1.9.0需配shiro-spring-boot-web-starterMyBatis Starter2.1.x与 SpringBoot 2.3 匹配度最好Shiro 1.9 以前默认的会话管理器在 Controller 直接返回ModelAndView时表现不一致推荐统一返回 JSON由前端做页面跳转。这个方案对课设前后端分离或者模板引擎配合都更稳定。6.2 三个必查的坑BPMN 缓存、过滤器乱序、富文本回显第一个坑是 BPMN 文件的缓存问题。多人协作时有人直接改了leave.bpmn但没重新部署导致引擎内存里还是旧定义ACT_RE_PROCDEF表中会出现多个版本。排查时去数据库执行SELECT ID_, KEY_, VERSION_, DEPLOYMENT_ID_, RESOURCE_NAME_ FROM ACT_RE_PROCDEF WHERE KEY_ leave ORDER BY VERSION_ DESC;如果存在多个版本且新版本不是VERSION_最大的一行需要把构建文件中的流程资源重新部署或者手动清理。第二个坑是 Shiro 过滤器和 SpringMVC 拦截器的执行顺序。RequiresPermissions由 AOP 生效filterChain里的authc由过滤器链生效。两者不是同一阶段结果就是未登录用户访问一个带权限注解的接口时可能先被authc拦截跳转到登录页压根走不到权限注解。这符合预期但很多同学误以为注解不生效。第三个坑是 summernote 内容回显问题。HTML 格式化后textarea 标签里嵌套的内容会被解析为子元素后端拿getParameter()拿不到完整字段。解决方式是用script typetext/html作为容器回显后再注入编辑器。6.3 启动后如何验证系统确实可用启动项目后按下面顺序检查比直接点页面更有把握打开http://localhost:8080/未登录时应跳转/login证明 Shiro 过滤器链生效。登录后访问一个无权限接口如普通员工访问leave:approve对应 URL应返回 403证明权限注解和 Realm 授权都正常。发起一条请假流程在ACT_RU_TASK中应出现managerApprove任务节点证明 Activiti 已成功部署并启动流程。上游审批人同意后ACT_RU_TASK中该任务消失流程进入下一节点或结束证明网关条件判断正常。另外注意 SpringBoot Actuator 相关的热词搜索。如果项目引用了spring-boot-starter-actuator且暴露了heapdump端点线上环境可能泄露 JVM 内存快照攻击者可从中提取密码等敏感信息。课设交付或生产部署时在application.properties中加入management.endpoints.web.exposure.includehealth,info management.endpoint.health.show-detailsnever只暴露健康检查端点其余全关。这个配置和 Shiro 的配套使用能让整个项目的安全性评估往上提一个档次也正好回应了检索里频繁出现的 heapdump 漏洞问题。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询