2021年7月14日星期三

Activiti7 与 Spring Boot 及 Spring Security 整合 踩坑记录

1.  前言

实话实说,网上关于Activiti的教程千篇一律,有参考价值的不多。很多都是老早以前写的,基本都是直接照搬官方提供的示例,要么就是用单元测试跑一下,要么排除Spring Security,很少有看到一个完整的项目。太难了,笔者在实操的时候,遇到很多坑,在此做一个记录。

其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑中国的实际情况,很多像回退、委派、撤销等等功能都没有,所以最省事的还是中国特色的BPM。

Activiti7的文档比较少,但是教程多。Flowable的文档比较齐全,但是网上教程少。

2.  Maven依赖

<?

配置 application.properties

server.port=8080server.servlet.context-path=/activiti7spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&nullCatalogMeansCurrent=truespring.datasource.username=rootspring.datasource.password=123456spring.jpa.database=mysqlspring.jpa.open-in-view=truespring.jpa.properties.hibernate.enable_lazy_load_no_trans=truespring.jpa.show-sql=truespring.redis.host=192.168.28.31spring.redis.port=6379spring.redis.password=123456spring.redis.database=1spring.activiti.database-schema-update=truespring.activiti.db-history-used=truespring.activiti.history-level=fullspring.activiti.check-process-definitions=falsespring.activiti.deployment-mode=never-fail

代码是最好的老师,查看代码所有配置项都一目了然

这里最好关闭自动部署,不然每次项目启动的时候就会自动部署一次

3.  集成 Spring Security

详见我另一篇 《基于 Spring Security 的前后端分离的权限控制系统》 

3.1.  实体类

权限

package com.cjs.example.entity;import lombok.Getter;import lombok.Setter;import javax.persistence.*;import java.io.Serializable;import java.util.Set;/** * 菜单表 * @Author ChengJianSheng * @Date 2021/6/12 */@Setter@Getter@Entity@Table(name = "sys_menu")public class SysMenuEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /**  * 资源编码  */ @Column(name = "code") private String code; /**  * 资源名称  */ @Column(name = "name") private String name; /**  * 菜单/按钮URL  */ @Column(name = "url") private String url; /**  * 资源类型(1:菜单,2:按钮)  */ @Column(name = "type") private Integer type; /**  * 父级菜单ID  */ @Column(name = "pid") private Integer pid; /**  * 排序号  */ @Column(name = "sort") private Integer sort; @ManyToMany(mappedBy = "menus") private Set<SysRoleEntity> roles;}

角色

package com.cjs.example.entity;import lombok.Getter;import lombok.Setter;import javax.persistence.*;import java.io.Serializable;import java.util.Set;/** * 角色表 * @Author ChengJianSheng * @Date 2021/6/12 */@Setter@Getter@Entity@Table(name = "sys_role")public class SysRoleEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /**  * 角色名称  */ @Column(name = "name") private String name; @ManyToMany(mappedBy = "roles") private Set<SysUserEntity> users; @ManyToMany @JoinTable(name = "sys_role_menu",   joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},   inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")}) private Set<SysMenuEntity> menus; @ManyToMany @JoinTable(name = "sys_dept_role",   joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},   inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")}) private Set<SysDeptEntity> depts;} 

部门 

package com.cjs.example.entity;import lombok.Getter;import lombok.Setter;import javax.persistence.*;import java.io.Serializable;import java.util.Set;/** * 部门表 * @Author ChengJianSheng * @Date 2021/6/12 */@Setter@Getter@Entity@Table(name = "sys_dept")public class SysDeptEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; /**  * 部门名称  */ @Column(name = "name") private String name; /**  * 父级部门ID  */ @Column(name = "pid") private Integer pid; /**  * 组对应的角色  */ @ManyToMany(mappedBy = "depts") private Set<SysRoleEntity> roles;} 

用户

package com.cjs.example.entity;import lombok.Getter;import lombok.Setter;import javax.persistence.*;import java.io.Serializable;import java.time.LocalDate;import java.util.Set;/** * 用户表 * @Author ChengJianSheng * @Date 2021/6/12 */@Setter@Getter@Entity@Table(name = "sys_user")public class SysUserEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Integer id; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "mobile") private String mobile; @Column(name = "enabled") private Integer enabled; @Column(name = "create_time") private LocalDate createTime; @Column(name = "update_time") private LocalDate updateTime; @OneToOne @JoinColumn(name = "dept_id") private SysDeptEntity dept; @ManyToMany @JoinTable(name = "sys_user_role",   joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},   inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}) private Set<SysRoleEntity> roles;}

3.2.  自定义 UserDetailsService

package com.cjs.example.domain;import lombok.Setter;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.Set;/** * @Author ChengJianSheng * @Date 2021/6/12 * @see User * @see User */@Setterpublic class MyUserDetails implements UserDetails { private String username; private String password; private boolean enabled; private Set<SimpleGrantedAuthority> authorities; public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {  this.username = username;  this.password = password;  this.enabled = enabled;  this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() {  return authorities; } @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 enabled; }}

MyUserDetailsService

package com.cjs.example.service;import com.cjs.example.domain.MyUserDetails;import com.cjs.example.entity.SysMenuEntity;import com.cjs.example.entity.SysRoleEntity;import com.cjs.example.entity.SysUserEntity;import com.cjs.example.repository.SysUserRepository;import org.apache.commons.lang3.StringUtils;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.HashSet;import java.util.Set;import java.util.stream.Collectors;/** * @Author ChengJianSheng * @Date 2021/6/12 */@Servicepublic class MyUserDetailsService implements UserDetailsService { @Resource private SysUserRepository sysUserRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);  Set<SysRoleEntity> userRoles = sysUserEntity.getRoles();  Set<SysRoleEntity> deptRoles = sysUserEntity.getDept().getRoles();  Set<SysRoleEntity> roleSet = new HashSet<>();  roleSet.addAll(userRoles);  roleSet.addAll(deptRoles);  Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())    .filter(menu-> StringUtils.isNotBlank(menu.getCode()))    .map(SysMenuEntity::getCode)//    .map(e -> "ROLE_" + e.getCode())    .map(SimpleGrantedAuthority::new)    .collect(Collectors.toSet());  return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities); }}

如果加了"ROLE_"前缀,那么比较的时候应该用 SimpleGrantedAuthority 进行比较

这里姑且不加这个前缀了,因为后面集成 Activiti 的时候用户组有一个前缀 GROUP_

package com.cjs.example.service;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.stereotype.Component;import java.util.Set;import java.util.stream.Collectors;@Component("myAccessDecisionService")public class MyAccessDecisionService { public boolean hasPermission(String permission) {  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();  Object principal = authentication.getPrincipal();  if (principal instanceof UserDetails) {   UserDetails userDetails = (UserDetails) principal;   Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());   return set.contains(permission);//   // AuthorityUtils.createAuthorityList(permission);//   SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);//   return userDetails.getAuthorities().contains(simpleGrantedAuthority);  }  return false; }}

3.3.  自定义Token过滤器

package com.cjs.example.filter;import com.alibaba.fastjson.JSON;import com.cjs.example.domain.MyUserDetails;import org.apache.commons.lang3.StringUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.stereotype.Component;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.concurrent.TimeUnit;/** * @Author ChengJianSheng * @Date 2021/6/17 */@Componentpublic class TokenFilter extends OncePerRequestFilter { @Autowired private StringRedisTemplate stringRedisTemplate; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {  String token = request.getHeader("token");  String key = "TOKEN:" + token;  if (StringUtils.isNotBlank(token)) {   String value = stringRedisTemplate.opsForValue().get(key);   if (StringUtils.isNotBlank(value)) {    MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);    if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {     UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());     SecurityContextHolder.getContext().setAuthentication(authenticationToken);     // 刷新token     // 如果生存时间小于10分钟,则再续1小时     long time = stringRedisTemplate.getExpire(key);     if (time < 600) {      stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);     }    }   }  }  chain.doFilter(request, response); }}

3.3.  WebSecurityConfig

package com.cjs.example.config;import com.cjs.example.filter.TokenFilter;import com.cjs.example.handler.*;import com.cjs.example.service.MyUserDetailsService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/** * @Author ChengJianSheng * @Date 2021/6/12 */@EnableGlobalMethodSecurity(prePostEnabled = true)@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Autowired private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowired private MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowired private MyLogoutSuccessHandler myLogoutSuccessHandler; @Autowired private TokenFilter tokenFilter; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {  auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception {  http.formLogin()    .successHandler(myAuthenticationSuccessHandler)    .failureHandler(myAuthenticationFailureHandler)    .and()    .logout().logoutSuccessHandler(myLogoutSuccessHandler)    .and()    .authorizeRequests()    .antMatchers("/activiti7/login").permitAll()    .anyRequest().authenticated()    .and()    .exceptionHandling()    .accessDeniedHandler(new MyAccessDeniedHandler())    .authenticationEntryPoint(new MyAuthenticationEntryPoint())    .and()    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)    .maximumSessions(1)    .maxSessionsPreventsLogin(false)    .expiredSessionStrategy(new MyExpiredSessionStrategy());  http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);  http.csrf().disable(); } public PasswordEncoder passwordEncoder() {  return new BCryptPasswordEncoder(); }}

至此一切都很顺利,毕竟之前也写过很多遍。

package com.cjs.example.controller;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * @Author ChengJianSheng * @Date 2021/6/12 */@RestController@RequestMapping("/hello")public class HelloController { @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')") @GetMapping("/sayHello") public String sayHello() {  return "hello"; } @PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')") @GetMapping("/sayHi") public String sayHi() {  return "hi"; }}

4. 集成 Activiti7

启动项目以后,activiti相关表已经创建好了

接下来,以简单的请假为例来演示

<process id="leave" name="leave" isExecutable="true"> <startEvent id="startevent1" name="Start"></startEvent> <userTask id="usertask1" name="填写请假单" activiti:assignee="${sponsor}"></userTask> <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow> <endEvent id="endevent1" name="End"></endEvent> <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow> <userTask id="usertask2" name="经理审批" activiti:candidateGroups="${manager}"></userTask> <sequenceFlow id="flow3" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow> <endEvent id="endevent2" name="End"></endEvent> <sequenceFlow id="flow4" sourceRef="usertask2" targetRef="endevent2"></sequenceFlow></process>

4.1.  部署流程定义

package com.cjs.example.controller;import com.cjs.example.domain.RespResult;import com.cjs.example.util.ResultUtils;import lombok.extern.slf4j.Slf4j;import org.activiti.engine.RepositoryService;import org.activiti.engine.repository.Deployment;import org.activiti.engine.repository.ProcessDefinition;import org.apache.commons.io.IOUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.util.zip.ZipInputStream;/** * @Author ChengJianSheng * @Date 2021/7/12 */@Slf4j@RestController@RequestMapping("/deploy")public class DeploymentController { @Autowired private RepositoryService repositoryService; /**  * 部署  * @param file ZIP压缩包文件  * @param processName 流程名称  * @return  */ @PostMapping("/upload") public RespResult<String> upload(@RequestParam("zipFile") MultipartFile file, @RequestParam("processName") String processName) {  String originalFilename = file.getOriginalFilename();  if (!originalFilename.endsWith("zip")) {   return ResultUtils.error("文件格式错误");  }  ProcessDefinition processDefinition = null;  try {   ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream());   Deployment deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();   processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();  } catch (IOException e) {   log.error("流程部署失败!原因: {}", e.getMessage(), e);  }  return ResultUtils.success(processDefinition.getId()); } /**  * 查看流程图  * @param deploymentId 部署ID  * @param resourceName 图片名称  * @param response  * @return  */ @GetMapping("/getDiagram") public void getDiagram(@RequestParam("deploymentId") String deploymentId, @RequestParam("resourceName") String resourceName, HttpServletResponse response) {  InputStream inputStream = repositoryService.getResourceAsStream(deploymentId, resourceName);//  response.setContentType(MediaType.IMAGE_PNG_VALUE);  try {   IOUtils.copy(inputStream, response.getOutputStream());  } catch (IOException e) {   e.printStackTrace();  } finally {   IOUtils.closeQuietly(inputStream);  } }}

首先登录一下

 

然后,将流程图文件打成zip压缩包

查看流程图

4.2.  启动流程实例

最开始,我是这样写的

ProcessInstance processInstance = processRuntime.start(ProcessPayloadBuilder     .start()     .withProcessDefinitionId(processDefinitionId)     .withVariable("sponsor", authentication.getName())     .build());

当我这样写了以后,第一个问题出现了,没有权限访问

查看代码之后,我发现调用ProcessRuntime的方法需要当前登录用户有"ACTIVITI_USER" 权限

于是,我在数据库sys_menu表里加了一条数据

 

重新登录后,zhangsan可以调用ProcessRuntime里面的方法了

很快,第二个问题出现了, 当我用 ProcessRuntime#start() 启动流程实例的时候报错了

org.activiti.engine.ActivitiException: Query return 2 results instead of max 1	at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:213) ~[activiti-engine-7.1.0.M6.jar:na]	at org.activiti.engine.impl.DeploymentQueryImpl.executeSingleResult(DeploymentQueryImpl.java:30) ~[activiti-engine-7.1.0.M6.jar:na]

查看代码,终于找到问题所在了

 

这明显就是 Activiti 的Bug,查询所有部署的流程没有加任何查询条件,吐了

于是,百度了一下,网上有人建议换一个版本,于是我将activiti-spring-boot-starter的版本从"7.1.0.M6"换成了"7.1.0.M5",呵呵,又一个错,缺少字段

原来M6和M5的表结构不一样。我又将版本将至"7.1.0.M4",这次直接起不来了

没办法,版本改回7.1.0.M6,不用ProcessRuntime,改用原来的RuntimeService

package com.cjs.example.controller;import com.cjs.example.domain.RespResult;import com.cjs.example.util.ResultUtils;import org.activiti.api.process.model.ProcessInstance;import org.activiti.api.process.model.builders.ProcessPayloadBuilder;import org.activiti.api.process.runtime.ProcessRuntime;import org.activiti.engine.RuntimeService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.Authentication;import org.springframework.security.core.annotation.AuthenticationPrincipal;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;import java.util.Map;/** * @Author ChengJianSheng * @Date 2021/7/12 */@RestController@RequestMapping("/processInstance")public class ProcessInstan......

原文转载:http://www.shaoqun.com/a/876494.html

跨境电商:https://www.ikjzd.com/

赛兔:https://www.ikjzd.com/w/2375

史泰博:https://www.ikjzd.com/w/2112

亚马逊应用商店:https://www.ikjzd.com/w/531


1.前言实话实说,网上关于Activiti的教程千篇一律,有参考价值的不多。很多都是老早以前写的,基本都是直接照搬官方提供的示例,要么就是用单元测试跑一下,要么排除SpringSecurity,很少有看到一个完整的项目。太难了,笔者在实操的时候,遇到很多坑,在此做一个记录。其实,选择用Activiti7没别的原因,就是因为穷。但凡是有钱,谁还用开源版的啊,当然是用商业版啦。国外的工作流引擎没有考虑
达方物流:https://www.ikjzd.com/w/2562
去香港购物哪里好?:http://www.30bags.com/a/404841.html
去香港购物刷卡好还是现金好?香港刷卡是怎么扣钱的?:http://www.30bags.com/a/426397.html
去香港购物选购手机要注意什么?:http://www.30bags.com/a/404807.html
去香港国际机场DFS需要注意些什么?:http://www.30bags.com/a/404090.html
口述被三人干了一晚上 两个男人做我的详细过程:http://lady.shaoqun.com/a/248079.html
七十女人喜欢被㖭 进去后女人就不反抗了:http://www.30bags.com/m/a/249920.html
又粗又长我被老外玩晕了 老外把我女朋友啪肿了:http://www.30bags.com/m/a/249739.html
性生活强忍不射精好不好 小心逆行射精带来4个危害 :http://lady.shaoqun.com/a/421136.html
退款不退货!亚马逊卖家损失上万元且不断增加:https://www.ikjzd.com/articles/146621
精致好用的独居生物!你们是穿着最朴素的衣服生活的小仙女。:http://lady.shaoqun.com/a/421137.html
救赎——我出轨的时候,我老公就在隔壁:http://lady.shaoqun.com/a/421138.html