写点什么

Spring Security + Vue + Flowable 怎么玩?

作者:江南一点雨
  • 2022 年 8 月 22 日
    广东
  • 本文字数:8521 字

    阅读完需:约 28 分钟

Spring Security + Vue + Flowable 怎么玩?

@[toc]之前松哥发过一篇文章,和小伙伴们分享了 Spring Boot+Vue+Flowable 的具体玩法,传送门:



不过在那篇文章中,所有涉及到用户的地方,都是手动输入的,这显然不太好,例如开启一个请假流程:



这个流程中需要用户输入自己的姓名,其实如果当前用户登录了,就不用输入用户名了,直接就使用当前登录的用户名。


另一方面,当我们引入了用户系统之后,当用户提交请假申请的时候,也可以指定审批人,这样看起来就更真实了。


所以,今天我们就整篇文章,我们引入 Spring Security,据此来构建用户系统,一起来看下有了用户系统的流程引擎该是什么样子。

1. 效果展示

东西我已经做好了,先截个图给大家看下:



这个页面分了三部分:


  1. 最上面的是请假申请,用户只需要填入请假天数、请假理由,并且选择审批人即可,选择审批人的时候,可以直接指定审批人的名字,也可以选择审批人的角色,例如选择经理这个角色,那么将来只要角色为经理的任意用户登录成功之后,就可以看到自己需要审批的请假了。

  2. 中间的列表展示当前登录用户曾经提交过的请假申请,这些申请的状态分为三种,分别是已通过、已拒绝以及待审批。

  3. 下面的列表是这个用户需要审批的其他用户提交的请假申请,图片中这个用户暂无要审批的任务,如果有的话,这个地方会通过表格展示出来,表格中每一行有批准和拒绝两个按钮,点击之后就可以实现自己的操作了。


这就是我们这次要实现的效果了,相比于SpringBoot+Vue+Flowable,模拟一个请假审批流程!文章的案例,这次的显然看起来更像一回事,不过本文的案例是在上篇文章案例的基础上完成的,没看过上篇文章的小伙伴建议先看下上篇文章上篇文章中的案例,大家可以在微信公众号江南一点雨的后台回复 flowable02 获取。

2. 两套用户体系

玩过工作流的小伙伴应该都知道,工作流中其实自带了一套用户系统,但是我们自己的系统往往也有自己的用户体系,那么如何将两者融合起来呢?或者说是否有必要将两者融合起来呢?


如果你想将自己系统的用户体系和 flowable 中的用户体系融合起来,那么整体上来说,大概就是两种办法吧:


  1. 我们可以以自己系统中的用户体系为准(因为 flowable 自己的用户体系字段往往不能满足我们的需求),然后创建对应的视图即可。例如 flowable 中的用户表 ACT_ID_USER、分组表 ACT_ID_GROUP、用户分组关联表 ACT_ID_MEMBERSHIP 等等,把这些和用户体系相关的表删除掉,然后根据这些表的字段和名称,结合自己的系统用户,创建与之相同的视图。

  2. 利用 IdentityService 这个服务,当我们要操作自己的系统用户的时候,例如添加、更新、删除用户的时候,顺便调用 IdentityService 服务添加、更新、删除 flowable 中的用户。


这两种思路其实都不难,也都很好实现,但是有没有可能我们就直接舍弃掉 flowable 中的用户体系直接用自己的用户体系呢?在松哥目前的项目中,这条路目前是行得通的,就是将 flowable 的用户体系抛到一边,当做没有,只用自己系统的用户体系。


如果在读这篇文章的小伙伴中,有人在自己的系统中,有场景必须用到 flowable 自带的用户体系,欢迎留言讨论。


本文松哥和小伙伴们展示的案例,就是完完全全使用了自己的用户体系,没有用 flowable 中的那一套用户体系。


好啦,这个问题捋清楚了,接下来我们就开搞!

3. 创建用户表

首先我们来创建三张表,分别是用户表 user、角色表 role 以及用户角色关联表 user_role,脚本如下:


SET NAMES utf8mb4;
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `nameZh` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `role` (`id`, `name`, `nameZh`)VALUES (1,'manager','经理'), (2,'team_leader','组长');
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `user` (`id`, `username`, `password`)VALUES (1,'javaboy','{noop}123'), (2,'zhangsan','{noop}123'), (3,'lisi','{noop}123'), (4,'江南一点雨','{noop}123');
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `uid` int(11) DEFAULT NULL, `rid` int(11) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;INSERT INTO `user_role` (`id`, `uid`, `rid`)VALUES (1,1,1), (2,4,1), (3,2,2), (4,3,2);
复制代码


我也大概说下我的用户:


  1. 一共四个用户,分别是 javaboy、江南一点雨、zhangsan、lisi。

  2. 一共两个角色,分别是经理和组长。

  3. javaboy 和江南一点雨是经理,zhangsan 和 lisi 是组长。


这就是我的用户表了。

4. 配置系统登录

简单起见,我就不自己写系统登录页面了,简单配置一下 Spring Security 即可。


小伙伴们看到,第三小节中,我的用户密码用的是 {noop}123,这就表示我 Spring Security 加密方案用的是 DelegatingPasswordEncoder,Spring Security 原本默认的加密方案也是这个。不过当我们在项目中引入 flowable 的依赖 flowable-spring-boot-starter 之后,这个将 Spring Security 默认的 PasswordEncoder 改成了 NoOpPasswordEncoder,所以我需要首先在 applicaiton.properties 中重新指定 Spring Security 使用的 PasswordEncoder,配置方式如下:


flowable.idm.password-encoder=spring_delegating
复制代码


接下来提供一个用户类(涉及到 Spring Security 基本用法的我就不啰嗦了):


public class User implements UserDetails {    private Integer id;    private String username;    private String password;    private List<Role> roles;
@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; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (roles != null && roles.size() > 0) { return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList()); } return new ArrayList<>(); }
@Override public String getPassword() { return password; } //省略其他 getter/setter}
复制代码


提供一个自己的 UserDetailsService,如下:


@Servicepublic class MyUserDetailsService implements UserDetailsService {
@Autowired UserMapper userMapper;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user != null) { user.setRoles(userMapper.getUserRolesByUserId(user.getId())); } return user; }}
复制代码


一些基本的增删改查我就不展示了,这个 MyUserDetailsService 只需要注册到 Spring 容器中即可,也不需要额外的配置。


最后再简单配置一下 Spring Security,关闭掉 CSRF 攻击防御机制,否则将来的 POST 请求处理起来会比较麻烦:


@Configurationpublic class SecurityConfig {
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf().disable(); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); return http.build(); }}
复制代码


好啦,有了这些东西之后,以后再访问系统就必须得先登录才能访问了。

5. 修改流程图

接下来我们的流程图要改一下。


我们先来回顾一下上篇文章中的流程图:



大家看到,在这张图中,有两个 UserTask(就是有用户图标的那个),在上篇文章中,请假审批组固定是 managers(也就是说,如果用户属于 managers 这个组,就具备审批别人请假的权限),但是现在我们引入了用户体系,用户在提交请假申请的时候,用户可以指定自己的请假申请由谁审批!所以这个地方不能再硬编码了,应该改为动态的,根据前端传来的参数,来设置这个 UserTask 应该由谁来处理,具体修改内容如下:


<userTask id="approveTask" name="Approve or reject request">    <extensionElements>        <flowable:taskListener event="create" class="org.javaboy.flowable03.listener.SettingApproveUser"/>    </extensionElements></userTask>
复制代码


小伙伴们看到,我这里为这个 UserTask 添加了一个监听器,当系统执行到这个监听器的时候,我将在这个监听器中来设置这个 UserTask 由谁来处理,我们来看下这个监听器的内容:


public class SettingApproveUser implements TaskListener {    @Override    public void notify(DelegateTask delegateTask) {        String approveType = (String) delegateTask.getVariable("approveType");        if ("by_user".equals(approveType)) {            delegateTask.setAssignee((String) delegateTask.getVariable("approveUser"));        } else if ("by_role".equals(approveType)) {            Object approveRole = delegateTask.getVariable("approveRole");            delegateTask.addCandidateGroup((String) approveRole);        }    }}
复制代码


小伙伴们看到,这里涉及到了三个变量,分别是:


  • approveType:这个表示审批的类型,是指定一个审批人还是指定一个审批角色,其中 by_user 表示指定一个具体的审批人,by_role 表示指定一个审批角色。

  • approveUser:如果 approveType 的值是 by_user,那么就调用 setAssignee 来设置这个 UserTask 的处理人。

  • approveRole:如果 approveType 的值是 by_role,那么就调用 addCandidateGroup 方法来设置这个 UserTask 的候选组,也就是这个 UserTask 将来由这个组中的用户进行处理。


流程图中的第二个 UserTask 我们也改一下,不过这次只改一下名字,将来通过这个变量来传递一下流程的审批人即可,修改后的 UserTask 如下:


<userTask id="holidayApprovedTask" flowable:assignee="${approveUser}" name="Holiday approved"/>
复制代码


不过这个地方改不改其实都行,改一下更容易理解一些。


好啦,流程图我们现在就调整好啦。


接下来我们就来看具体功能的实现了。

6. 提交请假申请

6.1 页面设计

先来看看页面,选择审批人:



也可以选择审批角色:



从数据库中加载所有用户和角色,这个比较简单,我就不贴代码了,大家在公众号江南一点雨后台回复 flowable03 可以下载本文源代码。


基于上面展示的页面,当前端点击提交请假申请按钮的时候,我们提交的数据如下:


afl: {    days: 3,    reason: '休息一下',    approveType: 'by_user',    approveUser: '',    approveRole: '',},
复制代码


approveType 表示审批人的类型,by_user 是提交给一个具体的人,by_role 是提交给某一个角色。approveUser 和 approveRole 则表示具体的审批人或者审批角色,根据 approveType 的取值,后面这两个二选一。请求提交方法如下:


submit() {    let _this = this;    axios.post('/ask_for_leave', this.afl)        .then(function (response) {            if (response.data.status == 200) {                //提交成功                _this.$message.success(response.data.msg);                _this.search();            } else {                //提交失败                _this.$message.error(response.data.msg);            }        })        .catch(function (error) {            console.log(error);        });},
复制代码


如果请求提交成功,就调用 _this.search(); 方法去刷新一下历史请假列表。


前端比较简单,不啰嗦了。

6.2 后台处理

再来看看后端的处理。


先来看实体类 AskForLeaveVO,这个类用来接收前端传来的请假参数:


public class AskForLeaveVO {    private String name;    private Integer days;    private String reason;    private String approveType;    private String approveUser;    private String approveRole;    // 省略 getter/setter}
复制代码


再来看看请假接口的处理:


@PostMapping("/ask_for_leave")public RespBean askForLeave(@RequestBody AskForLeaveVO askForLeaveVO) {    return askForLeaveService.askForLeave(askForLeaveVO);}
复制代码


来看一下对应的 askForLeaveService#askForLeave 方法:


@Transactionalpublic RespBean askForLeave(AskForLeaveVO askForLeaveVO) {    Map<String, Object> variables = new HashMap<>();    askForLeaveVO.setName(SecurityContextHolder.getContext().getAuthentication().getName());    variables.put("name", askForLeaveVO.getName());    variables.put("days", askForLeaveVO.getDays());    variables.put("reason", askForLeaveVO.getReason());    variables.put("approveType", askForLeaveVO.getApproveType());    variables.put("approveUser", askForLeaveVO.getApproveUser());    variables.put("approveRole", askForLeaveVO.getApproveRole());    try {        runtimeService.startProcessInstanceByKey("holidayRequest", askForLeaveVO.getName(), variables);        return RespBean.ok("已提交请假申请");    } catch (Exception e) {        e.printStackTrace();    }    return RespBean.error("提交申请失败");}
复制代码


大家注意,这里提交请假的用户的用户名,不再是前端传来的,而是我们从当前登录用户中提取出来的。


当我们在这里启动流程之后,会自动执行到第 5 小节所说的那个 UserTask 中,并在监听器中为 UserTask 设置处理的用户或者角色。

7. 历史请假列表

这个在我们上篇文章的案例中,是用户手动输入要查询的用户名,然后去查询的,现在有了登录系统之后,用户登录成功之后,系统就知道当前用户是谁了,直接根据当前登录用户名去查询历史流程信息就可以了,如下:


public RespBean searchResult() {    String name = SecurityContextHolder.getContext().getAuthentication().getName();    List<HistoryInfo> infos = new ArrayList<>();    List<HistoricProcessInstance> historicProcessInstances = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(name)            .orderByProcessInstanceStartTime().desc().list();    for (HistoricProcessInstance historicProcessInstance : historicProcessInstances) {        HistoryInfo historyInfo = new HistoryInfo();        historyInfo.setStatus(3);        Date startTime = historicProcessInstance.getStartTime();        Date endTime = historicProcessInstance.getEndTime();        List<HistoricVariableInstance> historicVariableInstances = historyService.createHistoricVariableInstanceQuery()                .processInstanceId(historicProcessInstance.getId())                .list();        for (HistoricVariableInstance historicVariableInstance : historicVariableInstances) {            String variableName = historicVariableInstance.getVariableName();            Object value = historicVariableInstance.getValue();            if ("reason".equals(variableName)) {                historyInfo.setReason((String) value);            } else if ("days".equals(variableName)) {                historyInfo.setDays(Integer.parseInt(value.toString()));            } else if ("approved".equals(variableName)) {                Boolean v = (Boolean) value;                if (v) {                    historyInfo.setStatus(1);                }else{                    historyInfo.setStatus(2);                }            } else if ("name".equals(variableName)) {                historyInfo.setName((String) value);            } else if ("approveUser".equals(variableName)) {                historyInfo.setApproveUser((String) value);            }        }        historyInfo.setStartTime(startTime);        historyInfo.setEndTime(endTime);        infos.add(historyInfo);    }    return RespBean.ok("ok", infos);}
复制代码


这里代码量虽然有点多,但是其实很好理解,就是我们先获取到当前登录的用户名,以这个用户名作为 BusinessKey 去查询所有的流程,包括已经执行结束和未结束的流程,然后再更进一步查询这些流程的变量信息。


对于一个已经执行结束的流程而言,流程变量中包含有 approved(这是审批的时候传入的),approved 的值无外乎就是 true 或者 false,对于一个尚未审批的流程而言,也就是还没执行结束的流程而言,流程变量中不含有 approved,所以在上面这段代码中,我首先设置 historyInfo 的 status 值为 3 表示流程未审批,将来要是读到了 approved 的值,那就据实设置 status 为 1 表示流程审批通过,设置 status 为 2 表示流程审批未通过。


前端在 el-table 表格中显示的时候,也是根据 status 的值分别展示不同的内容。

8. 待审批列表

这个是查看当前登录用户需要审批的任务,在上篇文章的案例中,我们是用户手动输入一个用户名,然后查询这个用户需要审批的任务列表。


现在不用这么麻烦了,用户登录成功之后,就可以直接查询当前用户的待审批的任务列表了:


@GetMapping("/list")public RespBean leaveList() {    return askForLeaveService.leaveList();}
复制代码


来看下具体的查询方法:


/** * 待审批列表 * * @return */public RespBean leaveList() {    String identity = SecurityContextHolder.getContext().getAuthentication().getName();    //找到所有分配给你的任务    List<Task> tasks = taskService.createTaskQuery().taskAssignee(identity).list();    //找到所有分配给你所属角色的任务    Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();    for (GrantedAuthority authority : authorities) {        tasks.addAll(taskService.createTaskQuery().taskCandidateGroup(authority.getAuthority()).list());    }    List<Map<String, Object>> list = new ArrayList<>();    for (int i = 0; i < tasks.size(); i++) {        Task task = tasks.get(i);        Map<String, Object> variables = taskService.getVariables(task.getId());        variables.put("id", task.getId());        list.add(variables);    }    return RespBean.ok("加载成功", list);}
复制代码


这个查询分两步:


  1. 根据当前登录用户的名字查询这个用户需要处理的任务。

  2. 根据当前登录用户的角色查询这个角色需要处理的任务。


最后将查询到的结果合并到一起,返回给前端就完事了。


好了,到此,我们的改造基本上就完成了。我主要是和大家说了实现的思路,具体的一些代码细节,大家可以在公众号江南一点雨后台回复 flowable03 下载。

9. 测试

来个简单的测试吧。


首先,zhangsan 登录,登录之后提交一个请假申请,要求经理审批:



可以看到,提交请假申请之后,下面的历史请假列表中就会展示出刚刚提交的请假申请,并且状态为待审批。


接下来注销登录,用 javaboy 或者江南一点雨登录,因为这两个用户都是经理,所以他俩中任意一个登录,都可以看到 zhangsan 刚刚提交的请假审批,以江南一点雨登录为例:



可以看到,最下面的列表中有 zhangsan 刚刚提交的请假申请,点击批准,然后再以 zhangsan 的身份重新登录,如下:



可以看到,请假已经审批通过啦~


好啦,本文就先和小伙伴们分享这么多,flowable 将来我也会录制一些视频放在 TienChin 项目中,感兴趣的小伙伴戳戳戳这里:TienChin 项目配套视频来啦

发布于: 刚刚阅读数: 3
用户头像

技术宅 2019.04.09 加入

Java猿

评论

发布
暂无评论
Spring Security + Vue + Flowable 怎么玩?_Java_江南一点雨_InfoQ写作社区