1. 为什么你的Camunda集成总被拦截?一个真实项目的权限冲突剖析
很多朋友在SpringBoot项目里集成Camunda时,都会遇到一个让人头疼的问题:明明按照官方文档配置好了,启动项目后访问http://localhost:8080/camunda/app,结果要么是404,要么直接跳到了自己系统的登录页。我刚开始做集成时也踩过这个坑,折腾了大半天才发现问题根源。
其实这个问题的本质很简单:你的业务系统有自己的权限拦截器,而Camunda的Web界面和REST接口也被这个拦截器给拦住了。想象一下,你家的门卫不认识Camunda这位客人,自然就不让他进门。Camunda默认是没有任何权限控制的,所有接口都是开放的,但你的Spring Security或者Shiro配置里,通常会有类似/**这样的拦截规则,把/camunda/**和/engine-rest/**这些路径也一并拦截了。
我遇到过最典型的情况是,团队用了某个开源框架作为基础,比如小诺(xiaonuo)或者若依(RuoYi),这些框架自带的权限拦截非常完善,但也非常“霸道”。它们会检查每个请求的Session里有没有登录用户,没有就直接重定向到登录页。而Camunda的Web界面初次访问时,显然没有你业务系统的Session,于是就被无情地赶走了。
更麻烦的是,即使你放行了Camunda的路径,另一个问题又来了:Camunda自己变成了“裸奔”状态。因为你只是让门卫放行了这位客人,但没给这位客人自己家的门加锁。任何知道Camunda控制台地址的人都能直接访问,查看所有流程、操作所有任务,这在生产环境是绝对不允许的。所以,我们需要做两件事:第一,让我们系统的门卫对Camunda“网开一面”;第二,给Camunda这位客人自己装上独立的安全锁。下面我们就一步步来解决。
2. 基础集成:三分钟让Camunda在SpringBoot里跑起来
我们先从最简单的开始,确保Camunda能正常集成到你的SpringBoot项目里。这里我以Camunda 7.19.0版本为例(写这篇文章时的稳定版),但原理对7.x系列都通用。
首先,在你的pom.xml里加入这几个核心依赖。别一股脑全加,看清楚每个是干嘛的:
<!-- Camunda核心启动器,必须的 -->
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter</artifactId>
<version>7.19.0</version>
</dependency>
<!-- 如果你需要Camunda自带的Web管理界面(Cockpit、Tasklist等),加上这个 -->
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
<version>7.19.0</version>
</dependency>
<!-- 如果你需要通过REST API操作引擎,加上这个 -->
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
<version>7.19.0</version>
</dependency>
<!-- 用于JSON/XML等数据格式处理,建议加上 -->
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-engine-plugin-spin</artifactId>
<version>7.19.0</version>
</dependency>
<dependency>
<groupId>org.camunda.spin</groupId>
<artifactId>camunda-spin-dataformat-json-jackson</artifactId>
<version>1.18.0</version>
</dependency>
加完依赖,在application.yml里做最基础的配置:
camunda:
bpm:
admin-user:
id: admin # Camunda默认管理员账号
password: admin # 生产环境一定要改!
filter:
create: All # 非常重要!这行让SpringBoot自动创建Camunda的过滤器
spring:
datasource:
url: jdbc:mysql://localhost:3306/camunda?useUnicode=true&characterEncoding=utf8
username: your_db_user
password: your_db_password
driver-class-name: com.mysql.cj.jdbc.Driver
配置完启动项目,如果没意外,Camunda会自动创建几十张表在你的数据库里。这时候你访问http://localhost:8080/camunda/app,应该能看到Camunda的登录界面了。用上面配置的admin/admin就能登录进去。但注意,这时候Camunda是“裸奔”的,没有任何权限控制,谁拿到这个地址都能登录操作,这显然不行。
提示:如果你用的是内存数据库比如H2,Camunda也能跑起来,但生产环境强烈建议用MySQL、PostgreSQL这类持久化数据库。第一次启动时Camunda会自动建表,这个过程可能需要几十秒,耐心等一下。
3. 解决第一道门禁:让你的业务系统放行Camunda
现在来解决第一个问题——你业务系统的权限拦截。不同的框架配置方式不同,我以最常见的Spring Security和基于拦截器的权限框架为例,告诉你该怎么放行。
如果你用的是Spring Security,配置相对简单。在你的Security配置类(通常叫SecurityConfig)里,找到configure(HttpSecurity http)方法,添加对Camunda路径的放行:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 放行Camunda的静态资源和API接口
.antMatchers("/camunda/app/**", "/camunda/api/**", "/camunda/lib/**").permitAll()
.antMatchers("/engine-rest/**").permitAll() // 放行REST API
.antMatchers("/forms/**").permitAll() // 放行表单引擎
// 其他业务路径按需配置
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
如果你用的是类似小诺(xiaonuo)这种基于拦截器的权限框架,情况稍微复杂点。这类框架通常有一个核心的拦截器配置,你需要找到配置拦截规则的地方。比如在小诺框架里,你可能会在WebMvcConfig或者类似的配置类里看到这样的代码:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 原有的拦截器配置
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/logout", "/captcha");
// 你需要在这里加上对Camunda路径的排除
registry.addInterceptor(authInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/login", "/logout", "/captcha",
"/camunda/**", // 放行所有Camunda Web路径
"/engine-rest/**", // 放行REST API
"/forms/**" // 放行表单相关
);
}
}
还有一种情况,你的拦截器可能是通过Filter实现的。这时候你需要修改web.xml或者FilterRegistrationBean的配置。我遇到过最隐蔽的一种情况是:框架的权限判断逻辑写在了Filter里,而且判断请求头Authorization的方式比较粗暴。比如有些框架看到Authorization头就认为是自己的Token,但Camunda的REST接口用的是Basic认证,格式是Basic Base64编码,这就会导致冲突。这时候你可能需要修改拦截器的逻辑,让它能区分这两种认证方式。
做完这一步,重启项目,再次访问http://localhost:8080/camunda/app,应该能正常看到Camunda的登录页了。但别高兴太早,这只是过了第一关。
4. Camunda原生权限体系深度解析:不只是用户名密码那么简单
很多人以为Camunda的权限就是登录时的用户名密码验证,其实远不止这么简单。Camunda有一套完整的、基于资源的权限体系,理解这个体系是做好权限集成的关键。
Camunda把权限分成了两个层面:认证(Authentication)和授权(Authorization)。认证解决“你是谁”的问题,就是验证用户名密码对不对。授权解决“你能干什么”的问题,就是验证这个用户有没有权限执行某个操作。
在数据库层面,Camunda用几张核心表来管理权限:
ACT_ID_USER:用户表,存用户名、密码(加密过的)、邮箱等ACT_ID_GROUP:用户组表,可以理解成角色ACT_ID_MEMBERSHIP:用户和组的关联表ACT_RU_AUTHORIZATION:运行时授权表,这里记录了谁对什么资源有什么权限
Camunda内置了多种资源类型,每种都有对应的数字编码。我从源码里扒出来的完整列表是这样的:
| 资源类型 | 编码 | 说明 |
|---|---|---|
| APPLICATION | 0 | 应用权限,比如能否访问Cockpit、Tasklist |
| USER | 1 | 用户管理权限 |
| GROUP | 2 | 用户组管理权限 |
| GROUP_MEMBERSHIP | 3 | 用户组成员关系权限 |
| AUTHORIZATION | 4 | 授权管理权限 |
| FILTER | 5 | 过滤器权限 |
| PROCESS_DEFINITION | 6 | 流程定义权限 |
| TASK | 7 | 任务权限(最重要的之一) |
| PROCESS_INSTANCE | 8 | 流程实例权限 |
| DEPLOYMENT | 9 | 部署权限 |
| TENANT | 11 | 租户权限 |
我举个例子你就明白了。假设我们新建一个用户user2,只给他分配了访问Cockpit应用的权限(APPLICATION,编码0)。这时候user2登录后,能看到Cockpit界面,但里面是空的,看不到任何任务。为什么?因为他没有TASK(编码7)的READ权限。即使有一个任务明确分配给他处理,他也看不到。
你需要通过Camunda的管理界面或者API,给user2添加对TASK资源的READ权限,他才能看到自己的待办任务。如果想让他看到所有任务(比如主管查看下属任务),还需要给他添加对FILTER(编码5)的READ权限。这种细粒度的控制非常灵活,但也意味着配置工作会比较繁琐。
注意:Camunda的权限验证是“白名单”机制。默认情况下,新建的用户没有任何权限(除了管理员)。你必须显式地给他授权,他才能执行相应操作。这点和很多系统“默认有基础权限”的设计思路不同。
5. 实战开发:编写自定义的Camunda认证过滤器
现在进入核心部分——如何把我们业务系统的用户体系,和Camunda的权限体系打通。Camunda提供了ProcessEngineAuthenticationFilter这个过滤器来处理认证,我们需要实现自己的AuthenticationProvider。
先创建一个配置类,注册这个过滤器:
@Configuration
public class CamundaSecurityFilterConfig {
@Bean
public FilterRegistrationBean<ProcessEngineAuthenticationFilter> processEngineAuthenticationFilter() {
FilterRegistrationBean<ProcessEngineAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setName("camunda-auth");
registration.setFilter(new ProcessEngineAuthenticationFilter());
// 关键:指定我们自定义的认证提供器
registration.addInitParameter("authentication-provider",
"com.yourcompany.camunda.provider.CustomAuthenticationProvider");
// 只拦截Camunda的路径,不要影响业务系统
registration.addUrlPatterns("/camunda/*", "/engine-rest/*");
registration.setOrder(1); // 设置合适的顺序,确保在业务系统过滤器之后
return registration;
}
}
重点是这个CustomAuthenticationProvider,它需要实现Camunda的AuthenticationProvider接口。我写了一个比较完整的版本,支持两种认证方式:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
// Basic认证的前缀,Camunda REST客户端会用这种格式
private static final String BASIC_AUTH_PREFIX = "Basic ";
// Bearer Token前缀,我们业务系统通常用这种
private static final String BEARER_PREFIX = "Bearer ";
@Autowired
private IdentityService identityService; // Camunda的身份服务
@Autowired
private YourUserService userService; // 你业务系统的用户服务
@Override
public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) {
// 1. 从请求头获取Authorization
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
// 没有认证头,返回未认证
return AuthenticationResult.unsuccessful();
}
// 2. 处理Basic认证(Camunda Web界面和REST客户端)
if (authHeader.startsWith(BASIC_AUTH_PREFIX)) {
return handleBasicAuth(authHeader, engine);
}
// 3. 处理Bearer Token(我们业务系统的Token)
if (authHeader.startsWith(BEARER_PREFIX)) {
return handleBearerToken(authHeader, engine);
}
return AuthenticationResult.unsuccessful();
}
private AuthenticationResult handleBasicAuth(String authHeader, ProcessEngine engine) {
try {
// 解码Basic认证信息
String encodedCredentials = authHeader.substring(BASIC_AUTH_PREFIX.length());
String decodedCredentials = new String(Base64.getDecoder().decode(encodedCredentials), StandardCharsets.UTF_8);
// 格式应该是 username:password
int colonIndex = decodedCredentials.indexOf(':');
if (colonIndex == -1) {
return AuthenticationResult.unsuccessful();
}
String username = decodedCredentials.substring(0, colonIndex);
String password = decodedCredentials.substring(colonIndex + 1);
// 验证用户名密码
boolean authenticated = engine.getIdentityService().checkPassword(username, password);
if (authenticated) {
return AuthenticationResult.successful(username);
}
} catch (Exception e) {
log.error("Basic认证解析失败", e);
}
return AuthenticationResult.unsuccessful();
}
private AuthenticationResult handleBearerToken(String authHeader, ProcessEngine engine) {
try {
String token = authHeader.substring(BEARER_PREFIX.length());
// 1. 用Token从我们业务系统获取用户信息
YourSystemUser user = userService.getUserByToken(token);
if (user == null) {
return AuthenticationResult.unsuccessful();
}
// 2. 检查Camunda里是否有这个用户,没有就同步创建
User camundaUser = engine.getIdentityService()
.createUserQuery()
.userId(user.getUsername())
.singleResult();
if (camundaUser == null) {
// 自动同步用户到Camunda
camundaUser = syncUserToCamunda(user, engine);
}
// 3. 检查用户状态是否正常
if (!user.isEnabled()) {
return AuthenticationResult.unsuccessful();
}
return AuthenticationResult.successful(camundaUser.getId());
} catch (Exception e) {
log.error("Token认证失败", e);
return AuthenticationResult.unsuccessful();
}
}
private User syncUserToCamunda(YourSystemUser yourUser, ProcessEngine engine) {
// 创建Camunda用户
User camundaUser = engine.getIdentityService().newUser(yourUser.getUsername());
camundaUser.setEmail(yourUser.getEmail());
camundaUser.setFirstName(yourUser.getFirstName());
camundaUser.setLastName(yourUser.getLastName());
// 这里密码怎么处理是个问题。几种方案:
// 1. 用随机密码,反正我们不走Camunda的密码验证
// 2. 用固定密码,但需要加密存储
// 3. 不设置密码,但Camunda要求密码不能为空
// 我通常用第一种方案
String randomPassword = UUID.randomUUID().toString();
camundaUser.setPassword(randomPassword);
engine.getIdentityService().saveUser(camundaUser);
// 同步用户组(角色)
syncUserGroups(yourUser, camundaUser, engine);
return camundaUser;
}
private void syncUserGroups(YourSystemUser yourUser, User camundaUser, ProcessEngine engine) {
// 获取用户在业务系统的角色
List<YourSystemRole> roles = userService.getUserRoles(yourUser.getId());
for (YourSystemRole role : roles) {
// 检查Camunda里是否有对应的组
Group group = engine.getIdentityService()
.createGroupQuery()
.groupId(role.getCode())
.singleResult();
if (group == null) {
// 创建组
group = engine.getIdentityService().newGroup(role.getCode());
group.setName(role.getName());
group.setType("WORKFLOW_ROLE"); // 自定义类型
engine.getIdentityService().saveGroup(group);
}
// 建立用户-组关系
engine.getIdentityService().createMembership(camundaUser.getId(), group.getId());
}
}
@Override
public void augmentResponseByAuthenticationChallenge(HttpServletResponse response, ProcessEngine engine) {
// 当认证失败时,告诉客户端需要Basic认证
response.setHeader(HttpHeaders.WWW_AUTHENTICATE,
BASIC_AUTH_PREFIX + "realm=\"Camunda Rest API\"");
}
}
这个提供器实现了两个关键功能:第一,支持Camunda原生的Basic认证,这样Camunda Modeler设计器、REST API客户端能正常使用;第二,支持我们业务系统的Bearer Token认证,实现单点登录。用户第一次通过业务系统访问Camunda时,会自动在Camunda里创建对应的用户和组。
6. 权限同步策略:如何让业务角色映射到Camunda权限
上面我们同步了用户和组,但还没解决权限问题。Camunda的权限是需要显式授权的,我们需要建立业务系统角色到Camunda权限的映射关系。
我建议在数据库里建一张映射表,记录这种对应关系:
CREATE TABLE camunda_auth_mapping (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_role_code VARCHAR(50) NOT NULL COMMENT '业务角色编码',
camunda_resource_type INT NOT NULL COMMENT 'Camunda资源类型,如7(TASK)',
camunda_resource_id VARCHAR(255) COMMENT '资源ID,*表示所有',
camunda_permission INT NOT NULL COMMENT '权限值,1=READ, 2=UPDATE等',
UNIQUE KEY uk_role_resource (biz_role_code, camunda_resource_type, camunda_resource_id)
);
然后写一个权限同步服务,在用户登录或者角色变更时触发:
@Service
@Slf4j
public class CamundaAuthSyncService {
@Autowired
private ProcessEngine processEngine;
@Autowired
private AuthMappingRepository authMappingRepository;
@Transactional
public void syncUserAuthorizations(String userId, List<String> roleCodes) {
// 1. 先清除用户现有权限
AuthorizationService authService = processEngine.getAuthorizationService();
List<Authorization> existingAuths = authService.createAuthorizationQuery()
.userIdIn(userId)
.list();
for (Authorization auth : existingAuths) {
authService.deleteAuthorization(auth.getId());
}
// 2. 根据角色获取权限配置
List<AuthMapping> mappings = new ArrayList<>();
for (String roleCode : roleCodes) {
mappings.addAll(authMappingRepository.findByBizRoleCode(roleCode));
}
// 3. 去重(同一个用户多个角色可能有相同权限)
Map<String, AuthMapping> uniqueMappings = new HashMap<>();
for (AuthMapping mapping : mappings) {
String key = mapping.getCamundaResourceType() + ":" +
mapping.getCamundaResourceId() + ":" +
mapping.getCamundaPermission();
uniqueMappings.put(key, mapping);
}
// 4. 创建授权
for (AuthMapping mapping : uniqueMappings.values()) {
Authorization auth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
auth.setUserId(userId);
auth.setResourceType(mapping.getCamundaResourceType());
// 资源ID为*表示所有资源
if ("*".equals(mapping.getCamundaResourceId())) {
auth.setResourceId(Authorization.ANY);
} else {
auth.setResourceId(mapping.getCamundaResourceId());
}
// 设置权限
if (mapping.getCamundaPermission() == 1) {
auth.addPermission(Permissions.READ);
} else if (mapping.getCamundaPermission() == 2) {
auth.addPermission(Permissions.UPDATE);
} else if (mapping.getCamundaPermission() == 4) {
auth.addPermission(Permissions.CREATE);
} else if (mapping.getCamundaPermission() == 8) {
auth.addPermission(Permissions.DELETE);
} else if (mapping.getCamundaPermission() == 16) {
auth.addPermission(Permissions.ACCESS);
} else if (mapping.getCamundaPermission() == 32) {
auth.addPermission(Permissions.ALL);
}
authService.saveAuthorization(auth);
}
log.info("已同步用户{}的Camunda权限,共{}条", userId, uniqueMappings.size());
}
}
这个同步服务可以在几个时机触发:
- 用户登录Camunda时
- 管理员修改用户角色时
- 定时任务批量同步(防止数据不一致)
对于权限映射的配置,我建议在管理后台做个界面,让管理员能直观地配置。比如这样:
| 业务角色 | Camunda资源 | 资源ID | 权限 | 说明 |
|---|---|---|---|---|
| 部门经理 | TASK | * | READ, UPDATE | 可查看和更新所有任务 |
| 普通员工 | TASK | * | READ | 只能查看分配给自己的任务 |
| 流程管理员 | PROCESS_DEFINITION | * | ALL | 可部署、删除流程定义 |
| 系统管理员 | ALL | * | ALL | 超级管理员权限 |
7. 生产环境加固:必须考虑的五个安全策略
在开发环境跑通只是第一步,要上生产环境,还得考虑更多安全因素。我总结了几条实战经验:
第一,一定要改掉默认密码和账号。Camunda默认的admin/admin是公开的秘密,必须在生产环境修改。你可以在application.yml里配置多个管理员:
camunda:
bpm:
admin-user:
id: admin
password: ${CAMUNDA_ADMIN_PASSWORD:请设置复杂密码}
firstName: 系统
lastName: 管理员
admin-users:
- id: admin
password: ${CAMUNDA_ADMIN_PASSWORD}
- id: workflow_admin
password: ${CAMUNDA_WF_ADMIN_PASSWORD}
更安全的做法是从数据库或LDAP初始化管理员,可以在应用启动时执行:
@Component
public class CamundaAdminInitializer implements CommandLineRunner {
@Autowired
private ProcessEngine processEngine;
@Value("${camunda.admin.initialPassword}")
private String initialPassword;
@Override
public void run(String... args) {
IdentityService identityService = processEngine.getIdentityService();
// 检查admin用户是否存在
User admin = identityService.createUserQuery()
.userId("admin")
.singleResult();
if (admin == null) {
admin = identityService.newUser("admin");
admin.setPassword(initialPassword);
admin.setFirstName("System");
admin.setLastName("Administrator");
admin.setEmail("admin@yourcompany.com");
identityService.saveUser(admin);
// 给admin所有权限
AuthorizationService authService = processEngine.getAuthorizationService();
Authorization auth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
auth.setUserId("admin");
auth.setResource(Resources.APPLICATION);
auth.setResourceId(Authorization.ANY);
auth.addPermission(Permissions.ALL);
authService.saveAuthorization(auth);
log.info("已创建Camunda管理员账户");
}
}
}
第二,限制Camunda Web控制台的访问IP。生产环境不应该让所有人都能访问/camunda/app。可以通过Nginx配置或者Spring Security的hasIpAddress来限制:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/camunda/app/**")
.access("hasIpAddress('192.168.1.0/24') or hasIpAddress('127.0.0.1')")
.antMatchers("/engine-rest/**")
.authenticated() // REST API需要认证
// ... 其他配置
}
}
第三,REST API一定要加认证。Camunda的REST API默认是开放的,这非常危险。我们的自定义认证过滤器已经处理了这个问题,但还要确保所有API请求都经过验证。可以在过滤器里加一层检查:
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) {
// 对于REST API,强制要求认证
String requestUri = request.getRequestURI();
if (requestUri.startsWith("/engine-rest/") &&
!requestUri.equals("/engine-rest/engine")) { // 引擎信息接口可放开
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || authHeader.trim().isEmpty()) {
// 记录审计日志
log.warn("未认证的REST API访问: {}", requestUri);
return AuthenticationResult.unsuccessful();
}
}
// ... 原有认证逻辑
}
}
第四,做好操作审计。Camunda自带历史记录,但对于敏感操作(如删除流程实例、修改任务分配人),建议额外记录审计日志:
@Component
public class CamundaAuditListener implements ExecutionListener, TaskListener {
@Override
public void notify(DelegateExecution execution) {
if (execution.getEventName().equals(ExecutionListener.EVENTNAME_END)) {
// 流程结束,记录审计
auditService.logProcessEnd(
execution.getProcessInstanceId(),
execution.getProcessBusinessKey(),
SecurityUtils.getCurrentUser()
);
}
}
@Override
public void notify(DelegateTask delegateTask) {
if (delegateTask.getEventName().equals(TaskListener.EVENTNAME_COMPLETE)) {
// 任务完成,记录审计
auditService.logTaskComplete(
delegateTask.getId(),
delegateTask.getName(),
delegateTask.getAssignee(),
SecurityUtils.getCurrentUser()
);
}
}
}
第五,定期审查和清理权限。权限配置久了容易混乱,建议每月跑一次权限审计脚本:
-- 检查是否有用户拥有过高权限
SELECT user_id_, resource_type_, resource_id_, count(*) as perm_count
FROM act_ru_authorization
WHERE resource_id_ = '*'
GROUP BY user_id_, resource_type_, resource_id_
HAVING perm_count > 5; -- 比如一个人对同一资源有超过5种权限
-- 检查长期未使用的账号
SELECT id_, first_, last_, email_
FROM act_id_user u
WHERE NOT EXISTS (
SELECT 1 FROM act_hi_taskinst
WHERE assignee_ = u.id_
AND end_time_ > DATE_SUB(NOW(), INTERVAL 90 DAY)
)
AND u.id_ != 'admin';
8. 常见坑点排查:我踩过的那些雷
做了这么多Camunda集成项目,我总结了一些常见的坑,你可能会遇到:
坑一:过滤器顺序问题。如果你的业务系统有多个过滤器,顺序很重要。Camunda的认证过滤器必须在业务系统的权限过滤器之后,否则业务系统的Token解析会失败。但又要确保在Camunda自己的过滤器之前。我通常这样设置顺序:
@Bean
public FilterRegistrationBean<ProcessEngineAuthenticationFilter> camundaAuthFilter() {
FilterRegistrationBean<ProcessEngineAuthenticationFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(new ProcessEngineAuthenticationFilter());
reg.addUrlPatterns("/camunda/*", "/engine-rest/*");
reg.setOrder(10); // 业务系统过滤器通常是1-5,Camunda设成10
return reg;
}
@Bean
public FilterRegistrationBean<YourAuthFilter> yourAuthFilter() {
FilterRegistrationBean<YourAuthFilter> reg = new FilterRegistrationBean<>();
reg.setFilter(new YourAuthFilter());
reg.addUrlPatterns("/*");
reg.setOrder(5); // 业务过滤器在前
return reg;
}
坑二:CORS跨域问题。如果你的前端单独部署,调用Camunda REST API时会遇到跨域问题。需要在SpringBoot配置里处理:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/engine-rest/**")
.allowedOrigins("http://your-frontend-domain.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
坑三:Session冲突。Camunda Web界面和你的业务系统可能都用Session,如果域名相同,Session会冲突。解决办法是给Camunda设置不同的Session Cookie名称:
server:
servlet:
session:
cookie:
name: CAMUNDA_SESSIONID # 默认是JSESSIONID
或者用不同的子域名分开,比如业务系统用app.yourcompany.com,Camunda用camunda.yourcompany.com。
坑四:性能问题。权限检查每次请求都会发生,如果用户多、权限复杂,可能影响性能。我建议:
- 在认证过滤器里加缓存,比如用Redis缓存用户权限信息,设置5-10分钟过期
- 批量查询权限,减少数据库查询次数
- 对于只读接口(如查询任务列表),可以考虑用更宽松的缓存策略
坑五:多租户权限隔离。如果你的系统是多租户的,Camunda也需要支持租户隔离。Camunda本身支持租户概念,但需要你在权限上做文章:
// 创建租户
Tenant tenant = identityService.newTenant("tenant1");
tenant.setName("租户A");
identityService.saveTenant(tenant);
// 把用户加入租户
identityService.createTenantUserMembership("tenant1", userId);
// 授权时指定租户
Authorization auth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
auth.setUserId(userId);
auth.setResource(Resources.PROCESS_DEFINITION);
auth.setResourceId(processDefinitionId);
auth.setTenantId("tenant1"); // 关键:指定租户
auth.addPermission(Permissions.READ);
authService.saveAuthorization(auth);
这样用户就只能看到自己租户下的流程定义了。
9. 高级技巧:动态权限与行级数据过滤
基础权限控制能满足大部分场景,但有些复杂需求需要更精细的控制。比如:销售总监只能看到自己部门的流程,项目经理只能看到自己项目的任务。这就需要动态权限和行级数据过滤。
Camunda提供了AuthorizationService的API,可以运行时动态授权。我们可以结合流程变量来实现动态权限。举个例子,报销流程的部门经理审批环节,只能由该部门的经理审批:
@Service
public class DynamicAuthorizationService {
@Autowired
private ProcessEngine processEngine;
/**
* 在流程启动时,根据流程变量设置动态权限
*/
public void setupProcessInstanceAuth(String processInstanceId, String starterUserId) {
RuntimeService runtimeService = processEngine.getRuntimeService();
AuthorizationService authService = processEngine.getAuthorizationService();
// 获取流程变量
String department = (String) runtimeService.getVariable(processInstanceId, "department");
// 找到该部门的经理
String departmentManager = getDepartmentManager(department);
// 给部门经理授权,可以查看这个流程实例
Authorization auth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
auth.setUserId(departmentManager);
auth.setResource(Resources.PROCESS_INSTANCE);
auth.setResourceId(processInstanceId); // 关键:指定具体的流程实例ID
auth.addPermission(Permissions.READ);
auth.addPermission(Permissions.UPDATE);
authService.saveAuthorization(auth);
// 流程发起人默认有所有权限
Authorization starterAuth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
starterAuth.setUserId(starterUserId);
starterAuth.setResource(Resources.PROCESS_INSTANCE);
starterAuth.setResourceId(processInstanceId);
starterAuth.addPermission(Permissions.ALL);
authService.saveAuthorization(starterAuth);
}
/**
* 任务创建时,设置谁能看到这个任务
*/
public void setupTaskAuth(String taskId, String processInstanceId) {
TaskService taskService = processEngine.getTaskService();
AuthorizationService authService = processEngine.getAuthorizationService();
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
// 如果是部门经理审批任务
if ("部门经理审批".equals(task.getName())) {
String department = (String) taskService.getVariable(taskId, "department");
String departmentManager = getDepartmentManager(department);
// 只给部门经理授权
Authorization auth = authService.createNewAuthorization(Authorization.AUTH_TYPE_GRANT);
auth.setUserId(departmentManager);
auth.setResource(Resources.TASK);
auth.setResourceId(taskId); // 关键:指定具体的任务ID
auth.addPermission(Permissions.READ);
auth.addPermission(Permissions.UPDATE);
authService.saveAuthorization(auth);
}
}
}
然后在流程监听器里调用这些方法:
@Component
public class ProcessAuthListener implements ExecutionListener {
@Autowired
private DynamicAuthorizationService authService;
@Override
public void notify(DelegateExecution execution) {
if (execution.getEventName().equals(ExecutionListener.EVENTNAME_START)) {
// 流程启动时设置权限
String starter = execution.getVariable("starter", String.class);
authService.setupProcessInstanceAuth(execution.getProcessInstanceId(), starter);
}
}
}
@Component
public class TaskAuthListener implements TaskListener {
@Autowired
private DynamicAuthorizationService authService;
@Override
public void notify(DelegateTask delegateTask) {
if (delegateTask.getEventName().equals(TaskListener.EVENTNAME_CREATE)) {
// 任务创建时设置权限
authService.setupTaskAuth(
delegateTask.getId(),
delegateTask.getProcessInstanceId()
);
}
}
}
这样配置后,每个流程实例、每个任务都有独立的权限控制。部门经理只能看到自己部门的流程和任务,实现了行级数据隔离。
10. 监控与维护:权限系统的健康检查
权限系统上线后,不能放任不管。我建议建立定期检查机制,确保权限配置正确、没有安全漏洞。
第一,权限审计日志。记录所有权限变更操作:
@Aspect
@Component
@Slf4j
public class AuthorizationAuditAspect {
@Autowired
private AuthorizationService authorizationService;
@AfterReturning(
pointcut = "execution(* org.camunda.bpm.engine.AuthorizationService.saveAuthorization(..))",
returning = "authorization"
)
public void auditSaveAuthorization(Authorization authorization) {
String currentUser = SecurityUtils.getCurrentUser();
log.info("用户{}创建了授权: 用户={}, 资源={}, 资源ID={}, 权限={}",
currentUser,
authorization.getUserId(),
authorization.getResourceType(),
authorization.getResourceId(),
authorization.getPermissions());
// 还可以存到数据库,方便后续审计
auditRepository.save(new AuthorizationAudit(
currentUser, "CREATE", authorization, new Date()
));
}
@AfterReturning(
pointcut = "execution(* org.camunda.bpm.engine.AuthorizationService.deleteAuthorization(..))",
returning = "authorizationId"
)
public void auditDeleteAuthorization(String authorizationId) {
// 删除前先查询记录
Authorization auth = authorizationService.createAuthorizationQuery()
.authorizationId(authorizationId)
.singleResult();
if (auth != null) {
String currentUser = SecurityUtils.getCurrentUser();
log.warn("用户{}删除了授权: ID={}, 用户={}, 资源={}",
currentUser, authorizationId,
auth.getUserId(), auth.getResourceType());
}
}
}
第二,定期权限巡检脚本。每周跑一次,检查异常情况:
-- 1. 检查是否有用户拥有过多权限(可能配置错误)
SELECT user_id_, count(*) as auth_count
FROM act_ru_authorization
WHERE resource_id_ = '*' -- 全局权限
GROUP BY user_id_
HAVING auth_count > 20;
-- 2. 检查是否有用户同时拥有冲突权限(如READ和DELETE)
SELECT user_id_, resource_type_,
MAX(CASE WHEN permission_ = 1 THEN 1 ELSE 0 END) as has_read,
MAX(CASE WHEN permission_ = 8 THEN 1 ELSE 0 END) as has_delete
FROM act_ru_authorization
GROUP BY user_id_, resource_type_
HAVING has_read = 1 AND has_delete = 1;
-- 3. 检查离职员工账号是否还有权限
SELECT u.id_, u.first_, u.last_, u.email_, count(a.id_) as active_auth
FROM act_id_user u
LEFT JOIN act_ru_authorization a ON u.id_ = a.user_id_
WHERE u.id_ NOT IN (SELECT user_id FROM hr_active_employees) -- 关联HR系统
GROUP BY u.id_, u.first_, u.last_, u.email_
HAVING active_auth > 0;
第三,权限变更审批流程。重要的权限变更(如给用户ALL权限)应该走审批流程。可以就用Camunda自己实现一个权限审批流程:
- 用户申请权限 -> 2. 直属领导审批 -> 3. 系统管理员执行授权
这样既能满足业务需求,又能保证安全可控。
第四,定期权限回顾。每季度组织一次权限回顾会议,检查:
- 是否有员工岗位变动但权限未及时调整
- 是否有权限配置不符合最小权限原则
- 是否有异常权限使用记录(如非工作时间大量查询)
这些检查看起来繁琐,但能有效避免权限泛滥和安全事故。我在一个金融项目里,通过定期审计发现了3个已离职员工账号仍有权限,及时清理避免了潜在风险。
11. 测试策略:如何验证权限配置正确性
权限系统最怕的就是配置错误——该有的权限没有,不该有的权限反而有了。我建议从三个层面测试:
单元测试:测试认证提供器和权限服务
@SpringBootTest
@AutoConfigureMockMvc
class CamundaAuthTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ProcessEngine processEngine;
@Test
void testBasicAuthSuccess() throws Exception {
// 测试Basic认证
String auth = "Basic " + Base64.getEncoder()
.encodeToString("admin:admin123".getBytes());
mockMvc.perform(get("/engine-rest/engine")
.header(HttpHeaders.AUTHORIZATION, auth))
.andExpect(status().isOk());
}
@Test
void testBearerTokenAuth() throws Exception {
// 测试业务系统Token认证
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
mockMvc.perform(get("/engine-rest/task")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray());
}
@Test
void testNoAuthShouldFail() throws Exception {
// 未认证应该返回401
mockMvc.perform(get("/engine-rest/task"))
.andExpect(status().isUnauthorized());
}
@Test
void testDepartmentManagerCanOnlySeeHisDepartment() {
// 测试部门经理只能看到本部门任务
IdentityService identityService = processEngine.getIdentityService();
TaskService taskService = processEngine.getTaskService();
// 创建两个部门的流程
startProcessForDepartment("dept_sales", "sales_task_1");
startProcessForDepartment("dept_tech", "tech_task_1");
// 用销售部经理查询
Authentication.setAuthenticatedUserId("sales_manager");
List<Task> salesTasks = taskService.createTaskQuery()
.taskCandidateUser("sales_manager")
.list();
// 应该只能看到销售部的任务
assertEquals(1, salesTasks.size());
assertEquals("sales_task_1", salesTasks.get(0).getName());
Authentication.clearAuthentication();
}
}
集成测试:模拟真实用户操作
@SpringBootTest
class CamundaIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testEndToEndPermissionFlow() {
// 1. 普通员工登录业务系统
String token = login("zhangsan", "password123");
// 2. 通过业务系统访问Camunda REST API
HttpHeaders headers = new HttpHeaders();
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token);
// 3. 应该只能看到自己的任务
ResponseEntity<TaskDto[]> response = restTemplate.exchange(
"/engine-rest/task",
HttpMethod.GET,
new HttpEntity<>(headers),
TaskDto[].class
);
TaskDto[] tasks = response.getBody();
assertNotNull(tasks);
// 验证每个任务都是分配给zhangsan的
for (TaskDto task : tasks) {
assertEquals("zhangsan", task.getAssignee());
}
// 4. 尝试访问管理员接口应该被拒绝
ResponseEntity<String> adminResponse = restTemplate.exchange(
"/engine-rest/group",
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);
assertEquals(HttpStatus.FORBIDDEN, adminResponse.getStatusCode());
}
}
UI自动化测试:用Selenium测试Web界面权限
public class CamundaWebPermissionTest {
@Test
void testManagerCanSeeAllTasks() {
WebDriver driver = new ChromeDriver();
try {
// 1. 部门经理登录
driver.get("http://localhost:8080/camunda/app");
driver.findElement(By.id("username")).sendKeys("sales_manager");
driver.findElement(By.id("password")).sendKeys("manager123");
driver.findElement(By.cssSelector("button[type='submit']")).click();
// 2. 进入Tasklist
driver.findElement(By.cssSelector("a[href='#/tasklist']")).click();
Thread.sleep(2000); // 等待加载
// 3. 应该看到所有销售部任务
WebElement taskTable = driver.findElement(By.cssSelector(".task-list-table"));
List<WebElement> tasks = taskTable.findElements(By.cssSelector("tbody tr"));
assertTrue(tasks.size() > 0);
// 验证任务都属于销售部
for (WebElement task : tasks) {
String department = task.findElement(By.cssSelector(".department-column")).getText();
assertEquals("销售部", department);
}
} finally {
driver.quit();
}
}
}
性能测试:模拟高并发下的权限验证
@SpringBootTest
class CamundaPerformanceTest {
@Test
void testAuthPerformanceUnderLoad() {
int userCount = 100;
int requestsPerUser = 50;
ExecutorService executor = Executors.newFixedThreadPool(20);
List<Future<Long>> futures = new ArrayList<>();
// 模拟100个用户并发查询任务
for (int i = 0; i < userCount; i++) {
final String userId = "user_" + i;
futures.add(executor.submit(() -> {
long totalTime = 0;
Authentication.setAuthenticatedUserId(userId);
TaskService taskService = processEngine.getTaskService();
for (int j = 0; j < requestsPerUser; j++) {
long start = System.currentTimeMillis();
taskService.createTaskQuery()
.taskCandidateUser(userId)
.list();
long end = System.currentTimeMillis();
totalTime += (end - start);
}
Authentication.clearAuthentication();
return totalTime;
}));
}
// 统计性能指标
long totalTime = 0;
for (Future<Long> future : futures) {
totalTime += future.get();
}
long avgTimePerRequest = totalTime / (userCount * requestsPerUser);
// 平均每个权限查询应该在100ms以内
assertTrue("权限查询太慢: " + avgTimePerRequest + "ms",
avgTimePerRequest < 100);
}
}
这些测试要纳入CI/CD流程,每次代码变更都自动运行。特别是权限相关的修改,必须通过所有测试才能上线。
12. 实际案例:电商订单审核流程的权限设计
最后,我分享一个真实的电商项目案例,看看权限设计是怎么落地的。这个系统有几种角色:客服、客服主管、运营经理、财务、系统管理员。
权限需求:
- 客服:只能看到和处理自己创建的订单
- 客服主管:能看到本组所有客服的订单,可以分配、转派
- 运营经理:能看到所有订单,可以加急、特批
- 财务:只能看到待付款、已付款的订单,不能看到售后订单
- 系统管理员:全部权限
实现方案:
首先,在Camunda里创建对应的组:
@Component
public class RoleGroupInitializer implements CommandLineRunner {
@Override
public void run(String... args) {
createGroupIfNotExists("ROLE_CS", "客服");
createGroupIfNotExists("ROLE_CS_LEADER", "客服主管");
createGroupIfNotExists("ROLE_OPERATION", "运营经理");
createGroupIfNotExists("ROLE_FINANCE", "财务");
createGroupIfNotExists("ROLE_ADMIN", "系统管理员");
}
private void createGroupIfNotExists(String groupId, String groupName) {
Group group = identityService.createGroupQuery()
.groupId(groupId)
.singleResult();
if (group == null) {
group = identityService.newGroup(groupId);
group.setName(groupName);
group.setType("BUSINESS_ROLE");
identityService.saveGroup(group);
}
}
}
然后,配置权限映射:
-- 客服:只能看到自己的任务
INSERT INTO camunda_auth_mapping (biz_role_code, camunda_resource_type, camunda_resource_id, camunda_permission) VALUES
('ROLE_CS', 7, '*', 1), -- TASK READ
('ROLE_CS', 0, 'cockpit', 16), -- 可以访问Cockpit
('ROLE_CS', 0, 'tasklist', 16); -- 可以访问Tasklist
-- 客服主管:可以看到组内所有任务
INSERT INTO camunda_auth_mapping VALUES
('ROLE_CS_LEADER', 7, '*', 1),
('ROLE_CS_LEADER', 5, '*', 1), -- 可以查看所有过滤器
('ROLE_CS_LEADER', 8, '*', 1); -- 可以查看所有流程实例
-- 运营经理:所有权限,但不能删除
INSERT INTO camunda_auth_mapping VALUES
('ROLE_OPERATION', 0, '*', 16), -- 所有应用
('ROLE_OPERATION', 6, '*', 1), -- 流程定义READ
('ROLE_OPERATION', 6, '*', 2), -- 流程定义UPDATE
('ROLE_OPERATION', 6, '*', 4), -- 流程定义CREATE
('ROLE_OPERATION', 7, '*', 1), -- 任务READ
('ROLE_OPERATION', 7, '*', 2), -- 任务UPDATE
('ROLE_OPERATION', 8, '*', 1), -- 流程实例READ
('ROLE_OPERATION', 8, '*', 2); -- 流程实例UPDATE
-- 财务:只能看到支付相关任务
INSERT INTO camunda_auth_mapping VALUES
('ROLE_FINANCE', 7, '*', 1), -- 可以看任务
('ROLE_FINANCE', 5, 'finance_tasks', 1); -- 只能看到财务相关过滤器
-- 系统管理员:全部权限
INSERT INTO camunda_auth_mapping VALUES
('ROLE_ADMIN', 0, '*', 32), -- ALL权限
('ROLE_ADMIN', 1, '*', 32),
('ROLE_ADMIN', 2, '*', 32),
('ROLE_ADMIN', 4, '*', 32),
('ROLE_ADMIN', 6, '*', 32),
('ROLE_ADMIN', 7, '*', 32),
('ROLE_ADMIN', 8, '*', 32),
('ROLE_ADMIN', 9, '*', 32);
在流程设计时,通过监听器动态设置权限:
@Component
public class OrderProcessAuthListener {
public void onOrderCreated(DelegateExecution execution) {
String orderCreator = (String) execution.getVariable("createdBy");
String department = (String) execution.getVariable("department");
// 给创建者所有权限
grantAllPermissions(execution.getProcessInstanceId(), orderCreator);
// 给部门主管查看权限
String departmentManager = getDepartmentManager(department);
grantReadPermission(execution.getProcessInstanceId(), departmentManager);
// 如果是大额订单,给运营经理加急权限
BigDecimal amount = (BigDecimal) execution.getVariable("orderAmount");
if (amount.compareTo(new BigDecimal("10000")) > 0) {
String operationManager = getOperationManager();
grantUpdatePermission(execution.getProcessInstanceId(), operationManager);
}
}
public void onPaymentTaskCreated(DelegateTask task) {
// 支付任务只对财务可见
String financeGroup = "ROLE_FINANCE";
// 清除其他候选组
task.getCandidates().clear();
// 只添加财务组
task.addCandidateGroup(financeGroup);
// 设置动态权限,只有财务能看到这个任务
AuthorizationService authService = task.getProcessEngine()
.getAuthorizationService();
Authorization auth = authService.createNewAuthorization(
Authorization.AUTH_TYPE_GRANT
);
auth.setGroupId(financeGroup);
auth.setResource(Resources.TASK);
auth.setResourceId(task.getId());
auth.addPermission(Permissions.READ);
auth.addPermission(Permissions.UPDATE);
authService.saveAuthorization(auth);
}
}
这个方案上线后运行稳定,不同角色的用户只能看到自己权限范围内的订单和任务。客服不会看到财务的付款任务,财务也看不到客服的售后任务,运营经理可以监控所有订单但无法删除流程,系统管理员有全部权限但操作都有审计日志。
通过这个案例我想说,Camunda的权限体系虽然复杂,但一旦掌握,就能设计出非常灵活、安全的权限方案。关键是要理解业务需求,把Camunda的权限概念(用户、组、资源、权限)映射到你的业务概念(员工、角色、功能、操作)上,然后通过代码实现这种映射关系。
887



被折叠的 条评论
为什么被折叠?



