做若依框架集成 Activiti 7 工作流的时候,我一度以为问题会出在流程引擎的表结构或事务配置上。结果真正的拦路虎是 Spring Security——明明项目里只有若依自己的一套权限体系,可一启动就冒出两个 Security 自动配置类互相抢地盘,启动日志红成一片。排查到最后,问题其实很单纯:只需要在启动类上多加一个exclude,把 Activiti 自带的 Security 自动配置类挡在门外。这篇文章就把完整的排查过程、底层原理,以及排除之后如何把若依登录用户桥接给流程引擎,一次讲清楚。如果你是做若依 + Activiti 集成、又恰好被自动配置冲突折磨的开发者,这篇应该能直接帮你落地。
1. 先看现场:一启动就报 Security 冲突
1.1 第一次启动时的异常堆栈
我在若依前后端分离版(RuoYi-Vue3 分支)基础上引入activiti-spring-boot-starter,刚把依赖加完,启动项目,不到三秒就抛了下面这段错误:
*************************** APPLICATION FAILED TO START *************************** Description: The bean 'springSecurityFilterChain', defined in class path resource [org/activiti/spring/security/ActivitiSpringSecurityAutoConfiguration.class], could not be registered. A bean with that name has already been defined in file [com/ruoyi/framework/config/SecurityConfig.class] and overriding is disabled.这段报错非常典型。springSecurityFilterChain这个名字出现了两次:一次来自若依自定义的SecurityConfig,另一次来自 Activiti 的ActivitiSpringSecurityAutoConfiguration自动配置类。Spring Boot 2.1 以后默认把allow-bean-definition-overriding设为false,也就是说,容器中不允许出现两个同名 Bean,一旦重复定义直接启动失败。
如果只是这一种报错还好。实际排查时,很多人遇到的还有:
Parameter 0 of method activitiSecurityManager in org.activiti.spring.security.ActivitiSpringSecurityAutoConfiguration required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.甚至其他UnsatisfiedDependencyException、循环依赖报错。表面现象各不相同,根子却都指向同一个地方:两套权限配置同时生效,插件在抢同一批 Bean 定义。
1.2 不是所有 Security 报错都要改业务代码
遇到这类报错,第一反应不应该是去改若依的SecurityConfig,也不要在application.yml里把allow-bean-definition-overriding直接打开。后者看着能启动,其实是把冲突掩盖了,后续流程引擎在获取当前用户时大概率还会出幺蛾子。
正确的思路是先搞明白:到底是谁重复装配了 Security?为什么若依已经有一套权限认证,Activiti 还要再自动配置一套?带着这个疑问去看自动配置类,问题就会变得很清楚。
2. 冲突的源头:两套 Security 自动配置同时抢地盘
2.1 若依的权限模型是怎么工作的
若依底层依赖 Spring Security,但它不是简单地用默认配置,而是做了一套无状态 JWT 认证:
- 用户登录后,后端通过
TokenService生成token,返回给前端。 - 前端后续请求把 token 放在请求头里,后端通过过滤器解析 token。
- 解析成功后在当前线程上下文中放入
LoginUser,业务代码通过SecurityUtils.getLoginUser()获取当前用户信息。 - 若依的
SecurityConfig定义了一个SecurityFilterChain,配置了哪些路径匿名访问、哪些路径需要认证,同时注册了自定义的AuthenticationEntryPoint和AccessDeniedHandler。
在若依这套体系里,用户、角色、菜单权限全部由数据库维护,权限编码已经写得很完善。框架的SecurityConfig本身就是整个权限体系的配置中心,它在项目里存在时,容器中应该有且仅有这一套过滤器链定义。
2.2 Activiti 7 的 Security 自动配置想要做什么
Activiti 7 在设计时就默认“找 Spring Security 要当前用户”。流程引擎在创建流程实例、完成审批任务时,需要知道当前操作人是谁,然后把这个用户 ID 写入流程变量或任务表。
为了开箱即用,Activiti 提供了activiti-spring-boot-starter,里面包含了自动配置类。它会尝试:
- 从 Spring Security 的
Authentication中读取当前登录用户名。 - 装配一个
SecurityManagerBean,供流程引擎的鉴权模块使用。 - 注册一个
springSecurityFilterChain,试图为流程引擎的在线用户体系服务。
听起来很好,问题是:若依已经配置好了一套更完整的 Security 过滤器链。两套配置同时落进 Spring 容器,要么同名 Bean 冲突,要么条件注解判断顺序出问题,导致其中一个配置把另一个覆盖掉。最终结果就是启动失败,或者启动成功但权限行为变得不可预测。
2.3 冲突的本质:自动配置没有“只保留一套”的约定
Spring Boot 的自动配置大量依赖@ConditionalOnMissingBean之类的条件注解:当项目中已经存在某个 Bean 时,自动配置就不该重复创建。但是,不同自动配置类对“是否已经存在”的判定粒度并不一样。
若依的SecurityConfig中定义的SecurityFilterChainBean 名是固定的;Activiti 的自动配置类里也定义了一个同名 Bean,而且它并不判断用户是否已经有了更完整的 SecurityFilterChain。结果就是两个定义同时存在,Spring 容器炸锅。
这就像公司本来只有一个行政负责排班表,新来的系统集成商又自动生成了一份排班表,两份都写“以我为准”。要解决,只能人为拿掉其中一份,明确告诉 Spring Boot:Activiti 的 Security 默认装配不需要启动。
3. 用 exclude 精准拆雷:排除哪个类、怎么排
3.1 先定位自动配置类的准确类名
在动手排除之前,必须找到 Activiti 项目里实际的 Security 自动配置类全限定名。不同版本差异很大,千万不要照抄一个类名就完事。
常见情况是:
- Activiti 7.x 中,自动配置类通常是
org.activiti.spring.security.ActivitiSpringSecurityAutoConfiguration。 - Activiti 5/6 时代,可能是
org.activiti.spring.boot.SecurityAutoConfiguration。 - 某些早期版本还可能是
org.activiti.spring.security.SecurityAutoConfiguration。
准确查找方式有两个:
第一,在 IDEA 里使用Ctrl + N全局搜索类名,直接输入SecurityAutoConfiguration,看命中的类里哪些在activiti包下。
第二,进入本地 Maven 仓库,找到activiti-spring-boot-autoconfigure-*.jar,解压后查看:
META-INF/spring.factories如果是 Spring Boot 2.7+,则看:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports里面会列出所有自动配置类的全限定名,看到含Security的项,就是我们要找的对象。
3.2 方式一:在启动类上使用 exclude 参数
若依的前后端分离版本,启动类一般在ruoyi-admin模块下,类名可能是RuoYiApplication或RuoYiAdminApplication。修改如下:
import org.activiti.spring.security.ActivitiSpringSecurityAutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication( exclude = { ActivitiSpringSecurityAutoConfiguration.class } ) public class RuoYiApplication { public static void main(String[] args) { SpringApplication.run(RuoYiApplication.class, args); } }这里用的是类字面量,编译期就能校验类是否存在,比字符串配置安全。排除后,Spring Boot 在自动配置阶段会把ActivitiSpringSecurityAutoConfiguration从候选列表中移除,不会再执行它内部定义的各种 Bean 注入和 Filter 注册。
3.3 方式二:在 application.yml 中配置 spring.autoconfigure.exclude
如果启动类被框架代码包住,不方便改,或者想在多个模块里统一处理,可以在application.yml里写:
spring: autoconfigure: exclude: - org.activiti.spring.security.ActivitiSpringSecurityAutoConfiguration这种方式的优点是零代码侵入,缺点是类名是字符串,写错了不会编译报错,只能等到启动时看异常。建议两边都检查一下,确保字符串和 jar 包里的全限定名一字不差。
如果用的是若依微服务版本,多个服务模块可能共用一份配置中心中的application.yml,那可以直接把这项配置放到配置中心公共配置里,所有服务一起生效,省得每个服务重复改。
3.4 千万注意:不要排错对象
有人排错类名,把 Spring Boot 自己的自动配置也排掉了:
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration如果排除的是这个类,若依的 Security 基础环境就没了,项目倒是能启动,但登录、认证、权限拦截全部失效,请求会在过滤链外裸奔。一定要看仔细,我们排除的是org.activiti.*包下的 Security 自动配置,不是 Spring Boot 框架自身的。
排除完成后,再次启动,之前的springSecurityFilterChain重复定义错误会消失,项目可以正常进入启动流程。
4. 排除不是终点:把若依当前用户桥接给 Activiti
4.1 为什么还要手工桥接
exclude解决的是“重复配置”的矛盾。但问题也随之而来:Activiti 原来靠自动配置里的SecurityManager去读 Spring Security 上下文,现在这个默认实现被排除了,流程引擎不知道该从哪里获取当前操作人。
如果不去管这一步,你很快会遇到新的问题:
- 发起一个流程实例,流程实例表里的
START_USER_ID为空。 - 任务列表里查不到当前登录人的待办任务。
- 流程引擎在调用
UserTask候选人表达式时取不到用户,导致审批人匹配失败。
因为 Activiti 并不会自己记住“若依的SecurityUtils.getLoginUser()是什么”,它需要实现者提供一个桥接器,让它能按统一的接口去拿当前登录人。
4.2 实现自定义 SecurityManager
Activiti 提供了一套用户身份接口,核心是org.activiti.api.runtime.shared.security.SecurityManager。我们只要实现它,并注册成 Spring Bean,流程引擎就会优先使用我们的实现。
下面这段是连接若依登录用户的核心代码:
package com.ruoyi.framework.config; import com.ruoyi.common.utils.SecurityUtils; import org.activiti.api.runtime.shared.identity.UserGroupManager; import org.activiti.api.runtime.shared.security.SecurityManager; import org.springframework.stereotype.Component; import java.util.List; @Component public class ActivitiSecurityManager implements SecurityManager { private final UserGroupManager userGroupManager; public ActivitiSecurityManager(UserGroupManager userGroupManager) { this.userGroupManager = userGroupManager; } @Override public String getAuthenticatedUserId() { // 若依工具类在未登录场景下会返回 null,必须判空 if (SecurityUtils.getLoginUser() == null) { return null; } return SecurityUtils.getUsername(); } @Override public List<String> getAuthenticatedUserGroups() { String userId = getAuthenticatedUserId(); if (userId == null) { return List.of(); } return userGroupManager.getUserGroups(userId); } }这段代码的关键点在于:它不再依赖 Activiti 默认的 SecurityContext 解析,而是直接调用若依自己的静态方法。只要当前请求经过了若依的 JWT 认证过滤器,SecurityUtils.getLoginUser()就能拿到用户信息,流程引擎也就能通过这个接口拿到当前操作人。
4.3 用户组映射方案
若依中的“角色”和 Activiti 的“用户组”天然对应。最简单的做法是实现UserGroupManager接口,把若依的角色 key 返回给 Activiti:
package com.ruoyi.framework.config; import com.ruoyi.common.core.domain.model.SysUser; import com.ruoyi.system.service.ISysUserService; import org.activiti.api.runtime.shared.identity.UserGroupManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @Component public class ActivitiUserGroupManager implements UserGroupManager { @Autowired private ISysUserService userService; @Override public List<String> getUserGroups(String username) { SysUser user = userService.selectUserByUserName(username); if (user == null || user.getRoles() == null) { return Collections.emptyList(); } return user.getRoles().stream() .map(role -> role.getRoleKey()) .collect(Collectors.toList()); } @Override public List<String> getUserRoles(String username) { return getUserGroups(username); } }这里要注意一个坑:若依查询用户时,默认可能不会把角色列表带出来。如果你的selectUserByUserName没有关联用户角色表,需要改用selectUserByUserName后手动查询角色,或者直接调ISysRoleService.selectRolesByUserId。
角色 key 与 Activiti 权限配置对应起来之后,在 BPMN 设计里,可以把用户任务的候选人组直接写成角色 key,比如${groupManager.getUserGroups(execution)}或者直接使用 Activiti 的候选人组配置,和若依角色体系打通。
4.4 未登录场景必须兜底
在后台定时任务、消息队列消费者、或第三方接口回调里调用流程引擎服务时,可能并不存在登录用户。此时getAuthenticatedUserId()返回null是正常的。
建议在需要当前用户的业务方法里主动判断:
String userId = activitiSecurityManager.getAuthenticatedUserId(); if (userId == null) { throw new ServiceException("当前操作人未登录,无法发起或处理流程"); }千万不要让null一路传到流程引擎内部,否则等到流程启动到一半才发现找不到用户,排查成本会大很多。
5. 完整实操记录:从报错到跑通
5.1 实验环境与依赖版本
我这次实际操作的环境如下:
| 组件 | 版本 |
|---|---|
| 若依前后端分离版 | RuoYi-Vue3,基于 Spring Boot 2.7.6 |
| Activiti | 7.1.0.M6 |
| 数据库 | MySQL 8.0.32 |
| JDK | 1.8 |
| Maven | 3.8.x |
在ruoyi-admin/pom.xml中添加核心依赖:
<dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency>5.2 连续排错过程记录
第一轮,我删掉了自动配置类,项目勉强能启动,但调接口发起流程时控制台出现这样一段日志:
Failed to get authenticated user原因就是不桥接当前用户,Activiti 内部拿不到登录人。于是按照上一节的方法实现了ActivitiSecurityManager,再重启。
第二轮启动后,发起流程接口能正常返回了,我去查ACT_RU_TASK和ACT_HI_PROCINST,发现流程实例的发起人已经变成了若依登录用户名,待办任务也关联到了当前登录人。
第三轮验证整个闭环流程:用测试账号登录若依,点击发起审批,然后切换到另一个有审批权限的账号,在待办列表里能查到这条任务,点击审批,流程正常向下流转,历史表中留下了完整操作记录。
完整配置整理成三个关键点:
- 启动类加入
exclude排除ActivitiSpringSecurityAutoConfiguration。 - 自定义
SecurityManager实现若依用户读取。 - 自定义
UserGroupManager实现角色到用户组映射。
这三步做完,若依环境和 Activiti 基本能稳定共存。
5.3 若依微服务版本要注意什么
如果你用的是若依微服务版(RuoYi-Cloud),同样的一套思路适用,但位置有变化:
- 启动类不是单一
RuoYiApplication,而是各个业务服务模块自己的XxxApplication,需要在用到工作流的服务上统一加exclude。 SecurityManager的实现应该放在公共的ruoyi-common-security或独立的workflow模块中,避免重复代码。- 如果服务之间通过 Feign 调用,远程请求并不会自动携带当前用户的 SecurityContext,需要额外传递用户 ID 参数,不能依赖服务端
SecurityUtils直接取。
微服务环境下,排除逻辑本身和单机版没有区别,难点是用户上下文如何在服务间传播。
6. 常见问题速查表与避坑建议
6.1 典型异常与解法速查表
| 异常信息 | 可能原因 | 解决办法 |
|---|---|---|
The bean 'springSecurityFilterChain' could not be registered | Activiti 与若依的 SecurityFilterChain 重名 | 在启动类或 yml 中排除 Activiti 的 Security 自动配置类 |
required a bean of type 'AuthenticationManager' that could not be found | Activiti 自动配置期望容器中的 AuthenticationManager 被移除或不可用 | 排除自动配置类,并自定义 SecurityManager 桥接若依用户 |
Failed to get authenticated user | 排除了默认 SecurityManager,但没有自定义实现 | 实现SecurityManager接口,返回若依当前登录用户名 |
| 任务列表为空,流程实例创建人为 NULL | 未做用户桥接,Activiti 无法读取当前人 | 注册自定义 SecurityManager,并在登录后发起流程 |
| 候选人组匹配不上 | 若依角色未映射为 Activiti 用户组 | 实现UserGroupManager,查询若依角色 key 并返回 |
| 整个接口直接 401 / 403 | 误排除了 Spring Boot 自己的 SecurityAutoConfiguration | 检查 exclude 配置,只排除org.activiti.*下的类 |
ClassNotFound: ActivitiSpringSecurityAutoConfiguration | 类名或包名与当前 Activiti 版本不一致 | 进入 jar 包查看spring.factories或AutoConfiguration.imports中真实类名 |
6.2 几条实用的避坑习惯
首先,不要为了省事全局打开allow-bean-definition-overriding。我见过有同事在application.yml里设置spring.main.allow-bean-definition-overriding=true,项目确实能启动,但同一个 Bean 被两个配置类各定义一次,具体运行时用哪一个完全取决于加载顺序,这就像抽奖,今天能跑明天可能就出诡异问题。用exclude把没有意义的自动配置直接移除,才是可控的做法。
其次,排除类名要跟着依赖版本走。Activiti 6、7 之间自动配置类路径变化很大,搜索时直接在 IDEA 里看反编译源码,比自己猜包名可靠得多。每次升级 Activiti 版本后,也建议重新确认一下自动配置类路径是否变化,避免排除失效。
最后,自定义 SecurityManager 的代码要简单直接。接口里只有两个方法,不要在这个类里塞复杂的业务逻辑。如果未来若依升级了SecurityUtils的获取方式,这里就是唯一需要修改的地方,集中管理能减少维护成本。
把exclude和用户桥接的逻辑沉淀成公共模块之后,若依项目里再接入工作流功能就轻快多了。我个人的体会是:Spring Boot 的自动配置本身是好设计,但不同框架对同一安全域名的预设有重叠,必须明确告诉容器“我自有安排,你的默认配置不要动”。一个exclude,看着只是多了一行注解,背后却省掉了无数个运行时才暴露的隐性坑。