SSM项目实战-云尚办公系统(中)

尚硅谷-云尚办公系统

一、权限管理

一、权限管理

在这里插入图片描述

1、权限管理介绍

每个系统的权限功能都不尽相同,各有其自身的业务特点,对权限管理的设计也都各有特色。不过不管是怎样的权限设计,大致可归为三种:页面权限(菜单级)、操作权限(按钮级)、数据权限。当前系统只是讲解:菜单权限与按钮权限的控制。

1.1、菜单权限

菜单权限就是对页面的控制,就是有这个权限的用户才能访问这个页面,没这个权限的用户就无法访问,它是以整个页面为维度,对权限的控制并没有那么细,所以是一种粗颗粒权限
在这里插入图片描述

1.2、按钮权限

按钮权限就是将页面的操作视为资源,比如删除操作,有些人可以操作有些人不能操作。对于后端来说,操作就是一个接口。于前端来说,操作往往是一个按钮,是一种细颗粒权限

在这里插入图片描述

1.3、权限管理设计思路

前面我们讲解了用户管理、角色管理及菜单管理,我们把菜单权限分配给角色,把角色分配给用户,那么用户就拥有了角色的所有权限(权限包含:菜单权限与按钮权限)。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

接下来需要实现这两个接口:

1、用户登录

2、登录成功根据token获取用户相关信息(菜单权限及按钮权限数据等)

用户登录我们需要用到JWT,接下来讲解JWT。

2、JWT

2.1、JWT介绍

JWT是JSON Web Token的缩写,即JSON Web令牌,是一种自包含令牌。 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。

JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

JWT最重要的作用就是对 token信息的防伪作用。

2.2、JWT令牌的组成

一个JWT由三个部分组成:JWT头、有效载荷、签名哈希
最后由这三者组合进行base64url编码得到JWT

典型的,一个JWT看起来如下图:该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
https://jwt.io/

在这里插入图片描述

JWT头

JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。

{
  "alg": "HS256",
  "typ": "JWT"
}

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);

typ属性表示令牌的类型,JWT令牌统一写为JWT。

最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss: jwt签发者
sub: 主题
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{
  "name": "Helen",
  "role": "editor",
  "avatar": "helen.jpg"
}

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)    ==>   签名hash

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+“,”/“和”=“,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。

2.3、项目集成JWT

操作模块:common-util

2.3.1、 引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>
2.3.2、 添加JWT帮助类

在这里插入图片描述

package com.atguigu.common.jwt;

import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;

import java.util.Date;

/**
 * jwt工具类
 */
public class JwtHelper {
    private static long tokenExpiration = 365 * 24 * 60 * 60 * 1000;//token有效时间
    private static String tokenSignKey = "123456";//签名密钥

    //根据用户id和用户名称生成token字符串
    public static String createToken(Long userId, String username) {
        String token = Jwts.builder()
                //分类
                .setSubject("AUTH-USER")
                //设置有效时长
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                //设置主体部分
                .claim("userId", userId)
                .claim("username", username)
                //设置签名部分
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
        return token;
    }

    //从生成token字符串获取用户id
    public static Long getUserId(String token) {
        try {
            if (StringUtils.isEmpty(token)) return null;

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            Integer userId = (Integer) claims.get("userId");
            return userId.longValue();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //从生成token字符串获取用户名称
    public static String getUsername(String token) {
        try {
            if (StringUtils.isEmpty(token)) return "";

            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return (String) claims.get("username");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
        String token = JwtHelper.createToken(1L, "admin");
        System.out.println(token);
        System.out.println(JwtHelper.getUserId(token));
        System.out.println(JwtHelper.getUsername(token));
    }
}

3、用户登录

修改登陆方法之前有处地方需要修改:
在这里插入图片描述
需要使用的MD5加密方法:
在这里插入图片描述

package com.atguigu.common.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public final class MD5 {

    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }

    public static void main(String[] args) {
        System.out.println(MD5.encrypt("111111"));
    }
}

3.1、修改登录方法

修改IndexController类登录方法
在这里插入图片描述

 /**
     * 登录
     * @return
     */
    @PostMapping("login")//使用RequestBody之后,这里必须使用Post提交方式,因为Get提交方式没有请求体
    public Result login(@RequestBody LoginVo loginVo) {
//        Map<String, Object> map = new HashMap<>();
//        map.put("token","admin");
//        return Result.ok(map);

//        1、获取输入的用户名和密码
        String username=loginVo.getUsername();

//        2、根据用户名查询数据库
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUsername,username);
        SysUser sysUser = sysUserService.getOne(wrapper);

//        3、用户信息是否存在
        if (sysUser==null){
            throw new GuiguException(201,"用户不存在");
        }

//        4、判断密码
        //数据库密码(MD5)
        String password_db = sysUser.getPassword();
        //获取输入的密码,并进行MD5加密,以便与数据库密码进行比对
        String password_input = MD5.encrypt(loginVo.getPassword());
        if (!password_db.equals(password_input)){
            throw new GuiguException(201,"密码错误");
        }
//        5、判断用户是否被禁用,1可用,0禁用
        if (sysUser.getStatus().intValue()==0){
            throw new GuiguException(201,"用户被禁用");
        }

//        6、使用jwt根据用户id和用户名称生成token字符串
        String token =
                JwtHelper.createToken(sysUser.getId(), sysUser.getUsername());

//        7、返回
        Map<String,Object> map=new HashMap<>();
        map.put("token",token);
        return Result.ok(map);
    }

4、获取用户信息

接口数据:

Map<String, Object> map = new HashMap<>();
map.put("roles","[admin]");
map.put("name","admin");
map.put("avatar","https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
map.put("buttons", new ArrayList<>());
map.put("routers", new ArrayList<>());

说明:主要是获取当前登录用户的菜单权限以及按钮权限数据

4.1、获取用户菜单权限

说明:获取菜单权限数据,我们要将菜单数据构建成路由数据结构

4.1.1、定义接口

SysMenuService类

/**
 * 获取用户菜单
 * @param userId
 * @return
 */
List<RouterVo> findUserMenuList(Long userId);
4.1.2、接口实现

其中包括buildRouter方法:根据菜单构建路由结构
getRouterPath:获取路由地址


    //构建成框架要求的路由结构
    private List<RouterVo> buildRouter(List<SysMenu> menus) {
        //创建list集合,存储最终数据
        List<RouterVo> routers=new ArrayList<>();
        //menus遍历
        for (SysMenu sysMenu:menus){
            RouterVo router = new RouterVo();
            router.setHidden(false);
            router.setAlwaysShow(false);
            router.setPath(getRouterPath(sysMenu));
            router.setComponent(sysMenu.getComponent());
            router.setMeta(new MetaVo(sysMenu.getName(), sysMenu.getIcon()));
            routers.add(router);

            //下一层数据
            List<SysMenu> children = sysMenu.getChildren();
            if (sysMenu.getType().intValue()==2){
                //加载出来下面隐藏路由
                List<SysMenu> hiddenMenuList = children.stream()
                        .filter(item -> !StringUtils.isEmpty(item.getComponent()))
                        .collect(Collectors.toList());
                for (SysMenu hiddenMenu : hiddenMenuList) {
                    RouterVo hiddenRouter = new RouterVo();
                    //true 隐藏路由
                    hiddenRouter.setHidden(true);
                    hiddenRouter.setAlwaysShow(false);
                    hiddenRouter.setPath(getRouterPath(hiddenMenu));
                    hiddenRouter.setComponent(hiddenMenu.getComponent());
                    hiddenRouter.setMeta(new MetaVo(hiddenMenu.getName(), hiddenMenu.getIcon()));
                    routers.add(hiddenRouter);
                }
            }else{
                if (!CollectionUtils.isEmpty(children)) {
                    if(children.size() > 0) {
                        router.setAlwaysShow(true);
                    }
                    //递归
                    router.setChildren(buildRouter(children));
                }
            }
            routers.add(router);
        }
        return routers;
    }


    /**
     * 获取路由地址
     *
     * @param menu 菜单信息
     * @return 路由地址
     */
    public String getRouterPath(SysMenu menu) {
        String routerPath = "/" + menu.getPath();
        if(menu.getParentId().intValue() != 0) {
            routerPath = menu.getPath();
        }
        return routerPath;
    }
4.1.3、添加mapper接口

SysMenuMapper类

List<SysMenu> findListByUserId(@Param("userId") Long userId);
4.1.4、添加xml方法

新建SysMenuMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">


<mapper namespace="com.atguigu.system.mapper.SysMenuMapper">

   <resultMap id="sysMenuMap" type="com.atguigu.model.system.SysMenu" autoMapping="true">
   </resultMap>

   <!-- 用于select查询公用抽取的列 -->
   <sql id="columns">
      m.id,m.parent_id,m.name,m.type,m.path,m.component,m.perms,m.icon,m.sort_value,m.status,m.create_time,m.update_time,m.is_deleted
   </sql>


    <select id="findListByUserId" resultMap="sysMenuMap">
      select
      distinct <include refid="columns" />
      from sys_menu m
      inner join sys_role_menu rm on rm.menu_id = m.id
      inner join sys_user_role ur on ur.role_id = rm.role_id
      where
      ur.user_id = #{userId}
      and m.status = 1
      and rm.is_deleted = 0
      and ur.is_deleted = 0
      and m.is_deleted = 0
   </select>

</mapper>
4.2、获取用户按钮权限

说明:只需要获取按钮标识即可

4.2.1、定义接口

SysMenuService类

    List<String> findUserPermsByUserId(Long userId);

4.2.2、接口实现
//根据用户id获取用户可以操作按钮列表
    @Override
    public List<String> findUserPermsByUserId(Long userId) {

        List<SysMenu> sysMenuList = null;
        if (userId.longValue() == 1) {//1 判断是否是管理员,如果是管理员,查询所有按钮列表
            sysMenuList = this.list(new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getStatus, 1));
        } else {
            //2 如果不是管理员,根据userId查询可以操作按钮列表
            //多表关联查询:用户角色关系表、角色菜单关系表、菜单表
            sysMenuList=baseMapper.findMenuListByUserId(userId);
        }
        //3 从查询出的数据中获取可以操作按钮的值
        List<String> permsList = sysMenuList.stream()
                .filter(item -> item.getType() == 2)
                .map(item -> item.getPerms()).collect(Collectors.toList());
        return permsList;


    }

knife4j测试:
http://localhost:8800/doc.html
可能遇到问题:
在这里插入图片描述
第二种解决方法:
1、将下述内容进行添加

<resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes> <include>**/*.yml</include>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>

在这里插入图片描述

2、application-dev.yml添加
mybatis-plus:
mapper-locations: classpath:com/atguigu/auth/mapper/xml/*.xml

注意:文件后缀需要对应
在这里插入图片描述
在这里插入图片描述

5、前后端联调

前端配置代码链接:
https://blog.csdn.net/weixin_44690215/article/details/138188582?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22138188582%22%2C%22source%22%3A%22weixin_44690215%22%7D
在这里插入图片描述
暂时没解决!

6、总结

当前我们已经实现前端菜单及按钮的权限控制,服务器端还没加任何控制,那么服务器端怎么控制呢?其实很简单,就是要在页面按钮对应的controller方法上面加对应的权限控制,即在进入controller方法前判断当前用户是否有访问权限。

怎么实现呢?如果我们自己实现,那么肯定想到的就是Fillter加Aop就可以实现,有现成的开源技术框架吗?答案是肯定的,如:Spring Security、Shiro等一系列开源框架可供选择。

二、Spring Security介绍

1、Spring Security简介

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

正如你可能知道的关于安全方面的两个核心功能是“认证”和“授权”,一般来说,Web 应用的安全性包括**用户认证(Authentication)和用户授权(Authorization)**两个部分,这两点也是 SpringSecurity 重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。

通俗点说就是系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

通俗点讲就是系统判断用户是否有权限去做某些事情。

2、同款产品对比

3.1、Spring Security

Spring 技术栈的组成部分。

https://spring.io/projects/spring-security

通过提供完整可扩展的认证和授权支持保护你的应用程序。

SpringSecurity 特点:

⚫ 和 Spring 无缝整合。

⚫ 全面的权限控制。

⚫ 专门为 Web 开发而设计。

​ ◼旧版本不能脱离 Web 环境使用。

​ ◼新版本对整个框架进行了分层抽取,分成核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。

⚫ 重量级。

3.2、 Shiro

Apache 旗下的轻量级权限控制框架。

特点:

⚫ 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。

⚫ 通用性。

​ ◼好处:不局限于 Web 环境,可以脱离 Web 环境使用。

​ ◼缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。

三、Spring Security实现权限

要对Web资源进行保护,最好的办法莫过于Filter
要想对方法调用进行保护,最好的办法莫过于AOP

Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。

img

如图所示,一个请求想要访问到API就会从左到右经过蓝线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。

在这里插入图片描述

这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限授权。

说明:Spring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!这个框架的使用方式就是对这些过滤器和组件进行扩展。

1、Spring Security入门

我们在现有的项目基础上做集成,Spring Security权限控制部分也是公共模块,后续哪个service服务模块需要,直接引入即可。

后续我们的Spring Cloud微服务项目可能就基于该权限系统开发,因此我们要做好技术扩展。

1.1、创建spring-security模块

在这里插入图片描述

在common模块下创建spring-security公共模块,创建方式如:service-util模块

1.2、添加依赖

修改pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.atguigu</groupId>
        <artifactId>common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>spring-security</artifactId>
    
    <dependencies>
        <dependency>
            <groupId>com.atguigu</groupId>
            <artifactId>common-util</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<scope>provided </scope>
		</dependency>
    </dependencies>

</project>

说明:依赖包(spring-boot-starter-security)导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:

​ 1、要求经过身份验证的用户才能与应用程序进行交互

​ 2、创建好了默认登录表单

​ 3、生成用户名为user的随机密码并打印在控制台上

​ 等等…

1.3、添加配置类
package com.atguigu.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

}
1.4、service-oa模块引入

在service-oa引入权限模块,将依赖添加到pom.mxl文件

<dependency>
    <groupId>com.atguigu</groupId>
    <artifactId>spring-security</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
1.5、启动项目测试

在浏览器访问:http://localhost:8800/admin/system/sysRole/findAll

自动跳转到了登录页面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

默认的用户名:user

密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!

在这里插入图片描述

输入用户名,密码,成功访问到controller方法并返回数据,说明Spring Security默认安全保护生效。

在实际开发中,这些默认的配置是不能满足我们需要的,我们需要扩展Spring Security组件,完成自定义配置,实现我们的项目需求。

2、用户认证

用户认证流程
在这里插入图片描述

2.1、用户认证核心组件

我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的,这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。

我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取AuthenticationSecurityContext就是我们的上下文对象!这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolder原理非常简单,就是使用ThreadLocal来保证一个线程中传递同一个对象!

现在我们已经知道了Spring Security中三个核心组件:

​ 1、Authentication:存储了认证信息,代表当前登录用户

​ 2、SeucirtyContext:上下文对象,用来获取Authentication

​ 3、SecurityContextHolder:上下文管理对象,用来在程序任何地方获取SecurityContext

Authentication中是什么信息呢:

​ 1、Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象

​ 2、Credentials:用户凭证,一般是密码

​ 3、Authorities:用户权限

2.2、用户认证

Spring Security是怎么进行用户认证的呢?

AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的,该过滤器负责认证逻辑。

Spring Security用户认证关键代码如下:

// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

下面我们来分析一下。

2.2.1、认证接口分析

AuthenticationManager的校验逻辑非常简单:

根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。

这个逻辑没啥好说的,再简单不过了。重点是这里每一个步骤Spring Security都提供了组件:

​ 1、是谁执行 根据用户名查询出用户对象 逻辑的呢?用户对象数据可以存在内存中、文件中、数据库中,你得确定好怎么查才行。这一部分就是交由**UserDetialsService** 处理,该接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。

​ 2、那查询出来的 用户对象 又是什么呢?每个系统中的用户对象数据都不尽相同,咱们需要确认我们的用户数据是啥样的才行。Spring Security中的用户数据则是由**UserDetails** 来体现,该接口中提供了账号、密码等通用属性。

​ 3、对密码进行校验大家可能会觉得比较简单,if、else搞定,就没必要用什么组件了吧?但框架毕竟是框架考虑的比较周全,除了if、else外还解决了密码加密的问题,这个组件就是**PasswordEncoder**,负责密码加密与校验。

我们可以看下AuthenticationManager校验逻辑的大概源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...省略其他代码

    // 传递过来的用户名
    String username = authentication.getName();
    // 调用UserDetailService的方法,通过用户名查询出用户对象UserDetail(查询不出来UserDetailService则会抛出异常)
    UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username);
    String presentedPassword = authentication.getCredentials().toString();

    // 传递过来的密码
    String password = authentication.getCredentials().toString();
    // 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配
    if (!passwordEncoder.matches(password, userDetails.getPassword())) {
        // 密码错误则抛出异常
        throw new BadCredentialsException("错误信息...");
    }

    // 注意哦,这里返回的已认证Authentication,是将整个UserDetails放进去充当Principal
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails,
            authentication.getCredentials(), userDetails.getAuthorities());
    return result;

...省略其他代码
}

UserDetialsServiceUserDetailsPasswordEncoder,这三个组件Spring Security都有默认实现,这一般是满足不了我们的实际需求的,所以这里我们自己来实现这些组件!

下面我们就在项目里面来实现用户认证。
在这里插入图片描述

2.2.3、加密器PasswordEncoder

在这里插入图片描述

加密我们项目采取MD5加密

操作模块:spring-security模块

自定义加密处理组件:CustomMd5PasswordEncoder

package com.atguigu.security.custom;

import com.atguigu.common.utils.MD5;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
//自定义加密器
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return MD5.encrypt(rawPassword.toString());
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
    }
}

2.2.4、用户对象UserDetails

该接口就是我们所说的用户对象,它提供了用户的一些通用属性,源码如下:

public interface UserDetails extends Serializable {
	/**
     * 用户权限集合(这个权限对象现在不管它,到权限时我会讲解)
     */
    Collection<? extends GrantedAuthority> getAuthorities();
    /**
     * 用户密码
     */
    String getPassword();
    /**
     * 用户名
     */
    String getUsername();
    /**
     * 用户没过期返回true,反之则false
     */
    boolean isAccountNonExpired();
    /**
     * 用户没锁定返回true,反之则false
     */
    boolean isAccountNonLocked();
    /**
     * 用户凭据(通常为密码)没过期返回true,反之则false
     */
    boolean isCredentialsNonExpired();
    /**
     * 用户是启用状态返回true,反之则false
     */
    boolean isEnabled();
}

实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:

操作模块:spring-security模块

添加CustomUser对象

package com.atguigu.security.custom;

import com.atguigu.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

public class CustomUser extends User {

    /**
     * 我们自己的用户实体对象,要调取用户信息时直接获取这个实体对象。(这里我就不写get/set方法了)
     */
    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }

    public SysUser getSysUser() {
        return sysUser;
    }

    public void setSysUser(SysUser sysUser) {
        this.sysUser = sysUser;
    }
}

2.2.5 业务对象UserDetailsService

在这里插入图片描述

该接口很简单只有一个方法:

public interface UserDetailsService {
    /**
     * 根据用户名获取用户对象(获取不到直接抛异常)
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

我们实现该接口,就完成了自己的业务

操作模块:service-oa

添加UserDetailsServiceImpl类,实现UserDetailsService接口

package com.atguigu.auth.service.impl;

import com.atguigu.auth.service.SysUserService;
import com.atguigu.model.system.SysUser;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.security.custom.UserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getByUsername(username);
        if(null == sysUser) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        if(sysUser.getStatus().intValue() == 0) {
            throw new RuntimeException("账号已停用");
        }
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

AuthenticationManager校验所调用的三个组件我们就已经做好实现了!

2.2.6、自定义用户认证接口
package com.atguigu.security.fillter;

import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import com.atguigu.security.custom.CustomUser;
import com.atguigu.vo.system.LoginVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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.HashMap;
import java.util.Map;

/**
 * <p>
 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,对用户名密码进行登录校验
 * </p>
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    public TokenLoginFilter(AuthenticationManager authenticationManager) {
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        if(e.getCause() instanceof RuntimeException) {
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}

添加工具类:ResponseUtil

添加模块:common-util

package com.atguigu.common.util;

import com.atguigu.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResponseUtil {

    public static void out(HttpServletResponse response, Result r) {
        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
        try {
            mapper.writeValue(response.getWriter(), r);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
2.2.7、认证解析token

因为用户登录状态在token中存储在客户端,所以每次请求接口请求头携带token, 后台通过自定义token过滤器拦截解析token完成认证并填充用户信息实体。

package com.atguigu.security.fillter;

import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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.Collections;

/**
 * <p>
 * 认证解析token过滤器
 * </p>
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    public TokenAuthenticationFilter() {

    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
            String useruame = JwtHelper.getUsername(token);
            logger.info("useruame:"+useruame);
            if (!StringUtils.isEmpty(useruame)) {
                return new UsernamePasswordAuthenticationToken(useruame, null, Collections.emptyList());
            }
        }
        return null;
    }
}
2.2.8、配置用户认证

修改WebSecurityConfig配置类

package com.atguigu.security.config;

import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.filter.TokenAuthenticationFilter;
import com.atguigu.security.filter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;


    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf跨站请求伪造
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                .antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager()));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
    auth.userDetailsService(userDetailsService)
        .passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}

说明:

1、我们是前后端分离项目,使用jwt生成token ,即用户状态保存在客户端中,前后端交互通过api接口 无session生成,所以我们不需要配置formLogin,session禁用

2、在浏览器访问:http://localhost:8800/admin/system/sysRole/findAll

{
    "code": 209,
    "message": "没有权限",
    "data": null
}
2.2.9、通过swagger测试登录

在相应的自定义组件设置断点,查看是否按照预期执行。

1、先输入正确的用户名与密码

2、输入错误的用户名与密码

结论:跟预期一致

3、用户授权

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。
总体思路
在这里插入图片描述
在这里插入图片描述

SpringSecurity中的Authentication类:

public interface Authentication extends Principal, Serializable {
	//权限数据列表
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

前面登录时执行loadUserByUsername方法时,return new CustomUser(sysUser, Collections.emptyList());后面的空数据对接就是返回给Spring Security的权限数据。

在TokenAuthenticationFilter中怎么获取权限数据呢?登录时我们把权限数据保存到redis中(用户名为key,权限数据为value即可),这样通过token获取用户名即可拿到权限数据,这样就可构成出完整的Authentication对象。

3.1、修改loadUserByUsername接口方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getByUsername(username);
    if(null == sysUser) {
        throw new UsernameNotFoundException("用户名不存在!");
    }

    if(sysUser.getStatus().intValue() == 0) {
        throw new GuiguException(ResultCodeEnum.ACCOUNT_STOP);
    }
    List<String> userPermsList = sysMenuService.findUserPermsList(sysUser.getId());
    List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    for (String perm : userPermsList) {
        authorities.add(new SimpleGrantedAuthority(perm.trim()));
    }
    return new CustomUser(sysUser, authorities);
}
3.2、spring-security模块配置redis

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3、修改TokenLoginFilter登录成功方法

登录成功我们将权限数据保单到reids

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
        this.setAuthenticationManager(authenticationManager);
        this.setPostOnly(false);
        //指定登录接口及提交方式,可以指定任意路径
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/system/index/login","POST"));
        this.redisTemplate = redisTemplate;
    }

    /**
     * 登录认证
     * @param req
     * @param res
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        try {
            LoginVo loginVo = new ObjectMapper().readValue(req.getInputStream(), LoginVo.class);

            Authentication authenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
            return this.getAuthenticationManager().authenticate(authenticationToken);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 登录成功
     * @param request
     * @param response
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        CustomUser customUser = (CustomUser) auth.getPrincipal();
        String token = JwtHelper.createToken(customUser.getSysUser().getId(), customUser.getSysUser().getUsername());
        //保存权限数据
        redisTemplate.opsForValue().set(customUser.getUsername(), JSON.toJSONString(customUser.getAuthorities()));

        Map<String, Object> map = new HashMap<>();
        map.put("token", token);
        ResponseUtil.out(response, Result.ok(map));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {

        if(e.getCause() instanceof RuntimeException) {
            ResponseUtil.out(response, Result.build(null, 204, e.getMessage()));
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.LOGIN_MOBLE_ERROR));
        }
    }
}
3.4、修改TokenAuthenticationFilter

认证是从redis里面获取权限数据

完整代码:

package com.atguigu.security.fillter;

import com.alibaba.fastjson.JSON;
import com.atguigu.common.jwt.JwtHelper;
import com.atguigu.common.result.Result;
import com.atguigu.common.result.ResultCodeEnum;
import com.atguigu.common.util.ResponseUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
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.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 认证解析token过滤器
 * </p>
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        logger.info("uri:"+request.getRequestURI());
        //如果是登录接口,直接放行
        if("/admin/system/index/login".equals(request.getRequestURI())) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        if(null != authentication) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } else {
            ResponseUtil.out(response, Result.build(null, ResultCodeEnum.PERMISSION));
        }
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        logger.info("token:"+token);
        if (!StringUtils.isEmpty(token)) {
            String username = JwtHelper.getUsername(token);
            logger.info("useruame:"+username);
            if (!StringUtils.isEmpty(username)) {
                String authoritiesString = (String) redisTemplate.opsForValue().get(useruame);
                List<Map> mapList = JSON.parseArray(authoritiesString, Map.class);
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for (Map map : mapList) {
                    authorities.add(new SimpleGrantedAuthority((String)map.get("authority")));
                }
                return new UsernamePasswordAuthenticationToken(useruame, null, authorities);
                } else {
                    return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                }
            }
        }
        return null;
    }
}
3.5、修改配置类

修改WebSecurityConfig类

配置类添加注解:

开启基于方法的安全认证机制,也就是说在web层的controller启用注解机制的安全确认

@EnableGlobalMethodSecurity(prePostEnabled = true)

添加注入bean:

@Autowired
private RedisTemplate redisTemplate;

添加参数:

连个fillter添加redisTemplate参数

完整代码如下:

package com.atguigu.security.config;

import com.atguigu.security.custom.CustomMd5PasswordEncoder;
import com.atguigu.security.fillter.TokenAuthenticationFilter;
import com.atguigu.security.fillter.TokenLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解功能,默认禁用注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomMd5PasswordEncoder customMd5PasswordEncoder;

    @Autowired
    private RedisTemplate redisTemplate;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
        http
                //关闭csrf
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登陆接口肯定是不需要认证的
                //.antMatchers("/admin/system/index/login").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                //TokenAuthenticationFilter放到UsernamePasswordAuthenticationFilter的前面,这样做就是为了除了登录的时候去查询数据库外,其他时候都用token进行认证。
                .addFilterBefore(new TokenAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
                .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate));

        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 指定UserDetailService和加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(customMd5PasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * 排除swagger相关请求
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/favicon.ico","/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/doc.html");
    }
}
3.6、service-oa模块添加redis配置

application-dev.yml配文件

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 1800000
    password:
    jedis:
      pool:
        max-active: 20 #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 5    #最大空闲
        min-idle: 0     #最小空闲
3.7、控制controller层接口权限

Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,来判断用户对某个控制层的方法是否具有访问权限

通过@PreAuthorize标签控制controller层接口权限

public class SysRoleController {

    @Autowired
    private SysRoleService sysRoleService;

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @ApiOperation(value = "获取分页列表")
    @GetMapping("{page}/{limit}")
    public Result index(
            @ApiParam(name = "page", value = "当前页码", required = true)
            @PathVariable Long page,

            @ApiParam(name = "limit", value = "每页记录数", required = true)
            @PathVariable Long limit,

            @ApiParam(name = "roleQueryVo", value = "查询对象", required = false)
                    SysRoleQueryVo roleQueryVo) {
        Page<SysRole> pageParam = new Page<>(page, limit);
        IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo);
        return Result.ok(pageModel);
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.list')")
    @ApiOperation(value = "获取")
    @GetMapping("get/{id}")
    public Result get(@PathVariable Long id) {
        SysRole role = sysRoleService.getById(id);
        return Result.ok(role);
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.add')")
    @ApiOperation(value = "新增角色")
    @PostMapping("save")
    public Result save(@RequestBody @Validated SysRole role) {
        sysRoleService.save(role);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.update')")
    @ApiOperation(value = "修改角色")
    @PutMapping("update")
    public Result updateById(@RequestBody SysRole role) {
        sysRoleService.updateById(role);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")
    @ApiOperation(value = "删除角色")
    @DeleteMapping("remove/{id}")
    public Result remove(@PathVariable Long id) {
        sysRoleService.removeById(id);
        return Result.ok();
    }

    @PreAuthorize("hasAuthority('bnt.sysRole.remove')")
    @ApiOperation(value = "根据id列表删除")
    @DeleteMapping("batchRemove")
    public Result batchRemove(@RequestBody List<Long> idList) {
        sysRoleService.removeByIds(idList);
        return Result.ok();
    }
    ...
}
3.8、测试服务器端权限

登录后台,分配权限进行测试,页面如果添加了按钮权限控制,可临时去除方便测试

测试结论:

​ 1、分配了权限的能够成功返回接口数据

​ 2、没有分配权限的会抛出异常:org.springframework.security.access.AccessDeniedException: 不允许访问

3.9、异常处理

异常处理有2种方式:

​ 1、扩展Spring Security异常处理类:AccessDeniedHandler、AuthenticationEntryPoint

​ 2、在spring boot全局异常统一处理

第一种方案说明:如果系统实现了全局异常处理,那么全局异常首先会获取AccessDeniedException异常,要想Spring Security扩展异常生效,必须在全局异常再次抛出该异常。

我们使用第二种方案。

全局异常添加处理

操作模块:service-util

/**
 * spring security异常
 * @param e
 * @return
 */
@ExceptionHandler(AccessDeniedException.class)
@ResponseBody
public Result error(AccessDeniedException e) throws AccessDeniedException {
    return Result.build(null, ResultCodeEnum.PERMISSION);
}

AccessDeniedException需要引入依赖,Spring Security对应的异常

在service-util模块引入依赖

`<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <scope>provided</scope>
</dependency>`

二、Activiti入门

在这里插入图片描述

一、了解工作流

1、什么是工作流

工作流(Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是“使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现”。**通俗来讲,就是业务上一个完整的审批流程。**例如员工的请假,出差,外出采购,合同审核等等,这些过程,都是一个工作流。

2、工作流引擎

对于工作流的处理,如果采用原始的方式,我们需要拿着各种文件到各个负责人那里去签字,需要在多个部门之间不断审批,这种方式费时费力。而我们可以借助软件系统来协助我们处理这些审批流程,这样就出现了工作流系统,使用工作流系统后可以极大的提高工作效率。

在学习工作流的过程中,我们肯定看到过这个模型:填写请假单->部门经理审批->总经理审批->人事备案。

(1)要实现上述的流程,我们自己可以通过字段标识来实现这个审批效果,在业务表中加个字段,比如填写请假单用1标识,部门经理用2标识,总经理用3标识,人事备案用4标识,好像看起来没啥问题,也实现了审批效果。可是一旦我们的流程出现了变化,这个时候我们就需要改动我们的代码了,这显然是不可取的,那么有没有专业的方式来实现工作流的管理呢?并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。在这样的背景下,就出现了工作流引擎

为什么使用工作流引擎,能实现业务流程改变,不用修改代码,流程还能自动推进?

(1)我们先来说说为什么流程改变,不用修改代码:我们的工作流引擎都实现了一个规范,这个规范要求我们的流程管理与状态字段无关,始终都是读取业务流程图的下一个节点。当业务更新的时候我们只需要更新业务流程图就行了。这就实现了业务流程改变,不用修改代码。

(2)再来说说流程自动推进,这个原理就更简单了,就拿上面的请假模型来说,工作流引擎会用一张表来记录当前处在的节点。当填写完请假单后肯定是要轮到部门经理来审批了,所以我们一旦完成了请假单填写那么这条记录将会被从这张表删除掉,并且会把下一个节点部门经理的信息插入到这张表中,当我们用部门经理的信息去这张表中查询的时候就能查出部门经理相关的审批的信息了,以此类推,这样层层递进,就实现了流程的自动递交了。

3、常见工作流引擎

主流的框架有:Activiti、jBPM、Camunda 、Flowable 、还有我们国产的盘古BPM、云程

4、Activiti7概述

4.1、Activiti介绍

activiti是一个工作流引擎,可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言BPMN进行定义,业务流程按照预先定义的流程进行执行。实现了系统的流程由activiti进行管理,减少业务系统由于流程变更进行系统升级改造的工作流量,从而提高系统的健壮性,同时也减少了系统开发维护成本。

官方网站:https://www.activiti.org

4.2、建模语言BPMN

BPM(Business Process Management)即业务流程管理,是一种规范化的构造端到端的业务流程,以持续提高组织业务效率

BPM 软件就是根据企业中业务环境的变化,推进人与人之间、人与系统之间以及系统与系统之间的整理及调整的经营方法与解决方案的 IT 工具。使用 BPM 软件对企业内部及外部的业务流程的整个生命周期进行建模、自动化、管理监控和优化,可以降低企业成本,提高利润

BPMN(Business Process Model AndNotation)即业务流程模型和符号,是一套标准的业务流程建模符号,使用 BPMN 提供的符号可以创建业务流程。Activit 就是使用 BPMN 进行流程建模、流程执行管理的

BPMN2.0 是业务流程建模符号 2.0 的缩写,它由 Business Process Management Initiative 这个非营利协会创建并不断发展。BPMN2.0 是使用一些符号来明确业务流程设计流程图的一套符号规范,能增进业务建模时的沟通效率。目前 BPMN2.0 是最新的版本,它用于在 BPM 上下文中进行布局和可视化的沟通

BPMN2.0 的基本符号主要包含:

  • 事件 Event

    开始:表示一个流程的开始

    中间:发生的开始和结束事件之间,影响处理的流程

    结束:表示该过程结束

    在这里插入图片描述

  • 活动 Activities

    活动是工作或任务的一个通用术语。一个活动可以是一个任务,还可以是一个当前流程的子处理流程;其次,你还可以为活动指定不同的类型。常见活动如下:

    在这里插入图片描述

  • 网关 GateWay

    用于表示流程的分支与合并,有几种常用网关需要了解:

在这里插入图片描述

  • 排他网关:只有一条路径会被选择

  • xxxxxxxxxx <el-button type=“success” icon=“el-icon-plus” size=“mini” @click=“add” :disabled=“$hasBP(‘bnt.sysRole.add’) === false”>添 加vue

  • 包容网关:可以同时执行多条线路,也可以在网关上设置条件

  • 事件网关:专门为中间捕获事件设置的,允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程处于等待状态,需要等待抛出事件才能将等待状态转换为活动状态

在这里插入图片描述

  • 流向 Flow

    流是连接两个流程节点的连线,常见的流向包含以下几种:

    顺序流:用一个带实心箭头的实心线表示,用于指定活动执行的顺序

    信息流:用一条带箭头的虚线表示,用于描述两个独立的业务参与者(业务实体/业务角色)之间发送和接受的消息流动

    关联:用一根带有线箭头的点线表示,用于将相关的数据、文本和其他人工信息与流对象联系起来。用于展示活动的输入和输出

    在这里插入图片描述

流程示例:

在这里插入图片描述

4.3、Activiti使用流程

第一步: 引入依赖并初始化数据库

既然activiti是一个框架,那么我们肯定是需要引入对应的jar包坐标的,具体参考代码中的。

第二步: 通过工具绘画流程图

使用 activiti 流程建模工具(activity-designer)定义业务流程(.bpmn 文件)

.bpmn 文件就是业务流程定义文件,通过 xml 定义业务流程。

第三步:流程定义部署;

向 activiti 部署业务流程定义(.bpmn 文件),使用 activiti 提供的 api 向 activiti 中部署.bpmn 文件,

通俗来讲,就是让activiti认识要使用的流程

第四步: 启动一个流程实例(ProcessInstance)

启动一个流程实例表示开始一次业务流程的运行,比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响,就好比定义一个java类,实例化两个对象一样,部署的流程就好比java类,启动一个流程实例就好比new一个java对象

第五步: 用户查询待办任务(Task)

因为现在系统的业务流程已经交给 activiti 管理,通过 activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 activiti帮我们管理了。实际上我们学习activiti也只是学习它的API怎么使用,因为很多功能activiti都已经封装好了,我们会调用就行了

第六步: 用户办理任务

用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其它用户办理,比如请假单创建后由部门经理审核,这个过程也是由 activiti 帮我们完成了,不需要我们在代码中硬编码指定下一个任务办理人了

第七步: 流程结束

当任务办理完成没有下一个任务节点了,这个流程实例就完成了。

二、Activiti7

1、Activiti使用

1.1、数据库支持

Activiti 运行必须要有数据库的支持,支持的数据库有:mysql、oracle、postgres、mssql、db2、h2

1.2、Activiti环境

我们直接在当前项目:guigu-oa-parent做Activiti入门讲解

1.2.1、引入依赖
<!--引入activiti的springboot启动器 -->
<dependency>
    <groupId>org.activiti</groupId>
    <artifactId>activiti-spring-boot-starter</artifactId>
    <version>7.1.0.M6</version>
    <exclusions>
        <exclusion>
            <artifactId>mybatis</artifactId>
            <groupId>org.mybatis</groupId>
        </exclusion>
    </exclusions>
</dependency>

说明:Activiti7与SpringBoot整合后,默认集成了SpringSecurity安全框架,当前我们项目已经集成过了SpringSecurity,后续案例设置审批人时都必须是系统用户,Activiti框架会检查用户是否存在,否则会出现异常,后续大家可以在案例中测试。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.2.2、添加配置

数据源项目已经添加,只需要如下配置即可

spring:    
	activiti:
      #    false:默认,数据库表不变,但是如果版本不对或者缺失表会抛出异常(生产使用)
      #    true:表不存在,自动创建(开发使用)
      #    create_drop: 启动时创建,关闭时删除表(测试使用)
      #    drop_create: 启动时删除表,在创建表 (不需要手动关闭引擎)
      database-schema-update: true
      #监测历史表是否存在,activities7默认不开启历史表
      db-history-used: true
      #none:不保存任何历史数据,流程中这是最高效的
      #activity:只保存流程实例和流程行为
      #audit:除了activity,还保存全部的流程任务以及其属性,audit为history默认值
      #full:除了audit、还保存其他全部流程相关的细节数据,包括一些流程参数
      history-level: full
      #校验流程文件,默认校验resources下的process 文件夹的流程文件
      check-process-definitions: true
1.2.3、启动项目

启动项目,即可生成项目数据库表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.2.4、数据库表介绍

Activiti 的运行支持必须要有这 25 张表的支持,主要是在业务流程运行过程中,记录参与流程的用户主体,用户组信息,以及流程的定义,流程执行时的信息,和流程的历史信息等等

1、 表的命名规则和作用

观察创建的表,我们发现 Activiti 的表都以 act_ 开头,紧接着是表示表的用途的两个字母标识,也和 Activiti 所提供的服务的 API 对应:

  • ACT_RE:RE 表示 repository,这个前缀的表包含了流程定义和流程静态资源 (图片、规则、等等)
  • ACT_RU:RU 表示 runtime,这些表运行时,会包含流程实例、任务、变量、异步任务等流程业务进行中的数据。Activiti 只在流程实例执行过程中保存这些数据,在流程结束时就会删除这些记录。这样表就可以一直保持很小的体积,并且速度很快
  • ACT_HI:HI 表示 history,这些表包含一些历史数据,比如历史流程实例、变量、任务等等
  • ACT_GE:GE 表示 general,通用数据

2、Activiti 数据表介绍

表分类表名解释
一般数据
[ACT_GE_BYTEARRAY]通用的流程定义和流程资源
[ACT_GE_PROPERTY]系统相关属性
流程历史记录
[ACT_HI_ACTINST]历史的流程实例
[ACT_HI_ATTACHMENT]历史的流程附件
[ACT_HI_COMMENT]历史的说明性信息
[ACT_HI_DETAIL]历史的流程运行中的细节信息
[ACT_HI_IDENTITYLINK]历史的流程运行过程中用户关系
[ACT_HI_PROCINST]历史的流程实例
[ACT_HI_TASKINST]历史的任务实例
[ACT_HI_VARINST]历史的流程运行中的变量信息
流程定义表
[ACT_RE_DEPLOYMENT]部署单元信息
[ACT_RE_MODEL]模型信息
[ACT_RE_PROCDEF]已部署的流程定义
运行实例表
[ACT_RU_EVENT_SUBSCR]运行时事件
[ACT_RU_EXECUTION]运行时流程执行实例
[ACT_RU_IDENTITYLINK]运行时用户关系信息,存储任务节点与参与者的相关信息
[ACT_RU_JOB]运行时作业
[ACT_RU_TASK]运行时任务
[ACT_RU_VARIABLE]运行时变量表
1.3、Activiti常用Service服务接口

简单介绍一下各个 Service 的实现类:

  • RepositoryService

    Activiti 的资源管理类,该服务负责部署流程定义,管理流程资源。在使用 Activiti 时,一开始需要先完成流程部署,即将使用建模工具设计的业务流程图通过 RepositoryService 进行部署

  • RuntimeService

    Activiti 的流程运行管理类,用于开始一个新的流程实例,获取关于流程执行的相关信息。流程定义用于确定一个流程中的结构和各个节点间行为,而流程实例则是对应的流程定义的一个执行,可以理解为 Java 中类和对象的关系

  • TaskService

    Activiti 的任务管理类,用于处理业务运行中的各种任务,例如查询分给用户或组的任务、创建新的任务、分配任务、确定和完成一个任务

  • HistoryService

    Activiti 的历史管理类,可以查询历史信息。执行流程时,引擎会保存很多数据,比如流程实例启动时间、任务的参与者、完成任务的时间、每个流程实例的执行路径等等。这个服务主要通过查询功能来获得这些数据

  • ManagementService

    Activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护

1.4、流程设计工具

IDEA版本小于等于2019,可使用Activiti插件actiBPM,大于该版本的IDEA可使用Activiti BPMN visualizer插件绘制流程设计。

今天我们主角是:Activiti Modeler

Activiti Modeler 是 Activiti 官方提供的一款在线流程设计的前端插件,开发人员可以方便在线进行流程设计,保存流程模型,部署至流程定义等等,后续我们的项目也是集成Activiti Modeler绘制流程定义。

1.4.1、下载activiti-explorer

官网下载:https://www.activiti.org/get-started

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.4.2、解压获取部署包

解压activiti-5.22.0.zip,在activiti-5.22.0\wars目录下获取activiti-explorer.war

1.4.3、部署activiti-explorer.war

将activiti-explorer.war放到tomcat部署目录,启动tomcat

1.4.4、访问activiti-explorer

http://localhost:8080/activiti-explorer

默认登录账号:kermit kermit

在这里插入图片描述

上面有很多功能,我们关注流程设计即可,如下图:

点击上图:流程 --> 新建模型 --> 输入模型名称(请假)–> 创建

在这里插入图片描述

2、Activiti流程操作

2.1、流程定义

我们定义一个请假流程

2.1.1、新建模型

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.1.2、开始节点

在这里插入图片描述

2.1.3、任务节点

在这里插入图片描述

2.1.4、结束节点

在这里插入图片描述

2.1.5、设置节点属性

指定标签名称:张三审批,节点任务负责人:zhangsan

在这里插入图片描述

指定标签名称:李四审批,节点任务负责人:lisi

在这里插入图片描述

2.1.5、设置流程定义key

在这里插入图片描述

2.1.6、保存流程定义模型

在这里插入图片描述

2.1.7、下载流程定义文件

在这里插入图片描述

下载文件为:qingjia.bpmn20.xml

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/processdef">
  <process id="qingjia" isExecutable="true">
    <startEvent id="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B"></startEvent>
    <userTask id="sid-38632C81-C407-4F0D-944D-FC30F90637A3" name="张三审批" activiti:assignee="zhangsan"></userTask>
    <sequenceFlow id="sid-081A176E-6756-4C4C-B36C-2649B12CFC5D" sourceRef="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B" targetRef="sid-38632C81-C407-4F0D-944D-FC30F90637A3"></sequenceFlow>
    <userTask id="sid-655780D5-8492-494F-9E30-2CFD6691E98D" name="李四审批" activiti:assignee="lisi"></userTask>
    <sequenceFlow id="sid-7DCE821D-4AE0-4F27-9811-80B575E7A758" sourceRef="sid-38632C81-C407-4F0D-944D-FC30F90637A3" targetRef="sid-655780D5-8492-494F-9E30-2CFD6691E98D"></sequenceFlow>
    <endEvent id="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D"></endEvent>
    <sequenceFlow id="sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1" sourceRef="sid-655780D5-8492-494F-9E30-2CFD6691E98D" targetRef="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_qingjia">
    <bpmndi:BPMNPlane bpmnElement="qingjia" id="BPMNPlane_qingjia">
      <bpmndi:BPMNShape bpmnElement="sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B" id="BPMNShape_sid-14A3E2A6-84E4-49E0-BF92-3DABD741430B">
        <omgdc:Bounds height="30.0" width="30.0" x="93.5" y="75.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-38632C81-C407-4F0D-944D-FC30F90637A3" id="BPMNShape_sid-38632C81-C407-4F0D-944D-FC30F90637A3">
        <omgdc:Bounds height="80.0" width="100.0" x="168.5" y="50.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-655780D5-8492-494F-9E30-2CFD6691E98D" id="BPMNShape_sid-655780D5-8492-494F-9E30-2CFD6691E98D">
        <omgdc:Bounds height="80.0" width="100.0" x="313.5" y="50.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D" id="BPMNShape_sid-7EE28419-BC61-49AC-8990-C63C4D2F7C0D">
        <omgdc:Bounds height="28.0" width="28.0" x="458.5" y="76.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="sid-7DCE821D-4AE0-4F27-9811-80B575E7A758" id="BPMNEdge_sid-7DCE821D-4AE0-4F27-9811-80B575E7A758">
        <omgdi:waypoint x="268.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="313.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-081A176E-6756-4C4C-B36C-2649B12CFC5D" id="BPMNEdge_sid-081A176E-6756-4C4C-B36C-2649B12CFC5D">
        <omgdi:waypoint x="123.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="168.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1" id="BPMNEdge_sid-2E583A5C-265A-4C05-B5E1-7F5DB98291F1">
        <omgdi:waypoint x="413.5" y="90.0"></omgdi:waypoint>
        <omgdi:waypoint x="458.5" y="90.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>
2.1.8、下载流程定义图片

单击右键上图图片,图片另存为:qingjia.png
在这里插入图片描述

2.1.9、将资源文件放入项目

在service-oa模块resources下新建process资源文件夹

将qingjia.bpmn20.xml与qingjia.png放入process目录

2.2、流程定义部署

将上面在设计器中定义的流程部署到activiti数据库中,就是流程定义部署。通过调用activiti的api将流程定义的bpmn和png两个文件一个一个添加部署到activiti中,也可以将两个文件打成zip包进行部署。

2.2.1、单个文件部署方式
package com.atguigu;

import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
public class ProcessTest {

    @Autowired
    private RepositoryService repositoryService;

    @Test
    public void deployProcess() {
        // 流程部署
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("process/qingjia.bpmn20.xml")
                .addClasspathResource("process/qingjia.png")
                .name("请假申请流程")
                .deploy();
        System.out.println(deploy.getId());
        System.out.println(deploy.getName());
    }
}
2.2.2、压缩包部署方式
@Test
public void deployProcessByZip() {
    // 定义zip输入流
    InputStream inputStream = this
            .getClass()
            .getClassLoader()
            .getResourceAsStream(
                    "process/qingjia.zip");
    ZipInputStream zipInputStream = new ZipInputStream(inputStream);

    // 流程部署
    Deployment deployment = repositoryService.createDeployment()
            .addZipInputStream(zipInputStream)
            .name("请假申请流程")
            .deploy();
    System.out.println("流程部署id:" + deployment.getId());
    System.out.println("流程部署名称:" + deployment.getName());
}
2.2.3、操作数据库表

流程定义部署后操作activiti的3张表如下:

act_re_deployment 流程定义部署表,每部署一次增加一条记录

act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录

act_ge_bytearray 流程资源表

2.3、启动流程实例

流程定义:将bpmn文件放到activiti的三张表中,好比是java中的一个类
流程实例:好比是java中的一个实例对象(一个流程定义可以对应多个流程实例),张三可以启动一个请假流程实例,李四也可以启动一个请假流程实例,他们互不影响

@Autowired
private RuntimeService runtimeService;

@Test
public void startUpProcess() {
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia");
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
    System.out.println("当前活动Id:" + processInstance.getActivityId());
}

操作数据表

act_hi_actinst 流程实例执行历史

act_hi_identitylink 流程的参与用户历史信息

act_hi_procinst 流程实例历史信息

act_hi_taskinst 流程任务历史信息

act_ru_execution 流程执行信息

act_ru_identitylink 流程的参与用户信息

act_ru_task 任务信息

2.4、查询任务

每个节点都配置了Assignee,流程启动后,任务的负责人就可以查询自己当前需要处理的任务,查询出来的任务都是该用户的待办任务。

@Autowired
private TaskService taskService;

/**
 * 查询当前个人待执行的任务
 */
@Test
public void findPendingTaskList() {
    //任务负责人
    String assignee = "zhangsan";
    List<Task> list = taskService.createTaskQuery()
            .taskAssignee(assignee)//只查询该任务负责人的任务
            .list();
    for (Task task : list) {
        System.out.println("流程实例id:" + task.getProcessInstanceId());
        System.out.println("任务id:" + task.getId());
        System.out.println("任务负责人:" + task.getAssignee());
        System.out.println("任务名称:" + task.getName());
    }
}

说明:

​ 流程实例id:一个流程只有一个,标识这个流程

​ 任务id:流程每进行到某个节点,就会给这个节点分配一个任务id

输出结果如下:

​ 流程实例id:d969f534-825e-11ed-95b4-7c57581a7819
​ 任务id:d96c3f28-825e-11ed-95b4-7c57581a7819
​ 任务负责人:zhangsan
​ 任务名称:张三审批

2.5、处理当前任务

任务负责人查询待办任务,选择任务进行处理,完成任务。

/**
 * 完成任务
 */
@Test
public void completTask(){
    Task task = taskService.createTaskQuery()
            .taskAssignee("zhangsan")  //要查询的负责人
            .singleResult();//返回一条

    //完成任务,参数:任务id
    taskService.complete(task.getId());
}

完成任务后,任务自动到下一个节点

2.6、查询已处理任务
@Autowired
private HistoryService historyService;

/**
 * 查询已处理历史任务
 */
@Test
public void findProcessedTaskList() {
    //张三已处理过的历史任务
    List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().taskAssignee("zhangsan").finished().list();
    for (HistoricTaskInstance historicTaskInstance : list) {
        System.out.println("流程实例id:" + historicTaskInstance.getProcessInstanceId());
        System.out.println("任务id:" + historicTaskInstance.getId());
        System.out.println("任务负责人:" + historicTaskInstance.getAssignee());
        System.out.println("任务名称:" + historicTaskInstance.getName());
    }
}
2.7、其他接口(了解)
/**
 * 查询流程定义
 */
@Test
public void findProcessDefinitionList(){
    List<ProcessDefinition> definitionList = repositoryService.createProcessDefinitionQuery()
            .orderByProcessDefinitionVersion()
            .desc()
            .list();
    //输出流程定义信息
    for (ProcessDefinition processDefinition : definitionList) {
        System.out.println("流程定义 id="+processDefinition.getId());
        System.out.println("流程定义 name="+processDefinition.getName());
        System.out.println("流程定义 key="+processDefinition.getKey());
        System.out.println("流程定义 Version="+processDefinition.getVersion());
        System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
    }
}

/**
 * 删除流程定义
 */
public void deleteDeployment() {
    //部署id
    String deploymentId = "82e3bc6b-81da-11ed-8e03-7c57581a7819";
    //删除流程定义,如果该流程定义已有流程实例启动则删除时出错
    repositoryService.deleteDeployment(deploymentId);
    //设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式
    //repositoryService.deleteDeployment(deploymentId, true);
}

3、流程实例

3.1、什么是流程实例

流程定义ProcessDefinition和流程实例ProcessInstance是Activiti重要的概念,类似于Java类和Java实例的关系

启动一个流程实例表示开始一次业务流程的运行,比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响,就好比定义一个 java 类,实例化两个对象一样,部署的流程就好比 java 类,启动一个流程实例就好比 new 一个 java 对象

3.2、让实际业务与activiti表关联(BusinessKey)

比如我们填写一个请假单,一定会有一个请假单的唯一标识,我们通常使用这个标识来关联activiti,这个标识在activiti中称为businesskey

BusinessKey:业务标识,通常为业务的主键,业务标识和流程标识一一对应,业务标识来源于业务系统,存储业务标识就是根据业务标识来关联查询业务系统的数据

举例:请假流程启动一个流程实例,就可以将请假单的id作为业务标识存储到activiti中,将来查询activiti的流程实例信息就可以获取请假单的id从而关联查询业务系统数据库得到请假单信息

/**
 * 启动流程实例,添加businessKey
 */
@Test
public void startUpProcessAddBusinessKey(){
    String businessKey = "1";
    // 启动流程实例,指定业务标识businessKey,也就是请假申请单id
    ProcessInstance processInstance = runtimeService.
            startProcessInstanceByKey("qingjia",businessKey);
    // 输出
    System.out.println("业务id:"+processInstance.getBusinessKey());
}
3.3、挂起,激活流程实例

某些情况可能由于流程变更需要将当前运行的流程暂停而不是直接删除,流程暂停后将不会执行;

3.3.1、全部流程实例挂起

操作流程定义为挂起状态,该流程定义下面的所有流程实例全部暂停:
流程定义为挂起状态,该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行

@Test
public void suspendProcessInstance() {
    ProcessDefinition qingjia = repositoryService.createProcessDefinitionQuery().processDefinitionKey("qingjia").singleResult();
    // 获取到当前流程定义是否为暂停状态 suspended方法为true是暂停的,suspended方法为false是运行的
    boolean suspended = qingjia.isSuspended();
    if (suspended) {
        // 暂定,那就可以激活
        // 参数1:流程定义的id  参数2:是否激活    参数3:时间点
        repositoryService.activateProcessDefinitionById(qingjia.getId(), true, null);
        System.out.println("流程定义:" + qingjia.getId() + "激活");
    } else {
        repositoryService.suspendProcessDefinitionById(qingjia.getId(), true, null);
        System.out.println("流程定义:" + qingjia.getId() + "挂起");
    }
}
3.3.2、单个流程实例挂起

操作流程实例对象,针对单个流程执行挂起操作,某个流程实例挂起则此流程不在继续执行,完成该流程实例的当前任务将报异常;

@Test
public void SingleSuspendProcessInstance() {
    String processInstanceId = "8bdff984-ab53-11ed-9b17-f8e43b734677";
    ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
    //获取到当前流程定义是否为暂停状态   suspended方法为true代表为暂停   false就是运行的
    boolean suspended = processInstance.isSuspended();
    if (suspended) {
        runtimeService.activateProcessInstanceById(processInstanceId);
        System.out.println("流程实例:" + processInstanceId + "激活");
    } else {
        runtimeService.suspendProcessInstanceById(processInstanceId);
        System.out.println("流程实例:" + processInstanceId + "挂起");
    }
}

4、任务分配

任务分配有三种方式

  1. 固定分配
  2. UEL表达式分配
  3. 监听器分配
4.1、固定分配

在前面进行业务流程建模时指定固定的任务负责人,如:Assignee:zhangsan/lisi

4.2、表达式分配

activiti使用UEL表达式,UEL是java EE6规范的一部分,UEL即统一表达式语言,activiti支持两个UEL表达式:UEL-value和UEL-method。

4.2.1、UEL-value

新建:一个加班流程

如图:
在这里插入图片描述
assignee1 这个变量是 activiti 的一个流程变量

我们启动流程实例,这个启动实例的方法跟之前的方法基本一致,唯一的不同是在启动时,添加了一个参数

@Test
public void deployProcess01() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
        .addClasspathResource("process/jiaban01.bpmn20.xml")
        .name("加班申请流程")
        .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());
}

/**
 * 启动流程实例
 */
@Test
public void startUpProcess01() {
    Map<String, Object> variables = new HashMap<>();
    variables.put("assignee1","zhangsan");
    variables.put("assignee2","lisi");
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia01", variables);
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
}
4.2.2、UEL-method 方式

如图:
在这里插入图片描述

userBean 是 spring 容器中的一个 bean,表示调用该 bean 的 getUsername(int id)方法。

经理审批:${userBean.getUsername(1)}

人事审批:${userBean.getUsername(2)}

package com.atguigu.process.bean;

import org.springframework.stereotype.Component;

@Component
public class UserBean {

    public String getUsername(int id) {
        if(id == 1) {
            return "zhangsan";
        }
        if(id == 2) {
            return "lisi";
        }
        return "admin";
    }
}

部署与启动

@Test
public void deployProcess02() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
        .addClasspathResource("process/jiaban02.bpmn20.xml")
        .name("加班申请流程")
        .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());
}

/**
 * 启动流程实例
 */
@Test
public void startUpProcess02() {
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("qingjia02");
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
}

启动流程实例,就会调用bean方法,参数为:1,经理审批后,接着调用bean方法,参数为:2

4.3、监听器分配

使用监听器的方式来指定负责人,那么在流程设计时就不需要指定assignee。

任务监听器是发生对应的任务相关事件时执行自定义 java 逻辑 或表达式

Event的选项包含:

Create:任务创建后触发
Assignment:任务分配后触发
Delete:任务完成后触发
All:所有事件发生都触发

定义任务监听类,且类必须实现 org.activiti.engine.delegate.TaskListener 接口

package com.atguigu.process.bean;

import org.activiti.engine.delegate.DelegateTask;
import org.activiti.engine.delegate.TaskListener;

public class MyTaskListener implements TaskListener {

    @Override
    public void notify(DelegateTask delegateTask) {
        if(delegateTask.getName().equals("经理审批")){
            //这里指定任务负责人
            delegateTask.setAssignee("zhangsan");
        } else if(delegateTask.getName().equals("人事审批")){
            //这里指定任务负责人
            delegateTask.setAssignee("lisi");
        }
    }
}

配置监听器

在这里插入图片描述

在这里插入图片描述

经理审批与人事审批,都设置一样的监听即可

部署与测试

@Test
public void deployProcess03() {
    // 流程部署
    Deployment deploy = repositoryService.createDeployment()
            .addClasspathResource("process/jiaban03.bpmn20.xml")
            .name("加班申请流程")
            .deploy();
    System.out.println(deploy.getId());
    System.out.println(deploy.getName());
}

/**
 * 启动流程实例
 */
@Test
public void startUpProcess03() {
    //创建流程实例,我们需要知道流程定义的key
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("jiaban03");
    //输出实例的相关信息
    System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
    System.out.println("流程实例id:" + processInstance.getId());
}

启动流程实例,就会调用MyTaskListener监听方法

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值