SpringBoot整合Activiti与Shiro:工作流权限审批系统实战

发布时间:2026/10/12 13:52:02

SpringBoot整合Activiti与Shiro:工作流权限审批系统实战 1. 为什么要在业务系统里同时引入工作流与权限框架做过企业级业务系统的人大概都有这种体会一个审批类需求刚上线时流程简单得用几个状态字段就能糊弄过去可一旦业务跑起来节点越加越多、审批人越来越复杂、还要支持驳回、加签、转办、撤回这时候再靠手写状态机就是给自己挖坑。我见过太多项目在第二期需求评审时被业务方一句“这个流程能不能改一下”直接问懵最后只能推倒重来。这个项目要解决的核心问题就是把Activiti工作流引擎和SpringBoot、Shiro权限框架这三样东西揉到一起搭出一套能真正落地的审批系统骨架。Activiti负责流程的定义、流转、任务分配和历史追溯SpringBoot负责把整个工程的组织结构、依赖注入、自动配置打理干净Shiro负责登录认证、角色授权、细粒度到按钮级别的权限控制。三者各司其职但整合起来有一堆坑尤其是任务候选人、候选组和Shiro角色之间的映射关系处理不好就会出现“流程走到某节点却没人能审批”的尴尬局面。这套东西适合谁看如果你已经写过SpringBoot项目对MVC那一套不陌生但一提到工作流就只知道画个流程图却不知道怎么和业务表关联那这篇内容就是给你准备的。如果你还在用if-else判断审批状态那更应该看看正规军是怎么打的。我会把整合过程中的设计思路、关键配置、踩过的坑都摊开讲代码片段可以直接抄参数选择我会告诉你为什么这么定。2. 整体架构设计与技术选型考量2.1 三层职责划分的底层逻辑先把三个框架的边界划清楚这是整个项目不跑偏的前提。很多整合失败的案例根源就在于职责混乱——有人把权限判断写进流程监听器里有人把流程节点状态塞进Shiro的授权信息里最后代码变成一团乱麻。我的划分原则是这样的Shiro管“谁能进这个系统、谁能点这个按钮”Activiti管“这个单据现在走到哪、下一步该谁处理”。两者通过用户身份这个纽带连接。具体来说Shiro的Subject里存着当前登录用户的身份信息Activiti在创建任务时通过AssignmentHandler或者任务监听器把这个身份信息转换成流程的候选人。SpringBoot在这里的角色是粘合剂。它通过starter机制把Activiti的ProcessEngine、Shiro的SecurityManager都纳入IOC容器管理让它们能共享数据源、共享事务管理器。这一点非常关键因为流程流转和业务数据更新必须在同一个事务里否则就会出现“任务已创建但业务单据状态没改”的数据不一致。2.2 版本选择与依赖冲突处理版本这块我踩过血泪教训。Activiti 6和Activiti 7的API差异很大Activiti 7走的是SpringBoot 2.x的路线但它的云版本Activiti Cloud又是另一套东西别搞混。我建议用Activiti 6.0.0配合SpringBoot 2.3.x这个组合最稳社区资料也最全。Shiro用1.7.x以上版本低版本有反序列化相关的安全隐患这个不用我多说。依赖冲突主要集中在两点一是Activiti自带的mybatis版本和SpringBoot管理的版本可能打架需要在pom里显式排除再引入二是Shiro的starter和SpringBoot的web starter在过滤器注册顺序上要协调。我的做法是在pom里把Activiti的mybatis依赖排除掉统一用SpringBoot管理的版本然后在配置类里手动调整FilterRegistrationBean的顺序确保Shiro的过滤器在Activiti的Web过滤器之前执行。dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter-basic/artifactId version6.0.0/version exclusions exclusion groupIdorg.mybatis/groupId artifactIdmybatis/artifactId /exclusion /exclusions /dependency注意排除mybatis后要确保SpringBoot的mybatis starter版本在3.5.x以上否则Activiti的查询会报奇怪的SQL语法错误。2.3 数据库表结构规划思路Activiti默认会建25张表ACT_开头这些表不要动也不要去改它的字段。业务表单独建通过BUSINESS_KEY字段和流程实例关联。我见过有人为了“省事”把业务数据直接塞进Activiti的变量表里结果流程一多变量表膨胀到几千万行查询慢得没法看。正确的做法是业务主表存核心字段流程实例ID和任务ID作为外键关联字段存在业务表里。这样查“我的待办”时先用Activiti的API查出任务列表拿到businessKey再去业务表里查详情。虽然多一次查询但数据边界清晰后期维护成本低得多。3. Shiro权限体系与Activiti任务分配的整合方案3.1 认证流程的定制化改造Shiro默认的认证是UsernamePasswordToken但企业系统往往需要支持多种登录方式比如手机号验证码、工号密码。我的做法是自定义一个Realm继承AuthorizingRealm在doGetAuthenticationInfo方法里根据登录类型走不同的校验逻辑。这里有个细节要注意Activiti在创建任务时如果指定了候选人它会去查用户表。但Activiti自带的IdentityService管理的用户表和Shiro的Realm是两套东西。我的建议是不要用Activiti的IdentityService而是通过自定义的TaskListener在任务创建时从Shiro的Realm里查用户信息然后手动设置候选人。这样用户数据只有一份避免同步问题。public class CustomRealm extends AuthorizingRealm { Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { String username (String) token.getPrincipal(); User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(用户不存在); } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } }3.2 角色与流程候选组的映射策略这是整个整合中最容易出问题的地方。Activiti的候选人可以是单个用户assignee也可以是候选组candidateGroup。Shiro的角色体系里一个用户可以有多个角色。那么问题来了流程定义时写的候选组到底对应Shiro的哪个概念我的方案是在Shiro里维护一套“业务角色”这套角色和Activiti的候选组一一对应。比如流程里写candidateGroupsdept_manager那Shiro里就有一个角色叫dept_manager。用户被赋予这个角色后就能看到对应的待办任务。查询待办的时候不能直接用taskService.createTaskQuery().taskCandidateUser(username)因为这样查不到候选组的任务。正确的姿势是先把用户的所有角色查出来然后ListString roleCodes shiroService.getRoleCodesByUser(username); ListTask tasks taskService.createTaskQuery() .taskCandidateUser(username) .taskCandidateGroupIn(roleCodes) .orderByTaskCreateTime().desc() .list();提示taskCandidateUser和taskCandidateGroupIn是OR关系不是AND所以这样写能同时查到个人任务和角色任务。3.3 任务监听器中的权限校验流程流转到某个节点时有时候需要动态判断审批人。比如金额小于1万走部门经理大于1万走总监。这种逻辑不要写在流程定义里而是通过任务监听器在运行时动态设置。我通常实现一个TaskListener在notify方法里根据流程变量判断然后调用delegateTask.addCandidateGroup()或者delegateTask.setAssignee()。这里要注意监听器里不要做太重的操作比如查数据库、调远程接口因为监听器是在流程引擎的事务里执行的耗时太长会拖垮整个流程。public class DynamicAssigneeListener implements TaskListener { Override public void notify(DelegateTask delegateTask) { Double amount (Double) delegateTask.getVariable(amount); if (amount ! null amount 10000) { delegateTask.addCandidateGroup(director); } else { delegateTask.addCandidateGroup(dept_manager); } } }4. 完整实操流程与关键环节实现4.1 工程搭建与基础配置新建SpringBoot工程pom里引入web、mybatis、mysql、shiro-spring-boot-web-starter、activiti-spring-boot-starter-basic。application.yml里配置数据源和Activiti的自动建表策略spring: datasource: url: jdbc:mysql://localhost:3306/workflow_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 activiti: database-schema-update: true check-process-definitions: true process-definition-location-prefix: classpath:/processes/ history-level: fullhistory-level设为full是为了能查历史审批记录如果只设为audit有些变量查不到。database-schema-update设为true会自动建表生产环境建议设为false用SQL脚本手动初始化。4.2 流程定义文件的编写要点在resources/processes目录下放bpmn文件。我习惯用Eclipse或者IDEA的Activiti插件画流程图但生成的XML要检查几个地方userTask的activiti:candidateGroups属性、sequenceFlow的条件表达式、以及startEvent的formKey。一个典型的请假流程bpmn片段userTask iddeptApprove name部门经理审批 activiti:candidateGroupsdept_manager extensionElements activiti:taskListener eventcreate classcom.demo.listener.DynamicAssigneeListener/ /extensionElements /userTask注意bpmn文件里的中文名称要确保文件编码是UTF-8否则部署时会报XML解析错误。4.3 业务表与流程的关联实现业务表设计一个leave_apply表字段包括id、user_id、days、reason、status、process_instance_id、create_time。status字段用来做列表展示实际的状态以Activiti的流程实例为准。发起流程的代码Transactional public void startLeaveProcess(LeaveApply apply, String username) { // 1. 保存业务数据 apply.setStatus(审批中); leaveApplyMapper.insert(apply); // 2. 设置流程变量 MapString, Object variables new HashMap(); variables.put(applyUser, username); variables.put(days, apply.getDays()); variables.put(amount, apply.getAmount()); // 3. 启动流程实例businessKey用业务表主键 String businessKey String.valueOf(apply.getId()); ProcessInstance instance runtimeService.startProcessInstanceByKey( leaveProcess, businessKey, variables); // 4. 回写流程实例ID apply.setProcessInstanceId(instance.getId()); leaveApplyMapper.updateById(apply); }这里Transactional是必须的保证业务数据和流程实例同时成功或同时失败。businessKey用业务表主键这样从任务反查业务数据时直接businessKey就能拿到。4.4 待办任务与已办任务的查询实现待办查询前面已经讲了核心是候选人候选组的OR查询。已办任务查询用historyService.createHistoricTaskInstanceQuery().taskAssignee(username).finished()。这里有个性能优化点如果历史任务很多不要一次性查出来要分页。Activiti的查询API支持listPage(offset, limit)。我一般会封装一个TaskQueryService把待办、已办、流程跟踪都封装成方法。流程跟踪用historyService.createHistoricActivityInstanceQuery().processInstanceId(pid).orderByHistoricActivityInstanceStartTime().asc()这样能拿到完整的流转路径前端画个时间轴就能展示。4.5 审批操作的完整实现审批通过Transactional public void approve(String taskId, String comment, String username) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if (task null) { throw new RuntimeException(任务不存在或已处理); } // 添加审批意见 taskService.addComment(taskId, task.getProcessInstanceId(), comment); // 完成任务 taskService.complete(taskId); // 更新业务状态 updateBusinessStatus(task.getProcessInstanceId()); }驳回稍微复杂一点需要先找到目标节点然后用runtimeService.createChangeActivityStateBuilder().moveActivityIdTo(currentActId, targetActId).changeState()。这个API是Activiti 6才有的5.x版本要用其他方式实现。5. 常见问题排查与避坑经验实录5.1 任务查不到候选人问题这是最高频的问题。现象是流程启动了但待办列表是空的。排查步骤先查ACT_RU_TASK表看ASSIGNEE_和CANDIDATE_字段有没有值。如果ASSIGNEE_为空且候选组也没设置那就是流程定义里没写候选人。如果候选组设置了但查不到检查Shiro里用户的角色编码和流程里的候选组编码是否完全一致大小写敏感。还有一个隐蔽的坑taskCandidateGroupIn传的是角色编码列表但如果用户有角色但角色编码是中文Activiti查询时会因为编码问题查不到。所以角色编码一律用英文。5.2 流程变量类型不匹配问题Activiti的变量是强类型的。如果你在启动时put了一个Integer在监听器里用getVariable(amount)强转成Double会抛ClassCastException。我的经验是金额类变量统一用Double数量类统一用Integer字符串统一用String在流程定义的条件表达式里也要对应类型。条件表达式写错也是常见问题。比如${amount 10000}如果amount是String类型比较结果可能不符合预期。所以变量类型一定要在启动流程前就确定好。5.3 事务与异步执行的冲突Activiti的异步任务比如定时边界事件默认是在独立事务里执行的这时候Shiro的SecurityManager可能拿不到当前用户上下文。如果异步任务里需要用户信息要在流程变量里把用户信息传进去而不是从Shiro的ThreadLocal里取。另外Transactional和Activiti的complete方法一起用时要注意事务传播级别。如果业务方法本身有事务complete会加入当前事务这是对的。但如果业务方法没有事务complete会自己开一个事务这时候如果后续业务操作失败流程已经流转了数据就不一致了。5.4 常见问题速查表问题现象可能原因排查方法解决方案待办列表为空候选人/候选组未设置查ACT_RU_TASK表检查bpmn定义和监听器流程启动报错流程定义未部署查ACT_RE_PROCDEF表检查processes目录和部署配置审批后状态未更新事务未生效检查Transactional注解确保业务方法和complete在同一事务历史记录查不到history-level配置过低查ACT_HI_ACTINST表改为full级别候选人查询慢候选组过多看SQL执行计划加索引或改用assignee5.5 独家避坑技巧第一个技巧流程定义文件不要频繁改ID。流程定义的key一旦上线就不要改改了会导致历史流程实例关联不上。如果非要改用版本控制Activiti会自动生成新版本。第二个技巧审批意见用addComment而不是setVariable。addComment会存到ACT_HI_COMMENT表和任务关联查询方便。setVariable存到变量表数据量大了查询慢。第三个技巧前端传taskId时要做权限校验。不能只靠前端隐藏按钮后端在complete之前要校验当前用户是否有权限处理这个任务。校验逻辑是查任务的候选人和候选组判断当前用户是否在其中。private void checkTaskPermission(String taskId, String username) { Task task taskService.createTaskQuery().taskId(taskId).singleResult(); if (task null) { throw new RuntimeException(任务不存在); } if (username.equals(task.getAssignee())) { return; } ListString roleCodes shiroService.getRoleCodesByUser(username); ListIdentityLink links taskService.getIdentityLinksForTask(taskId); boolean hasPermission links.stream() .anyMatch(link - candidate.equals(link.getType()) (username.equals(link.getUserId()) || roleCodes.contains(link.getGroupId()))); if (!hasPermission) { throw new RuntimeException(无权处理该任务); } }这个校验看起来简单但能挡住大部分越权操作。我见过有系统因为没做这个校验普通用户通过改URL就能审批总监节点的任务这是很严重的安全漏洞。6. 权限缓存与流程性能优化6.1 Shiro缓存配置Shiro默认每次授权都会查数据库用户一多数据库压力很大。必须配缓存。我用的是EhCache在ShiroConfig里设置securityManager.setCacheManager(new EhCacheManager())。缓存的是AuthorizationInfo也就是用户的角色和权限列表。用户角色变更时要手动清缓存调用shiroService.clearAuthorizationCache(username)。Activiti这边也有缓存主要是流程定义的缓存。默认是开启的不用额外配置。但如果流程定义文件很大部署时会慢这是正常的。6.2 待办查询的性能优化待办查询是最高频的操作优化空间很大。第一给ACT_RU_TASK表的ASSIGNEE_和CANDIDATE_字段加索引。第二如果候选组很多考虑用taskCandidateGroupIn的时候限制数量或者改用自定义SQL直接查。第三待办列表不要查全量字段只查需要的。我实测过一个场景5000个用户每人平均20个待办用Activiti原生API查询平均耗时200ms加了索引后降到50ms。如果还嫌慢可以把待办数据同步到Redis里用空间换时间。6.3 历史数据归档策略ACT_HI_开头的表会随着流程运行不断增长。我的做法是每季度归档一次把已完成超过半年的流程实例数据迁移到历史归档表然后从Activiti的历史表里删除。删除的时候要注意外键关联先删ACT_HI_DETAIL再删ACT_HI_COMMENT最后删ACT_HI_PROCINST。归档脚本用存储过程或者定时任务实现不要手动删容易删出问题。归档前一定要备份这是铁律。7. 项目扩展方向与个人实操体会这套骨架搭好之后扩展方向其实很多。比如加个流程设计器让业务人员自己画流程图前端用bpmn-js后端提供部署接口。再比如加个消息通知任务创建时发邮件或者站内信用TaskListener的create事件触发。还可以加流程监控用Activiti的historyService查所有运行中的流程实例统计每个节点的平均耗时找出瓶颈节点。我在实际项目里踩过最大的坑是流程定义和业务代码的耦合。一开始我把业务逻辑写在Service里流程流转也写在Service里后来流程一改Service就要跟着改。后来我把流程相关的操作全部抽到ProcessService里业务Service只负责业务数据两者通过businessKey关联改流程的时候业务代码基本不用动。还有一个体会是不要试图用工作流解决所有问题。有些简单的状态流转用状态机就够了上工作流反而增加复杂度。工作流适合节点多、审批人动态、需要历史追溯的场景。选型的时候要克制不要为了用而用。最后分享一个小技巧Activiti的runtimeService.getVariables()返回的是Map如果流程变量很多每次查都全量返回性能不好。可以用getVariablesLocal()只查当前节点的局部变量或者用getVariable()单个查。这个细节在变量多的流程里能省不少时间。
延伸阅读

更多相关文章

2026/10/12 13:52:02

计算机二级C语言程序修改题:高频错误类型与考场排查技巧

1. 从一道程序修改题说起:为什么这类题总让人栽跟头计算机二级C语言考试里,程序修改题是很多人的心头痛。选择题可以蒙,填空题可以猜,但程序修改题摆在那里,代码一行行看过去,明明感觉没什么问题&#xff0…

2026/10/12 13:52:02

手写朴素贝叶斯实现豆瓣影评情感分类:从数据清洗到模型评估

简介:面向高校程序设计课程设计场景,这份压缩包围绕豆瓣影评情感分类任务,提供基于朴素贝叶斯算法的完整Python实现方案,覆盖从原理学习到编码实践的完整链路。资源共10个文件,包含4个Python脚本、1个Jupyter notebook…

2026/10/12 13:47:01

Python深度学习图像处理最小可行系统:工业级分类检测部署

简介:本资源是一套基于Python与PyTorch实现的深度学习图像处理完整源码工程,面向计算机视觉初学者、算法工程师及高校研究者,聚焦图像分类、目标检测与语义分割等核心任务,提供从模型训练到服务部署的全链路实践参考。压缩包共436…

2026/10/12 14:57:15

基于SSM的社区养老信息App开发指南与答辩要点

以下是根据你的标题和内容要求生成的博文正文,直接以 Markdown 格式输出。请查收。 1. 先说清楚:这类"社区养老信息App"到底在做什么 每年到了三四月份,都会有一批计算机专业的学生开始为毕业设计发愁。如果你打开过选题清单&…

2026/10/12 14:57:15

偏振图像去噪实战:Python资源与配套数据集详解

简介:面向计算机视觉与图像处理研究者的偏振图像去噪Python项目,重点解决强反光、眩光场景下偏振数据的噪声抑制与细节恢复问题。资源共38个文件,压缩包仅906KB,其中包含15个Python源码、14个编译后的pyc缓存、5张数据集示例图以及…

2026/10/12 14:57:15

Python实现图书推荐系统:协同过滤与文本相似度实战

简介:这份资源是一套基于协同过滤与文本相似度算法的Python图书推荐系统完整源码,面向计算机、软件工程等专业需要完成课程设计、毕业设计或推荐算法入门实践的学生与开发者,帮助解决推荐系统从算法到前后端落地的完整实现问题。压缩包共1828…

2026/10/12 14:57:15

wx-ipad协议8049双端适配:Linux与Windows跨平台构建实战

简介:该资源为微信iPad协议8049的Linux与Windows双版本实现,面向需要对接微信通信协议、搭建服务端接口的开发者与运维人员。包内共27个文件,以js脚本、map映射、conf配置、html页面及png图标为主,另含exe可执行程序、yml接口描述…

2026/10/12 14:57:15

2026年安全运维拐点:AI原生安全与行为基线分析实战

2025年做安全运维的时候,我最大的感受是:告警越来越不像人制造的,更像是一台不知疲倦的机器在试探我的边界。几个大项目一跑,很多团队的负责人都在问同一个问题——2026年我们到底该往哪儿投钱、往哪儿堆人?这篇东西不…

2026/10/12 14:52:12

Java开发者必看:LangChain4j大模型应用开发入门实战

1. 为什么Java开发者需要一个专属的LLM框架先说个现象:这半年陆续有几个做Java后端的朋友问我,说他们想在自己的项目里接入大模型能力,结果一搜教程全是Python。什么LangChain、LlamaIndex,Demo跑起来倒是热闹,可真要落…

2026/10/11 0:02:13

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

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

2026/10/12 12:01:26

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

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

2026/10/12 12:54:07

Flutter StatefulWidget 生命周期核心解析

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

2026/10/12 0:04:22

绝缘子缺陷检测数据集清洗与工业级训练实战指南

简介:本资源是面向电力AI研发人员、工业视觉工程师及智能巡检系统开发者的绝缘子缺陷检测专用YOLO格式数据集,解决无人机航拍场景下绝缘子破损、污闪、积雪等9类典型缺陷的精准识别与定位难题。数据集共2139张真实巡检图像(含训练/验证/测试集…

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

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

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