前言
今天在springboot项目中完成了对Activiti7的整合,activiti7提供了对springboot的场景启动器(starter),也提供了相应的依赖管理的包,所以整个过程非常的的方便,也比较简单。
但是整合的过程中以为Activiti7默认使用了Spring Security,所以整合的过程中有温习了一遍关于Spring Security的配置信息。Activiti在鉴权方便帮我们做了选择,这有的时候也限制了我们使用其他的库,比如Shiro等,这也可以看出Activiti是提倡我们使用SpringSecurity来做权限认证的。
网上也已经有许多博客讲了如何整合Shiro到Activit中,这个我目前没有这方面的需求所以也没有去做相应的研究,闲暇之时我也回去尝试下如何整合Shiro。
本篇文章主要作为学习笔记记录所用,也希望能帮助希望快速整合Activiti的朋友做个参考,文中不足之处还有望各路大神指出。
鉴权
官方Example中的Util
@Component
public class SecurityUtil {
// 模拟调用了SpringSecurity 登录鉴权
private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
@Autowired
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
}
logger.info("> Logged in as: " + username);
SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
}
为了方便测试,Activiti的官方示例中提供了一个Util,我们注意到这个仓库进行了模拟用户登录,并将鉴权信息赋值到引擎中:
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
新API中的鉴权原理
- TaskRuntime
package org.activiti.runtime.api.impl;
@PreAuthorize("hasRole('ACTIVITI_USER')")
public class TaskRuntimeImpl implements TaskRuntime {
}
- ProcessRuntime
package org.activiti.runtime.api.impl;
@PreAuthorize("hasRole('ACTIVITI_USER')")
public class ProcessRuntimeImpl implements ProcessRuntime {
}
activiti7中对原有的一些接口做了二次封装,从而进一步简化了用户的使用流程。
通过查看这个两个API的实现类源码来看,调用的话需要调用的用户含有ACTIVITI_USER
角色权限。所以,如果没有使用SpringSecurity的话,这两个API便不能直接调用。
POM文件
<!-- springboot 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- mysql驱动 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.19</version>
</dependency>
<!-- activiti 依赖 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M4</version>
</dependency>
<dependency>
<groupId>org.activiti.dependencies</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.1.0.M4</version>
<type>pom</type>
</dependency>
SpringSecurity简易配置
如果仅仅是测试的话,可以直接将用户存在内存中实现。我这里还是使用的数据库方式来保存用户信息。
-
用户表结构
+----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int | NO | PRI | NULL | auto_increment | | username | varchar(255) | YES | | NULL | | | password | varchar(255) | YES | | NULL | | | roles | varchar(255) | YES | | NULL | | +----------+--------------+------+-----+---------+----------------+
需要实现的配置
用户查询接口
- 实现UserDetails的接口类作为鉴权用户实体
@Component
public class LocalUserDetail implements UserDetails {
private int id;
private String username;
private String password;
private String roles;
// Entity 转 LocalUserDetail
public static LocalUserDetail of(User user){
LocalUserDetail localUserDetail = new LocalUserDetail();
localUserDetail.id = user.getId();
localUserDetail.username = user.getUsername();
localUserDetail.password = user.getPassword();
localUserDetail.roles = user.getRoles();
return localUserDetail;
}
// 权限包装
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(roles.split(",")).map(e->new SimpleGrantedAuthority(e)).collect(Collectors.toSet());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
-
用户查询接口
@Component public class CustomUserDetailService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 调用DAO实现用户的查询 Optional<User> user = userMapper.selectOne(c -> c.where(UserDynamicSqlSupport.username, isEqualTo(username))); if (!user.isPresent()){ throw new UsernameNotFoundException("用户不存在"); } User u = user.get(); return LocalUserDetail.of(u); } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
登录成功的处理(可选)
@Component()
public class LoginSuccessHandle implements AuthenticationSuccessHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
System.out.println("浏览器表单登录");
}
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("AJAX登录");
// 统一返回json体作为回应
httpServletResponse.setContentType("application/json;charset=UTF-8"); httpServletResponse.getWriter().write(objectMapper.writeValueAsString(BaseResponse.success(UUID.randomUUID().toString())));
}
}
登录失败的处理(可选)
@Component
public class LoginFailHandle implements AuthenticationFailureHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 统一返回json体作为回应
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(BaseResponse.error("登录失败,请重试!")));
}
}
鉴权失效的处理(可选)
/**
* 检测到未登录的时候,这里返回json的应答,而不是跳转到登录页面
*/
public class AuthEntryPoint implements AuthenticationEntryPoint {
public static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
BaseResponse<Void> response = new BaseResponse<>(401, "请先登录系统");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(response));
}
}
SpringSecurity 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginSuccessHandle successHandle;
@Autowired
LoginFailHandle failHandle;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginProcessingUrl("/login")
// 配置登录成功处理器
.successHandler(successHandle)
// 配置登录失败处理器
.failureHandler(failHandle)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.anyRequest()
.authenticated()
.and()
.logout().permitAll().and()
.headers().frameOptions().disable()//让frame页面可以正常使用
.and()
// 配置自定义鉴权失败端点
.exceptionHandling().authenticationEntryPoint(new AuthEntryPoint())
.and()
.csrf().disable();
}
}
Activiti 使用
运行前提
运行后没有生成数据库表
在数据库访问的JDBC URL上添加配置:nullCatalogMeansCurrent=true
在使用mysql-connect 8.+以上版本的时候需要添加
nullCatalogMeansCurrent=true
参数,否则在使用mybatis-generator生成表对应的xml等时会扫描整个服务器里面的全部数据库中的表,而不是扫描对应数据库的表。因此mysql会扫描所有的库来找表,如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。
调用接口是报缺少字段
Activiti自身问题。
alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL;
流程部署相关
自动加载BPMN文件部署
将bpmn文件放在resource下的processes目录下,activiti启动的时候会自动加载该目录下的bpmn文件
调用接口部署
上传文件部署
@PostMapping("/uploadFileAndDeployment")
public BaseResponse uploadFileAndDeployment(
@RequestParam("processFile")MultipartFile processFile,
@RequestParam(value = "processName",required = false) String processName){
String originalFilename = processFile.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
if (processName != null){
processName = originalFilename;
}
try {
InputStream inputStream = processFile.getInputStream();
Deployment deployment = null;
if ("zip".equals(extension)){
// 压缩包部署方式
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();
}else if ("bpmn".equals(extension)){
// bpmn文件部署方式
deployment = repositoryService.createDeployment().addInputStream(originalFilename,inputStream).name(processName).deploy();
}
return BaseResponse.success(deployment);
} catch (IOException e) {
e.printStackTrace();
}
return BaseResponse.success();
}
上传BPMN内容字符串部署
@PostMapping("/postBPMNAndDeployment")
public BaseResponse postBPMNAndDeployment(@RequestBody AddXMLRequest addXMLRequest){
Deployment deploy = repositoryService.createDeployment()
// .addString 第一次参数的名字如果没有添加.bpmn的话,不会插入到 ACT_RE_DEPLOYMENT 表中
.addString(addXMLRequest.getProcessName()+".bpmn", addXMLRequest.getBpmnContent())
.name(addXMLRequest.getProcessName())
.deploy();
return BaseResponse.success(deploy);
}
获取流程资源文件
@GetMapping("/getProcessDefineXML")
public void getProcessDefineXML(String deploymentId, String resourceName, HttpServletResponse response){
try {
InputStream inputStream = repositoryService.getResourceAsStream(deploymentId,resourceName);
int count = inputStream.available();
byte[] bytes = new byte[count];
response.setContentType("text/xml");
OutputStream outputStream = response.getOutputStream();
while (inputStream.read(bytes) != -1) {
outputStream.write(bytes);
}
inputStream.close();
} catch (Exception e) {
e.toString();
}
}
流程实例相关
启动实例
@PostMapping("/startProcess")
public BaseResponse startProcess(
String processDefinitionKey,
String instanceName,
@AuthenticationPrincipal LocalUserDetail userDetail){
ProcessInstance processInstance = null;
try{
StartProcessPayload startProcessPayload = ProcessPayloadBuilder.start().withProcessDefinitionKey(processDefinitionKey)
.withBusinessKey("businessKey")
.withVariable("sponsor",userDetail.getUsername())
.withName(instanceName).build();
processInstance = processRuntime.start(startProcessPayload);
}catch (Exception e){
System.out.println(e);
return BaseResponse.error("开启失败:"+e.getLocalizedMessage());
}
return BaseResponse.success(processInstance);
}
挂起实例
@PostMapping("/suspendInstance/{instanceId}")
public BaseResponse suspendInstance(@PathVariable String instanceId){
ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder.suspend().withProcessInstanceId(instanceId).build());
return BaseResponse.success(processInstance);
}
激活实例
@PostMapping("/resumeInstance/{instanceId}")
public BaseResponse resumeInstance(@PathVariable String instanceId){
ProcessInstance processInstance = processRuntime
.resume(ProcessPayloadBuilder.resume().withProcessInstanceId(instanceId).build());
return BaseResponse.success(processInstance);
}
任务相关接口
完成任务
@PostMapping("/completeTask/{taskId}")
public BaseResponse completeTask(@PathVariable String taskId){
Task task = taskRuntime.task(taskId);
if (task.getAssignee()==null){
// 说明任务需要拾取
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
}
taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
return BaseResponse.success();
}
获取自己的任务
@GetMapping("/getTasks")
public BaseResponse getTasks(){
Page<Task> taskPage = taskRuntime.tasks(Pageable.of(0, 100));
List<Task> tasks = taskPage.getContent();
List<TaskVO> taskVOS = new ArrayList<>();
for (Task task : tasks) {
TaskVO taskVO = TaskVO.of(task);
ProcessInstance instance = processRuntime.processInstance(task.getProcessInstanceId());
taskVO.setInstanceName(instance.getName());
taskVOS.add(taskVO);
}
return BaseResponse.success(taskVOS);
}
历史数据查询
public List<HistoricActivityInstanceVO> getProcessHistoryByBusinessKey(String businessKey) {
ProcessInstance instance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult();
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.getId())
.orderByHistoricActivityInstanceStartTime().asc().list();
List<HistoricActivityInstanceVO> historicActivityInstanceVOList = new ArrayList<>();
historicActivityInstanceList.forEach(historicActivityInstance -> historicActivityInstanceVOList.add(VOConverter.getHistoricActivityInstanceVO(historicActivityInstance)));
return historicActivityInstanceVOList;
}
历史详情查询
HistoricDetailQuery historicDetailQuery = historyService.createHistoricDetailQuery();
List<HistoricDetail> historicDetails = historicDetailQuery.processInstanceId(instanceId).orderByTime().list();
for (HistoricDetail hd: historicDetails) {
System.out.println("流程实例ID:"+hd.getProcessInstanceId());
System.out.println("活动实例ID:"+hd.getActivityInstanceId());
System.out.println("执行ID:"+hd.getTaskId());
System.out.println("记录时间:"+hd.getTime());
}
历史流程实例查询
HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
List<HistoricProcessInstance> processInstances = historicProcessInstanceQuery.processDefinitionId(processDefinitionId).list();
for (HistoricProcessInstance hpi : processInstances) {
System.out.println("业务ID:"+hpi.getBusinessKey());
System.out.println("流程定义ID:"+hpi.getProcessDefinitionId());
System.out.println("流程定义Key:"+hpi.getProcessDefinitionKey());
System.out.println("流程定义名称:"+hpi.getProcessDefinitionName());
System.out.println("流程定义版本:"+hpi.getProcessDefinitionVersion());
System.out.println("流程部署ID:"+hpi.getDeploymentId());
System.out.println("开始时间:"+hpi.getStartTime());
System.out.println("结束时间:"+hpi.getEndTime());
}
package org.activiti.engine.history; @Internal public interface HistoricProcessInstance { String getId(); String getBusinessKey(); String getProcessDefinitionId(); String getProcessDefinitionName(); String getProcessDefinitionKey(); Integer getProcessDefinitionVersion(); String getDeploymentId(); Date getStartTime(); Date getEndTime(); Long getDurationInMillis(); String getEndActivityId(); String getStartUserId(); String getStartActivityId(); String getDeleteReason(); String getSuperProcessInstanceId(); String getTenantId(); String getName(); String getDescription(); Map<String, Object> getProcessVariables(); }
任务历史查询
某一次流程的执行经历的多少任务
HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
List<HistoricTaskInstance> taskInstances = historicTaskInstanceQuery.taskId(taskId).list();
for (HistoricTaskInstance hti : taskInstances) {
System.out.println("开始时间:"+hti.getStartTime());
System.out.println("结束时间:"+hti.getEndTime());
System.out.println("任务拾取时间:"+hti.getClaimTime());
System.out.println("删除原因:"+hti.getDeleteReason());
}
活动历史查询
查询某个流程的每个阶段(活动)
HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService.createHistoricActivityInstanceQuery();
List<HistoricActivityInstance> historicActivityInstances = historicActivityInstanceQuery.processInstanceId(instanceId).list();
for (HistoricActivityInstance hai : historicActivityInstances) {
System.out.println("活动ID:"+hai.getActivityId());
System.out.println("活动类型:"+hai.getActivityType());
System.out.println("活动名称:"+hai.getActivityName());
System.out.println("任务ID:"+hai.getTaskId());
}
package org.activiti.engine.history; @Internal public interface HistoricActivityInstance extends HistoricData { String getId(); String getActivityId(); String getActivityName(); String getActivityType(); String getProcessDefinitionId(); String getProcessInstanceId(); String getExecutionId(); String getTaskId(); String getCalledProcessInstanceId(); String getAssignee(); Date getStartTime(); Date getEndTime(); Long getDurationInMillis(); String getDeleteReason(); String getTenantId(); }
变量历史信息
某一次流程的执行时设置的流程变量
HistoricVariableInstanceQuery historicVariableInstanceQuery = historyService.createHistoricVariableInstanceQuery();
List<HistoricVariableInstance> variableInstances =historicVariableInstanceQuery
.processInstanceId(instanceId)
.list();
for (HistoricVariableInstance hva : variableInstances) {
System.out.println("变量名称:"+hva.getVariableName());
System.out.println("变量类型名称:"+hva.getVariableTypeName());
System.out.println("变量值:"+hva.getValue());
System.out.println("流程实例ID:"+hva.getProcessInstanceId());
System.out.println("任务ID:"+hva.getTaskId());
}