乐趣区

关于springsecurity:Spring-Security-Vue-Flowable-怎么玩

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

  • SpringBoot+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,如下:

@Service
public 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 申请解决起来会比拟麻烦:

@Configuration
public 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 办法:

@Transactional
public 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 我的项目配套视频来啦。

退出移动版