RBAC实战

news2024/12/22 22:50:27

一、权限控制概述

1.1、访问控制目的

在实际的组织中,为了完成组织的业务工作,需要在组织内部设置不同的职位,职位既表示一种业务分工,又表示一种责任与权利。根据业务分工的需要,职位被划分给不同群体,各个群体的人根据其工作任务的需要被赋予不同的职责和权利,每个人有权了解与使用与自己任务相关的信息与资源,对于那些不应该被知道的信息则应该限制他们访问。这就产生了访问控制的需求。

限制主体对资源的访问,限制用户可以访问而且只能访问自己被授权的资源,从而保障数据资源在合法范围内得以有效使用和管理。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。

1.2、访问控制策略

在权限管理中使用最多的还是基于角色访问控制(RBAC:Role Based Access Control)。基于角色的访问控制,是 20 世纪 90 年代研究出来的一种模型。这种模型的基本概念是把许可权(Permission)与角色(Role)联系在一起,用户通过充当合适角色的成员而获得该角色的许可权。

例如,在一间公司中,有老板、经理、行政、人事、财务等不同的职位,在通常情况下,职位所赋予的权利是不变的,但在某个职位上工作的人可以根据需要调整。RBAC 模型对组织内部的这些关系与访问控制要求给出了非常恰当的描述。

1.3、RBAC 重要对象

  • 用户(Employee):角色施加的主体;用户通过拥有某个或多个角色以得到对应的权限。
  • 角色(Role):表示一组权限的集合。
  • 权限(Permission):一个资源代表一个权限,是否能访问该资源,就是看是否有该权限。

在这里插入图片描述

1.4、RBAC 流程与模型分析

管理员可为用户分配角色和权限(用户、角色和权限数据还有其之间关系数据都得保存起来)。

动手画流程图分析以及模型图。

1.5、项目演示

观察演示项目文档, 搞清楚项目要做哪些功能有哪些模块

在这里插入图片描述

二、搭建项目

2.1、技术选型

在项目开发前,需要对技术方案先做方向性的评估。在投资有限、硬件资源有限的条件下,为了满足需求,需要进行技术方案选型、技术点使用范围进行分析。也就是需要选择哪种类型脚手架(包含哪些技术的脚手架)

常见的技术选型度量点:

(1)快速开发

(2)学习成本低

(3)技术成熟度

(4)稳定性

(5)性能

当前项目技术架构:

后端框架:Springboot 、MyBatis、Spring

前端框架:Vue 、elementUi 、es6 、axios

数据库: MySQL、Redis

项目结构: 前后端分离

2.2、导入依赖

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Mybatis 集成到 SpringBoot 中的依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!-- 引入 redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- lettuce 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>
       <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- MyBatis 的 generator 插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>false</overwrite>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>5.1.45</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

2.3、加入配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/rbac?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: admin
    # redis 配置
  redis:
    # 地址
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
server:
  port: 8080
mybatis:
  type-aliases-package: cn.xxx.rbac.domain
logging:
  level:
    root: info
    cn:
      xxx:
        rbac:
          mapper: trace

2.4、创建启动类

@SpringBootApplication
@MapperScan(basePackages = "cn.xxx.mapper")
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
}

三、部门管理

部门表:department

在这里插入图片描述

3.1、 部门列表

前端界面

在这里插入图片描述

后端接口:

功能描述部门分页查询
URLhttp://localhost:8080/api/department/list?pageNum=1&pageSize=5
请求方式get
参数名必填类型描述
pageNumint当前页
pageSizeint每页显示多少条
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 } ], “total”: 6 总条数 }
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”: { “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 } ], “total”: 6 总条数 }}

代码:

根据后台接口文档创建出来返回对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class R {
    private int code;// 状态码:200成功,500失败
    private String status;// 状态
    private String msg;// 响应提示的信息
    private Object data;// 响应的具体内容
    public static final String SUCCESS = "success";
    public static final String FAIL = "fail";

    public static R ok() {
        return new R(200, SUCCESS, "", null);
    }

    public static R ok(String msg, Object data) {
        return new R(200, SUCCESS, msg, data);
    }

    public static R ok(Object data) {
        return new R(200, SUCCESS, "", data);
    }

    public static R fail() {
        return new R(500, FAIL, "", null);
    }

    public static R fail(String msg) {
        return new R(500, FAIL, msg,null);
    }
}

把数据封装到 R 对象中在 controller

@GetMapping("/list")
public R query(QueryObject queryObject){

    PageResult result = departmentService.query(queryObject);
   
    Result r = new Result(result.getTotal(),result.getList(),queryObject.getPageNum(),queryObject.getPageSize());
    return R.ok(r);

}

测试: 使用 postman 访问http://localhost:8080/api/department/list?pageNum=1&pageSize=5看数据返回值是否正确

3.2 、PageHelper

PageHelper 是一个 MyBatis 的分页插件,负责将已经写好的 SQL 语句,进行分页加工。无需你自己去封装以及关心 SQL 分页等问题,使用很方便。

要掌握如何在一个新项目中加入该分页插件,以及如何实现分页功能。

3.2.1、添加依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

3.2.2、配置分页插件

修改 application.properties,如下配置:

pagehelper:
  reasonable: true
  page-size-zero: true

3.2.3、实现分页

  • 删除 Mapper.xml 文件中查询数量的代码。

  • 删除 Mapper.xml 文件的分页查询的 SQL 中的 Limit 子句,QueryObject 中也不需要提供 getStart 方法了。

  • 使用分页插件提供的 PageInfo 类进行封装,不需要我们自己再定义 PageResult 类了。

    public PageInfo<Department> query(QueryObject qo) {
        // 使用分页插件,传入当前页,每页显示数量
        PageHelper.startPage(qo.getCurrentPage(), qo.getPageSize());
        List<Department> departments = departmentMapper.selectForList(qo);
        return new PageInfo(departments);
    }
    

3.2.4、DepartmentController 修改

@GetMapping("/list")
    public R query(QueryObject queryObject){

        PageInfo pageinfo = departmentService.query(queryObject);
        Result r = new Result(pageinfo.getTotal(),pageinfo.getList(),queryObject.getPageNum(),queryObject.getPageSize());
        return R.ok(r);

    }

测试: 使用 postman 测试看是否正确http://localhost:8080/api/department/list?pageNum=1&pageSize=5

3.2.5 PageHelper 原理解析

画图分析

3.3 、部门删除

前台界面:

在这里插入图片描述

后台接口:

功能描述删除部门信息
URLhttp://localhost:8080/api/department/delete/11
请求方式delete
参数名必填类型描述
idint部门标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

代码:

@DeleteMapping("/delete/{id}")
public R delete(@PathVariable Long id){
    departmentService.deleteByPrimaryKey(id);
    return R.ok();
}

测试:使用 postman 访问http://localhost:8080/api/department/delete/11>

3.4、部门新增和编辑

前台界面:

在这里插入图片描述

后台接口:

功能描述保存或更新部门信息
URLhttp://localhost:8080/api/department/saveOrUpdate
请求方式post
参数名必填类型描述
departmentjson添加 { “name”: “总裁办”,部门名称 “sn”: “President” 部门简称 }更新 { “id”: “12”,部门标识 “name”: “总裁办1”,部门名称 “sn”: “President1” 部门简称 }
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “保存或者更新成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

代码:

@PostMapping("/saveOrUpdate")
    public R saveOrUpdate(@RequestBody Department department){
        if(department.getId() == null){
            departmentService.insert(department);
        }else{
            departmentService.updateByPrimaryKey(department);
        }
        return R.ok();
    }

测试: 使用 postman测试 http://localhost:8080/api/department/saveOrUpdate

3.5、查询单个部门

功能描述获取部门信息
URLhttp://localhost:8080/api/department/info/1
请求方式get
参数名必填类型描述
idint部门标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 }
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: { “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 }}

代码:

  @GetMapping("/info/{id}")
    public R get(@PathVariable Long id){
        Department department = departmentService.selectByPrimaryKey(id);
        return R.ok(department);
    }

测试:使用 postman 测试 http://localhost:8080/api/department/info/1

3.6、查询所有部门的

功能描述查询所有部门
URLhttp://localhost:8080/api/department/listAll
请求方式get
参数名必填类型描述
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataarray[ { “id”: 1, “name”: “人力资源部”, “sn”: “Human Resources Department” }]
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”:[ { “id”: 1, “name”: “人力资源部”, “sn”: “Human Resources Department” }]}

代码:

@GetMapping("/listAll")
public R listAll(QueryObject queryObject){

    List<Department> list = departmentService.selectAll();
    return R.ok(list);
}

测试: 使用 postman测试 http://localhost:8080/api/department/listAll

3.7、接口统一异常处理

当我们 service 中判断抛出异常的时候发现返回信息和预期是不一致的

比如:

 public int deleteByPrimaryKey(Long id) {
        int i = 1/0;
        return departmentMapper.deleteByPrimaryKey(id);
}

观察结果发现:

在这里插入图片描述

统一异常处理类:

//@ControllerAdvice
public class CommonControllerAdvice {

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public R handler(RuntimeException e){
        e.printStackTrace();
        return R.fail("系统异常");
    }
}

在进行测试:

在这里插入图片描述

测试结果数据已经回复正常了, 但是有一个问题,现在所有的信息都为系统异常了, 有一些错误信息我是想要让用户看到的,比如密码错误,账号不存在这些错误信息。

解决: 自定义异常, 框架内部的错误就返回系统异常, 如果是我们想要让用户看到的信息我们就自己抛出异常

自定义异常类:

public class BusinessException extends RuntimeException {

    public BusinessException() {
    }

    public BusinessException(String message) {
        super(message);
    }
}

Service:

public int deleteByPrimaryKey(Long id) {
if(1==1){
    throw new BusinessException("id 不为空");
}
return departmentMapper.deleteByPrimaryKey(id);
}

异常处理类:

@ExceptionHandler(BusinessException.class)
@ResponseBody
public R handler1(RuntimeException e){
    e.printStackTrace();
    return R.fail(e.getMessage());
}

测试:

在这里插入图片描述

四、员工管理

员工表:employee

在这里插入图片描述

4.1 、员工列表

前台界面

在这里插入图片描述

后台接口

功能描述员工分页条件查询
URLhttp://localhost:8080/api/employee/list?pageNum=1&pageSize=5
请求方式get
参数名必填类型描述
pageNumint当前页
pageSizeint每页显示多少条
deptIdint部门id
keywordstring模糊查询关键字(姓名或邮箱)
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,员工标识 “name”: “admin”,员工名称 “password”: null,员工密码 “email”: “admin@qq.com”,员工邮箱 “age”: 18,员工年龄 “admin”: true,是否为管理员 “department”: { 部门信息 “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 } } ], “total”: 21 总条数 }
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”: { “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,员工标识 “name”: “admin”,员工名称 “password”: null,员工密码 “email”: “admin@qq.com”,员工邮箱 “age”: 18,员工年龄 “admin”: true,是否为管理员 “department”: { 部门信息 “id”: 1,部门标识 “name”: “人力资源部”,部门名称 “sn”: “Human Resources Department” 部门简称 } } ], “total”: 21 总条数 }}

代码:

@GetMapping("/list")
public R query(EmployeeQueryObject queryObject){

    PageInfo pageinfo = employeeService.query(queryObject);
    Result r = new Result(pageinfo.getTotal(),pageinfo.getList(),queryObject.getPageNum(),queryObject.getPageSize());
    return R.ok(r);

}

测试:使用 postman 访问 http://localhost:8080/api/employee/list?pageNum=1&pageSize=5

4.2 、员工高级查询

创建 EmployeeQueryObject

@Setter
@Getter
public class EmployeeQueryObject extends QueryObject {
    private String keyword; // 关键字
    private Long deptId; // 部门 id

}

xml 文件加条件

 <sql id="where_sql">
        <where>
            <if test="keyword != null and keyword !=''">
                AND (e.name like concat('%', #{keyword}, '%') or e.email like concat('%', #{keyword}, '%'))
            </if>
            <if test="deptId != null">
                AND e.dept_id = #{deptId}
            </if>
        </where>
    </sql>

4.3 、员工删除

前端界面

在这里插入图片描述

后台接口:

功能描述删除员工信息
URLhttp://localhost:8080/api/employee/delete/11
请求方式delete
参数名必填类型描述
idint部门标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

代码:

   @DeleteMapping("/delete/{id}")
    public R delete(@PathVariable Long id){
        employeeService.delete(id);
        return R.ok();
    }

**测试:**使用 postman发送http://localhost:8080/api/employee/delete/11](http://localhost:8080/api/department/delete/11)

4.3.1 删除关系

由于员工和角色之间是存在多对多的关系的, 关系存在中间表中, 如果员工不存在了, 中间表的数据也应该删除掉

代码:

 @Override
    public void delete(Long id) {
        employeeMapper.deleteByPrimaryKey(id);
        employeeMapper.deleteRelation(id);
    }

Xml:

<delete>
delete id="deleteRelation">
    delete from employee_role where employee_id = #{id}
</delete>

4.4 、查询单个员工

后台接口:

功能描述获取员工信息
URLhttp://localhost:8080/api/employee/info/11
请求方式get
参数名必填类型描述
idint员工标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “id”: 12,员工标识 “name”: “吴总”,员工名称 “password”: null,员工密码 “email”: “wuz@xxx.cn”,员工邮箱 “age”: 35,员工年龄 “admin”: false,是否为管理员 “department”: { “id”: 5,部门标识 “name”: “技术部”,部门名称 “sn”: "Technolog Department " 部门简称 } }
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: { “id”: 12,员工标识 “name”: “吴总”,员工名称 “password”: null,员工密码 “email”: “wuz@xxx.cn”,员工邮箱 “age”: 35,员工年龄 “admin”: false,是否为管理员 “department”: { “id”: 5,部门标识 “name”: “技术部”,部门名称 “sn”: "Technolog Department " 部门简称 } }}

代码:

@GetMapping("/info/{id}")
public R get(@PathVariable Long id){
    Employee employee = employeeService.get(id);
    return R.ok(employee);
}

测试:使用 postman 访问http://localhost:8080/api/employee/info/11

4.5、员工保存和编辑

前台界面

在这里插入图片描述

在前台界面中发现不仅仅要添加员工的基本信息,还要添加员工拥有的角色

修改界面

在这里插入图片描述

关系表:employee_role

在这里插入图片描述

后台接口:

功能描述保存或更新员工信息
URLhttp://localhost:8080/api/employee/saveOrUpdate
请求方式post
参数名必填类型描述
employeeRoleVojson添加 { “employee”:{ 员工信息 “name”: “xiaoliu”,员工名称 “password”: “123”,员工密码 “email”: “111@qq.com”,员工邮箱 “age”: 18 员工年龄 }, “roleIds”:[1,2,3] 员工对应角色数组 }
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “保存或者更新成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

代码:

controller:

 @PostMapping("/saveOrUpdate")
    @RequiredPermission(name = "员工列表",expression = "employee:saveOrUpdate")
    public R saveOrUpdate(@RequestBody EmployeeVo employeeVo){
        if(employeeVo.getEmployee().getId() == null){
            employeeService.save(employeeVo);
        }else{
            employeeService.update(employeeVo);
        }
        return R.ok();
    }

service:

 @Override
  @Transactional
    public void save(EmployeeVo employeeVo) {
        Employee employee = employeeVo.getEmployee();
        employeeMapper.insert(employee);
        Long[] roleIds = employeeVo.getRoleIds();
        if (roleIds != null && roleIds.length > 0) {
            for (Long roleId : roleIds) {
                //保存关系到中间表中
                employeeMapper.insertRelation(employee.getId(),roleId);
            }
//            employeeMapper.insertBatchRelation(employee.getId(), roleIds);
        }
    }

 @Override
    public void update(EmployeeVo employeeVo) {
        Employee employee1 = employeeVo.getEmployee();
        //更新员工基本数据
        employeeMapper.updateByPrimaryKey(employee1);
        //先删除旧关系, 在添加新关系
        employeeMapper.deleteRelation(employeeVo.getEmployee().getId());

        Long[] roleIds = employeeVo.getRoleIds();
        if (roleIds != null && roleIds.length > 0) {
            for (Long roleId : roleIds) {
                //保存关系到中间表中
                employeeMapper.insertRelation(employee1.getId(), roleId);
            }
        }
    }

mapper:

<insert id="insertRelation">
        insert into employee_role (employee_id,role_id) values (#{employeeId},#{roleId})
</insert>
<delete id="deleteRelation">
        delete from employee_role where employee_id = #{id}
</delete>

4.5.1 保存优化

由于在 for 循环中进行遍历,会对 mysql 造成冲击,采用批量插入

 @Override
    @Transactional
    public void save(EmployeeVo employeeVo) {
        Employee employee = employeeVo.getEmployee();
        employeeMapper.insert(employee);
        Long[] roleIds = employeeVo.getRoleIds();
        if (roleIds != null && roleIds.length > 0) {           employeeMapper.insertBatchRelation(employee.getId(), roleIds);
        }
    }

xml:

<insert id="insertBatchRelation">
    insert into employee_role (employee_id,role_id) values
    <foreach collection="roleIds" separator="," item="roleId">
        (#{id},#{roleId})
    </foreach>
</insert>

4.6 、更新管理员状态

前端:

在这里插入图片描述

后端接口:

功能描述更新员工管理员状态
URLhttp://localhost:8080/api/employee/updateState
请求方式post
参数名必填类型描述
adminStateVojson{ “id”:2,员工标识 “admin”:true 是否为管理员}
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “保存或者更新成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

代码:

 @PostMapping("/updateState")
    @RequiredPermission(name = "更新状态",expression = "employee:updateState")
    public R updateState(@RequestBody AdminVO adminVO){
        employeeService.updateState(adminVO);
        return R.ok();
    }

测试:使用 postman http://localhost:8080/api/employee/updateState观察数据库数据

五、权限管理

权限表:permission

在这里插入图片描述

5.1、权限列表

前端界面:

在这里插入图片描述

后端接口:

功能描述权限分页查询
URLhttp://localhost:8080/api/permission/list?pageNum=1&pageSize=5
请求方式get
参数名必填类型描述
pageNumint当前页
pageSizeint每页显示多少条
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 35,权限标识 “name”: “部门分页列表”,权限名称 “expression”: “department:list” 权限表达式 } ], “total”: 10 总条数 }
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”: { “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 35,权限标识 “name”: “部门分页列表”,权限名称 “expression”: “department:list” 权限表达式 } ], “total”: 10 总条数 }}

5.2、加载权限

前端:

在这里插入图片描述

后端接口:

功能描述加载方法标识权限信息到数据库
URLhttp://localhost:8080/api/permission/load
请求方式post
参数名必填类型描述
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “加载权限信息成功” 操作信息 “status”: “success”,操作状态 “data”: null}

加载权限分析: 画图解析

访问控制其实就控制访问 controller 方法, 方法就是我们要控制的资源, 如何做到区分, 那些需要权限, 那些是不需要权限的呢。

解决: 自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredPermission {
    String name();
    String expression();
}

在需要权限的方法上贴注解:

@GetMapping("/listAll")
@RequiredPermission(name = "部门列表",expression = "department:list")
public R listAll(QueryObject queryObject){

    List<Department> list = departmentService.selectAll();
    return R.ok(list);
}

做到区分完以后我们要思考如何把贴了注解中的信息最终保存到数据库中呢?

方式 1:

分析步骤

  • 获取到 ApplicationContext (Spring 容器对象)
  • 获取容器中的 所有的 Controller
  • 获取到 所有 Controller 的方法
  • 获取到方法上的注解
  • 获取注解中的信息
  • 保存到数据库中

代码实现:

/**
 * 加载权限
 */
@Override
public void load() {
   //0 先查询数据库所有的权限
    List<String> list = permissionMapper.selectExpression();
    //1 获取 Spring 容器对象
    Map<String, Object> beans = ctx.getBeansWithAnnotation(Controller.class);
    Collection<Object> values = beans.values();
    //2 从 Spring 容器对象中获取 Controller
    for (Object controller : values) {
        //3 从 Controller 中拿到所有的方法
        Method[] methods = controller.getClass().getDeclaredMethods();
        for (Method method : methods) {
            RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
            if (annotation != null) {
                //权限名称
                String name = annotation.name();
                //权限表达式
                String expression = annotation.expression();
                if (!list.contains(expression)) {
                    Permission p = new Permission();
                    p.setExpression(expression);
                    p.setName(name);
                    permissionMapper.insert(p);
                }
            }
        }
    }
}

5.2.1 存在重复加载问题

解决:

    //0 先查询数据库所有的权限
    List<String> list = permissionMapper.selectExpression();
    //判断数据库如果不存在在插入
    if (!list.contains(expression)) {}

方式 2:

Spring MVC 应用启动时,会利用 RequestMappingHandlerMapping 对象,搜集并分析每个控制器中每个带 @RequestMapping 注解的处理方法,通过 HandlerMethod 来封装和表示该方法,并与注解中的映射路径一一绑定。HandlerMethod 封装了很多属性,可以方便的访问到请求方法、方法参数、方法上的注解、所属类等信息。

分析步骤:

  • 获取 RequestMappingHanlderMapping 对象
  • 获取所有 HandlerMethod
  • 通过 HandlerMethod 获取所有的方法
  • 通过方法获取注解
  • … 后面和之前一样

代码:

@Override
public void load() {

    //0 先查询数据库所有的权限
    List<String> list = permissionMapper.selectExpression();
    Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
    Collection<HandlerMethod> handlerMethods = map.values();
    //获取 HandlerMethod
    for (HandlerMethod handlerMethod : handlerMethods) {
        Method method = handlerMethod.getMethod();
        RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
        if (annotation != null) {
            //权限名称
            String name = annotation.name();
            //权限表达式
            String expression = annotation.expression();
            if (!list.contains(expression)) {
                Permission p = new Permission();
                p.setExpression(expression);
                p.setName(name);
                permissionMapper.insert(p);
            }
        }
    }
}

注:不建议采用这种方式应为在一些情况下直接注入requestMappingHandlerMapping会报错

5.3、获取权限信息

前端界面:

在这里插入图片描述

右侧是角色拥有的权限:

后端接口:

功能描述通过角色id获取相应的权限信息
URLhttp://localhost:8080/api/permission/queryPermission/11
请求方式get
参数名必填类型描述
idint角色标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataarray[ { “id”: 79,权限标识 “name”: “部门删除”,权限名称 “expression”: “department:delete” 权限表达式 } ]
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”: [ { “id”: 79, “name”: “部门删除”, “expression”: “department:delete” } ]}

代码:

select id="queryPermissionByEmpId" resultType="java.lang.String">
        select expression
        from permission p
                 left join role_permission rp on (p.id = rp.permission_id)
                 left join employee_role er on (er.role_id = rp.role_id)
        where er.employee_id = #{id}
    </select>

六、角色管理

角色表:role

在这里插入图片描述

6.1、角色列表

前端界面:

在这里插入图片描述

后台接口:

功能描述角色分页查询
URLhttp://localhost:8080/api/role/list?pageNum=1&pageSize=5
请求方式get
参数名必填类型描述
pageNumint当前页
pageSizeint每页显示多少条
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,角色标识 “name”: “人事管理”,角色名称 “sn”: “HR_MGR” 角色简称 } ], “total”: 21 总条数 }
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”: { “pageNum”: 1,当前页 “pageSize”: 2,每页数据 “list”: [ { “id”: 1,角色标识 “name”: “人事管理”,角色名称 “sn”: “HR_MGR” 角色简称 } ], “total”: 21 总条数 }}

6.2、删除角色

后台接口:

功能描述删除角色信息
URLhttp://localhost:8080/api/role/delete/11
请求方式delete
参数名必填类型描述
idint角色标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

6.2.1、删除关系

<delete id="deleteRelation">
        delete
        from role_permission
        where role_id = #{id}
    </delete>

6.3、查询单个角色

后台接口:

功能描述获取角色信息
URLhttp://localhost:8080/api/role/info/11
请求方式get
参数名必填类型描述
idint角色标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “id”: 11, 角色标识 “name”: “市场经理”, 角色名称 “sn”: “Market_Manager” 角色简称 }
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: { “id”: 11, 角色标识 “name”: “市场经理”, 角色名称 “sn”: “Market_Manager” 角色简称 }}

6.4、查询所有角色

后台接口:

功能描述查询所有角色
URLhttp://localhost:8080/api/role/listAll
请求方式get
参数名必填类型描述
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataarray[ { “id”: 1,角色id “name”: “人事管理”,角色名称 “sn”: “HR_MGR” 角色简称 } ]
样例{ “code”: 200,状态码 “msg”: “”,操作提示信息 “status”: “success”,操作状态 “data”:[ { “id”: 1,角色id “name”: “人事管理”,角色名称 “sn”: “HR_MGR” 角色简称 } ]}

6.5、通过员工ID获取角色信息

前端界面:

在这里插入图片描述

后台接口:

功能描述通过员工ID获取角色信息
URLhttp://localhost:8080/api/role/query/11
请求方式get
参数名必填类型描述
idint员工标识
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataarray[ { “id”: 1, “name”: “人事管理”, “sn”: “HR_MGR” } ]
样例{ “code”: 200,状态码 “msg”: “删除成功”,操作提示信息 “status”: “success”,操作状态 “data”: [ { “id”: 11, 角色标识 “name”: “市场经理”, 角色名称 “sn”: “Market_Manager” 角色简称 } ]}

代码:

select id="queryByRoleId" resultType="cn.xxx.domain.Role">
        select *
        from role r
                 left join employee_role er on (r.id = er.role_id)
        where er.employee_id = #{id}
    </select>

6.6、角色新增或保存

前台界面:

在这里插入图片描述

关系表:role_employee

在这里插入图片描述

后台接口:

功能描述保存或更新角色信息
URLhttp://localhost:8080/api/role/saveOrUpdate
请求方式post
参数名必填类型描述
roleVojson添加 { “role”:{ 角色信息 “name”:“测试经理”,角色名称 “sn”:“TEST_MRG” } } 或 { “role”:{ 角色信息 “name”:“开发经理”,角色名称 “sn”:“DEV_MRG” 角色简称 }, “permissionIds”:[35,36,37,38,39,40,41,43] 角色对应权限数组 }更新 { “role”:{ 角色信息 “id”:13, 角色标识 “name”:“开发经理1”, 角色名称 “sn”:“DEV_MRG” 角色简称 }, “permissionIds”:[35,36,37,38,39,40,41,42,43] 角色对应权限数组 }
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200,状态码 “msg”: “保存或者更新成功”,操作提示信息 “status”: “success”,操作状态 “data”: null}

后台在保存的时候要考虑保存关系,和员工非常相似可以参考员工去写

代码:

    @Override
    public void insert(RoleVO record) {
        roleMapper.insert(record.getRole());
        Long[] permissionIds = record.getPermissionIds();
        //'chauffeur 关系
        if(permissionIds!=null &&  permissionIds.length>0){
            for (Long permissionId : permissionIds) {
                roleMapper.insertRelation(permissionId,record.getRole().getId());
            }
        }
    }
    
      @Override
    public void updateByPrimaryKey(RoleVO record) {
        Long[] permissionIds = record.getPermissionIds();
        roleMapper.updateByPrimaryKey(record.getRole());
        //先删除旧关系
        roleMapper.deleteRelation(record.getRole().getId());
        //插入新关系
        if(permissionIds!=null &&  permissionIds.length>0){
            for (Long permissionId : permissionIds) {
                roleMapper.insertRelation(permissionId,record.getRole().getId());
            }
        }
    }

七、登录管理

7.1、验证码

前台界面:

在这里插入图片描述

后台接口:

功能描述获取验证码
URLhttp://localhost:8080/api/code
请求方式get
参数名必填类型描述
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “img”: “”, 图片base64编码 “uuid”: “1a547608-cf34-4d98-9d6b-154935e9c71c” 唯一标识 }}
样例{ “code”: 200, “msg”: “”, “status”: “success”, “data”: { “img”: “”,图片base64编码 “uuid”: “1a547608-cf34-4d98-9d6b-154935e9c71c” 唯一标识 }}

流程画图分析-》看图

代码:

Controller:

@GetMapping("/code")
public R code(){
    Map<String,String> map = loginService.generate();
    return R.ok(map);
}

Service:

@Override
public Map<String, String> generate() {
    Map<String, String> map = VerifyCodeUtil.generateVerifyCode();
    String code = map.get("code");
    System.out.println(code);
    System.out.println(map.get("uuid"));
    // 把验证码放到 redis 中  verify_code: uuid     code
    redisUtils.set(Constant.VERFI_CODE_PREFIX+map.get("uuid"),code,Constant.EXPRE_TIME);
    map.remove("code");
    return map;
}

7.2、登录实现

后台接口:

功能描述员工登录
URLhttp://localhost:8080/api/login
请求方式post
参数名必填类型描述
loginInfoVojson{ “uuid”:“1a547608-cf34-4d98-9d6b-154935e9c71c”,用户名称 “username”:“xiaohei”,用户名称 “password”:“123”,用户密码 “code”:"8KYY"验证码 }
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobject{ “id”: 23,员工标识 “name”: “xiaohei”,员工名称 “password”: “123”,员工密码 “email”: “111@qq.com”,员工邮箱 “age”: 22,员工年龄 “admin”: false,是否为管理员 “department”: null 部门信息 }
样例{ “code”: 200,状态码 “msg”: “登录成功”,操作提示信息 “status”: “success”,操作状态 “data”: { “id”: 23,员工标识 “name”: “xiaohei”,员工名称 “password”: null,员工密码 “email”: “111@qq.com”,员工邮箱 “age”: 22,员工年龄 “admin”: false,是否为管理员 “department”: null 部门信息 }}

流程画图分析-》看图

思路:

  • 参数非空校验
  • 验证码校验 , 这里主要对比 Redis 中存的验证码和用户传递过来的验证码是否一致
  • 根据用户的账号密码上数据库查询数据
  • 如果为空抛出异常
  • 如果不为空 , 把用户信息放到 Redis 中
  • 把当前用户所拥有的权限表达式放到 Redis 中

思考: 为什么把数据放到 Redis 中而没有放到 session 中?

代码:

@Override
public Employee login(LoginVO loginVO) {
    //参数校验
    if(loginVO==null){
        throw new BusinessException("非法操作");
    }

    if(StringUtils.isEmpty(loginVO.getUsername()) || StringUtils.isEmpty(loginVO.getPassword())){
        throw new BusinessException("账号密码不能为空");
    }

    if(StringUtils.isEmpty(loginVO.getCode())){
        throw new BusinessException("验证码不能为空");
    }
    // 从 redis 中获取验证码
    String redisCode = redisUtils.get(Constant.VERFI_CODE_PREFIX + loginVO.getUuid());
    boolean flag = VerifyCodeUtil.verification(redisCode, loginVO.getCode(), true);
    if(!flag){
        throw new BusinessException("验证码不正确");
    }
    // 根据账号密码去查询数据
    Employee employee = employeeService.login(loginVO.getUsername(),loginVO.getPassword());
    if(employee == null){
        throw new BusinessException("账号密码错误");
    }
    // 把当前登录用户放到 redis 中为了后去判断是否登录做铺垫
    // login_employee:id     employee
    redisUtils.set(Constant.LOGIN_EMPLOYEE+employee.getId(), JSON.toJSONString(employee),Constant.EXPRE_TIME);
    // 把当前登录用户所拥有的权限放到 session 中
    // 根据当前用户查询 用户拥有权限表达式
    List<String> expressions = permissionService.queryPermissionByEmpId(employee.getId());
    redisUtils.set(Constant.EMPLOYEE_EXPRESSIONS+employee.getId(),JSON.toJSONString(expressions),Constant.EXPRE_TIME);
    return employee;
}

八、登出操作

思路:

  • 把之前登录存到 Redis 的数据删除

后台接口:

功能描述注销
URLhttp://localhost:8080/api/logout
请求方式get
参数名必填类型描述
返回
参数名类型描述
codeint200成功,401认证失败,403无权限操作,500失败
msgstring操作提示信息
statusstringsuccess 成功,fail 失败
dataobjectnull
样例{ “code”: 200, “msg”: “退出成功”, “status”: “fail”, “data”: null}

注: 这里 userId 没有放到请求参数中, 而是后续在每次请求头中携带, 每次携带后台都需要接受麻烦, 直接放在请求头中

代码实现

@Override
public void logout(String userId) {
    // 退出主要就是将之前登录放到 redis 中的数据给删除掉
    redisUtils.del(Constant.LOGIN_EMPLOYEE+userId);
    redisUtils.del(Constant.EMPLOYEE_EXPRESSIONS+userId);

}

九、拦截管理

9.1、登录拦截

流程画图分析-》 看图

思路:

  • 上 Redis 中查询是否有用户数据
  • 如果没有响应{code:401,status:false,msg:验证失败,data:null}
  • 如果有放行

拦截实现:

@Component
public class CheckLoginIntercptor extends HandlerInterceptorAdapter {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisUtils redisUtils;

    //主要做的事情就是判断 redis 中是否有数据, 如果有就放行, 如果没有返回 401 验证失败
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        String userId = request.getHeader("userId");
        response.setContentType("application/json;charset=utf8");
        //上 redis 中获取数据
        String obj = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);
        if(StringUtils.isEmpty(obj)){
            response.getWriter().write(JSON.toJSONString(R.fail(401,"验证失败")));
            return false;
        }
        return true;
    }
}

配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private CheckLoginIntercptor checkLoginIntercptor;
    @Autowired
    private CheckPermissionIntercptor checkPermissionIntercptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkLoginIntercptor).
                addPathPatterns("/**").
             excludePathPatterns("/api/login","/api/code","/api/logout");

}

9.2、权限拦截

流程画图分析-》 看图

思路:

  • 从 Redis 中查询当前登录用户数据
  • 判断当前登录用户是否是超级管理员,如果是直接放行
  • 拿到当前访问的方法
  • 拿到访问方法的注解,看是否有@RequiredPermission 注解,如果没有放心
  • 上 Redis 中获取当前登录用户所拥有的权限表达式集合
  • 判断注解中的权限表达式是否在集合中,如果在直接放行
  • 如果不在返回{code:403,state:false,msg:“没有权限”,data:null},进行拦截

代码实现:

@Component
public class CheckPermissionIntercptor extends HandlerInterceptorAdapter {
    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        response.setContentType("application/json;charset=utf-8");

        // 1 获取到当前登录用户
        String userId = request.getHeader("userId");
        String jsonObj = redisUtils.get(Constant.LOGIN_EMPLOYEE + userId);
        Employee employee = JSON.parseObject(jsonObj, Employee.class);
        // 2 判断是否是超级管理员如果是直接放行
        if(employee.isAdmin()){
            return true;
        }
        // 3 获取到访问的方法, 如果方法上没有注解放行
        if(handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
            if(annotation == null){
                return true;
            }
            // 4 获取 redis 中的权限集合表达式
            String jsonExpressions = redisUtils.get(Constant.EMPLOYEE_EXPRESSIONS + userId);
            ArrayList<String> list = JSON.parseObject(jsonExpressions, ArrayList.class);
            // 5 如果在集合中放行
            if(list.contains(annotation.expression())){
                return true;
            }
            // 6 如果不在就进行拦截, 返回 403 没有权限
            response.getWriter().write(JSON.toJSONString(R.fail(403,"没有权限")));
        }

        return false;
    }

}

配置:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private CheckLoginIntercptor checkLoginIntercptor;
    @Autowired
    private CheckPermissionIntercptor checkPermissionIntercptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkLoginIntercptor).
                addPathPatterns("/**").
                excludePathPatterns("/api/login","/api/code","/api/logout");

        registry.addInterceptor(checkPermissionIntercptor).
                addPathPatterns("/**").
                excludePathPatterns("/api/login","/api/code","/api/logout");
    }
}

十、项目联调

项目架构采用前后端分离,目前后端接口全部写完, 前端项目假设也写完了, 那么要一起启动调试一下看是否有 bug。

联调的过程中可能会出现各种各样的问题,可能一个参数名字,或者请求方式不对应都会报错,所以后台接口要严格按照接口文档去写。

操作步骤:

  • 启动后端项目
  • 用 vscode 打开前端项目,输入 npm run serve

这里注意先启动后端项目, 在启动前端项目, 前端项目在启动的时候会看8080端口是否占用,如果占用就会换其他端口,或者直接修改前端项目中main.js的axios.defaults.baseURL="http://127.0.0.1:8080/api/"属性值

发现问题: 登录界面中的验证码没办法加载出来

10.1 、跨域问题

错误图片:

在这里插入图片描述

当在浏览器的控制台中看到这段的错误信息,那么已经产生跨域了。

10.1.1、跨域产生的原因

跨域是是因为浏览器的同源策略限制,是浏览器的一种安全机制,服务端之间是不存在跨域的。所谓同源指的是两个页面具有相同的协议、主机和端口,三者有任一不相同即会产生跨域。

在这里插入图片描述

10.1.2、跨域的解决方案

跨域的解决方案有很多,有贴注解的方式有配置的方式,在这里我们用注解的方式

方式 1: 局部注解方式:@CrossOrigin

@RestController
@CrossOrigin(allowCredentials = "true")
public class EmployeeController {
 
	  @GetMapping("/list")
    public List<Employee> list(){
        return employeeServcie.list();
    }
}

问题: 我们按照解决方案处理了以后发现还是有跨域问题

在这里插入图片描述

原因: 这是因为浏览器会发送一个预请求,看服务器是否支持跨域,如果支持才会发送真实请求, 但是在发送预请求的时候是不会携带 userId, 所以在拦截器中过不去。

解决:

在这里插入图片描述

如果是预请求我们就放行,预请求的handler类型不是 HandlerMethod。

方式 2:全局配置

@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址
        config.addAllowedOrigin("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 有效期 1800秒
        config.setMaxAge(1800L);
        // 添加映射路径,拦截一切请求
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        // 返回新的CorsFilter
        return new CorsFilter(source);
    }

}

10.2、测试接口

  • 测试部门 CRUD 接口是否正常
  • 测试员工 CRUD 加高级查询接口是否正常
  • 测试角色 CRUD 接口是否正常
  • 测试权限加载和权限列表是否正常
  • 测试登出功能
  • 测试登录拦截
  • 测试权限拦截

注: 在测试的过程中如果发现问题, 先使用 postman测试自己的后端接口是否有问题, 如果没有问题,就是前端的问题。

比如:测试登出接口的时候,发现在控制台报错

在这里插入图片描述

但是我们使用 postman 测试接口的时候没有任何问题,那么说明是前端哪里存在问题, 我们找到前端的位置,发现请求的方式没有对应上。

在这里插入图片描述

在公司中如果出现了这种问题如果自己后台的接口都没有任何问题, 那么就要找到前端沟通看到底是哪里出现了问题,遇到问题不需要相互推卸责任,还是要多沟通去解决问题。

十一、阅读前端代码(了解)

现目前外面公司的项目前后端分离的比例还是很高的, 后端人员只需要写接口,前端人员负责写界面, 但是如果作为后端人员能够看懂前端的代码肯定是更好的。

注意:这个章节主要的目的,想要利用这个前端项目提高一下大家看代码的能力, 因为以后进入到公司前一周不是写代码而是看代码熟悉项目,这就考验大家看代码的能力,作为一名合格的程序员我们不仅仅要会写代码还要会看代码, 还要会模仿别人的代码改造出来我们自己的代码。

11.1 、项目结构分析

观察项目结构

在这里插入图片描述

项目明细图:

在这里插入图片描述

组件图:

一个界面是有若干个组件拼接而成,这些组件我们是从elementUi官网直接拷贝的, 然后把数据修改成我们的数据就可以了。

官网: https://element.eleme.cn/#/zh-CN

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1490070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++:Vector的模拟实现

创作不易&#xff0c;感谢三连 &#xff01;&#xff01; 一&#xff0c;前言 在学习string类的时候&#xff0c;我们可能会发现遍历的话下标访问特别香&#xff0c;比迭代器用的舒服&#xff0c;但是下标其实只能是支持连续的空间&#xff0c;他的使用是非常具有局限性的&am…

迷不迷糊?前后端、三层架构和MVC傻傻分不清

现在的项目都讲究前后端分离&#xff0c;那到底什么是前后端&#xff0c;前后端和以前的MVC以及三层架构啥关系呢&#xff1f;今天就这个问题展开一下&#xff0c;方面后面的学习&#xff0c;因为前面讲的jsp、servlet和javabean根据实例&#xff0c;基本上有一个框架的理解了&…

基于STC12C5A60S2系列1T 8051单片机的TM1638键盘数码管模块的按键扫描、数码管显示按键值、显示按键LED应用

基于STC12C5A60S2系列1T 8051单片机的TM1638键盘数码管模块的按键扫描、数码管显示按键值、显示按键LED应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍TM1638键盘…

【如何在Docker中,修改已经挂载的卷(Volume)】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 提示&#xff1a;添加投票&#xff01;&#xff01;&#xff01; 目录 简述概要知识图谱 简述概要 如何在Docker中&#xff0c;修改已经挂载的卷&#xff08;Volume&#xff09; 知识图谱 在Docker中&#xff0c;修改已经挂载…

消息队列-kafka-消息发送流程(源码跟踪)

官方网址 源码&#xff1a;https://kafka.apache.org/downloads 快速开始&#xff1a;https://kafka.apache.org/documentation/#gettingStarted springcloud整合 发送消息流程 主线程&#xff1a;主线程只负责组织消息&#xff0c;如果是同步发送会阻塞&#xff0c;如果是异…

安装Proxmox VE虚拟机平台

PVE是专业的虚拟机平台&#xff0c;可以利用它安装操作系统&#xff0c;如&#xff1a;Win、Linux、Mac、群晖等。 1. 下载镜像 访问PVE官网&#xff0c;下载最新的PVE镜像。 https://www.proxmox.com/en/downloads 2. 下载balenaEtcher balenaEtcher用于将镜像文件&#…

【Vue3】3-6 : 仿ElementPlus框架的el-button按钮组件实

文章目录 前言 本节内容实现需求完整代码如下&#xff1a; 前言 上节,我们学习了 slot插槽&#xff0c;组件内容的分发处理 本节内容 本小节利用前面学习的组件通信知识&#xff0c;来完成一个仿Element Plus框架的el-button按钮组件实现。 仿造的地址&#xff1a;uhttps://…

docker pull 拉取失败,设置docker国内镜像

遇到的问题 最近在拉取nginx时&#xff0c;显示如下错误&#xff1a;Error response from daemon: Get “https://registry-1.docker.io/v2/”: net/http: request canceled (Client.Timeout exceeded while awaiting headers)。 这个的问题是拉取镜像超时&#xff0c;通过检索…

基于Golang客户端实现Nacos服务注册发现和配置管理

基于Golang客户端实现Nacos服务注册发现和配置管理 背景 最近需要把Golang实现的一个web项目集成到基于Spring Cloud Alibaba的微服务体系中&#xff0c;走Spring Cloud Gateway网关路由实现统一的鉴权入口。 软件版本 组件名称组件版本Nacos2.2.0Go1.21.0Ginv1.9.1Nacos-s…

项目部署发布

目录 上传数据库 修改代码中的数据源配置 修改配置文件中的日志级别和日志目录 打包程序 ​编辑​编辑 上传程序 查看进程是否在运行 以及端口 云服务器开放端口(项目所需要的端口) 上传数据库 通过xshell控制服务器 创建目录 mkdir bit_forum 然后进入该目录 查看路…

【AI+CAD】(一)ezdxf 解析DXF文件

DXF文件格式理解 DXF文件格式是矢量图形文件格式&#xff0c;其详细说明了如何表示不同的图形元素。 DXF是一个矢量图形文件&#xff0c;它捕获CAD图形的所有元素&#xff0c;例如文本&#xff0c;线条和形状。更重要的是&#xff0c;DXF是用于在CAD应用程序之间传输数据的图形…

Java日志框架的纷争演进与传奇故事

在Java的世界里&#xff0c;日志记录是每一个应用不可或缺的部分。它帮助开发者了解应用的运行状态、调试问题、监控性能等。而在这背后&#xff0c;是一系列日志框架的发展与演进。今天&#xff0c;就让我们一起回顾这些日志框架的历史&#xff0c;探寻它们背后的故事。 1. Lo…

分布式数据库中全局自增序列的实现

自增序列广泛使用于数据库的开发和设计中&#xff0c;用于生产唯一主键、日志流水号等唯一ID的场景。传统数据库中使用Sequence和自增列的方式实现自增序列的功能&#xff0c;在分布式数据库中兼容Oracle和MySQL等传统数据库语法&#xff0c;也是基于Sequence和自增列的方式实现…

使用Visual Studio 2022 创建lib和dll并使用

概述&#xff1a;对于一个经常写javaWeb的人来说,使用Visual Studio似乎没什么必要&#xff0c;但是对于使用ffi的人来说&#xff0c;使用c或c编译器&#xff0c;似乎是必不可少的&#xff0c;下面我将讲述如何用Visual Studio 2022 来创建lib和dll&#xff0c;并使用。 静态库…

UNIapp实现局域网内在线升级

首先是UNIapp 生成apk 用Hbuilder 进行打包 可以从网站https://www.yunedit.com/reg?gotocert 使用自有证书&#xff0c;目测比直接使用云证书要快一些。 发布apk 网站 用IIS发布即可 注意事项中记录如下内容 第一、需要在 iis 的MiMe 中添加apk 的格式&#xff0c;否则无法…

Java架构之路-架构应全面了解的技术栈和工作域

有时候我在想这么简单简单的东西&#xff0c;怎么那么难以贯通。比如作为一个架构师可能涉及的不单单是技术架构&#xff0c;还包含了项目管理&#xff0c;一套完整的技术架构也就那么几个技术栈&#xff0c;只要花点心思&#xff0c;不断的往里面憨实&#xff0c;总会学的会&a…

UE4升级UE5 蓝图节点变更汇总(4.26/27-5.2/5.3)

一、删除部分 Ploygon Editing删除 Polygon Editing这个在4.26、4.27中的插件&#xff0c;在5.1后彻底失效。 相关的蓝图&#xff0c;如编辑器蓝图 Generate mapping UVs等&#xff0c;均失效。 如需相关功能&#xff0c;请改成Dynamic Mesh下的方法。 GetSupportedClass删…

在K8S集群中部署SkyWalking

1. 环境准备 K8S 集群kubectlhelm 2. 为什么要部署SkyWalking&#xff1f; 我也不道啊&#xff0c;老板说要咱就得上啊。咦&#xff0c;好像可以看到服务的各项指标&#xff0c;像SLA&#xff0c;Apdex这些&#xff0c;主要是能够进行请求的链路追踪&#xff0c;bug排查的利…

C向C++的一个过渡

思维导图 输入输出&#xff0c;以及基础头文件 在c语言中我们常用scanf("%d",&n);和printf("%d\n",n);来输出一些变量和常量&#xff0c;在C中我们可以用cin;和cout;来表示输入输出。 在C语言中输入输出有头文件&#xff0c;在C也有头文件&#xff0…

解放人力,提升品质:码垛输送机的工业应用与价值

在现代工业生产中&#xff0c;码垛输送机已成为许多企业自动化生产线上的关键设备。它不仅可以提高生产效率&#xff0c;降低人力成本&#xff0c;还能确保产品质量&#xff0c;并为企业带来许多其他方面的实际好处。 1. 提高生产效率&#xff1a; 快速码垛&#xff1a;码垛输…