【开源风云】从若依系列脚手架汲取编程之道(五)

📕开源风云系列

  • 🍊本系列将从开源名将若依出发,探究优质开源项目脚手架汲取编程之道。
  • 🍉从不分离版本开写到前后端分离版,再到微服务版本,乃至其中好玩的一系列增强Plus操作
  • 🍈希望你具备如下技术栈
    • 🍎Spring
    • 🍍SpringMVC
    • 🍐Mybatis/Mybatis-plus
    • 🍅Thymeleaf
    • 🥝SpringBoot
    • 🍓Shiro
    • 🍏SpringSecurity
    • 🍌SpringCloud
    • 🍒云服务器相关知识
  • 本篇是分离版第三篇

目录

1、分页

[!NOTE]

分页主要是后端做,前端只需要传给后端,我要第几页,每页多少条数据。

1.1、前端封装分页

操作日志页面为例子,前端每次给后端发请求的时候,都是带了分页参数的,默认都是请求第一页,每页10条数据。

我们请求后端接口的时候会带上queryParams对象,我们进入日志页面,就会执行getList方法,进而执行list方法发送请求,发送请求的时候会带上queryParams、dateRange 参数,这个dateRange参数是日期范围,也就是前端搜索框筛选日期的参数,为什么这个日期范围参数不放在 queryParams 对象里面一起传参进去呢?

其实 dateRange 日期范围是在 data 里面,和 element ui 的日期选择器进行了 v-model 双向绑定

若依对 element ui 的分页进行了封装,包括分页的页码数等等,可以结合 element ui 的 Pagination 分页的参数进行更改。

[!NOTE]

前端只需要维护好数据,我需要当前是第几页,一共有几条数据,每页多少条

前端只需要维护好这三个数据,其他的交给后端就行了。

1.2、后端分页方式

后端是通过pagehelper插件实现的。ry通过pagehelper间接引入了mybatis:

[!NOTE]

  • 若依在 BaseController中封装了分页方法。也就是说我们自己写的controller只要继承了BaseController,直接调用startPage方法即可完成分页。
  • 分页的源码调试讲解:源码解析 很牛,值得一看

我们的Controller继承BaseController,只需要在我们的Service的查询方法之前加上startPage()就会默认表示sql需要分页。

2、MyBatisPlus整合

关于若依集成MyBatisPlus,我们可以按照步骤如下:

  1. 在根pom.xml中引入mybatis-plus,在common模块中继承
  1. 在common模块下删除mybatis冲突依赖
  1. 填写 yml 配置
yaml 复制代码
mybatis-plus:
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  configLocation: classpath:mybatis/mybatis-config.xml
  # 搜索指定包别名
  typeAliasesPackage: com.kuang.**.domain
  1. 删除MyBatisConfig,也就是删除ry自己的配置文件,全部注释掉也可以

然后直接重启项目,成功!


  1. 为什么要排除pagehelper中的mybatis依赖?

答:因为mybatis-plus的启动器和pagehelper都引入了mybatis,mybatis-plus的启动器的mybatis版本比较高。pagehelper引入的mybatis的版本比较低,mybatis-plus用到了高版本mybatis的方法。所以需要排除pagehelper中的mybatis。否则报错没找到方法。但是我自己的版本如下:

Pagehelper V1.4.7:

  • mybatis:3.5.13
  • mybatis-spring:2.1.1

mybatis-plus V3.5.3:

  • mybatis:3.5.10
  • mybatis-spring:2.0.7

也就是其实mybatis-plus用的版本都是低的版本,那么maven会自动将高的版本排除,保留低版本的依赖。所以不需要手动排除,Maven会自动排除。

那么我把低版本的依赖排除,使用高版本的依赖,发现也是OK的。

2.1、MP测试

  1. 我们测试一下,改造一下登录日志的查询,首先给 mapper 继承 BaseMapper
  1. 在impl实现类下执行查询
  1. 访问日志管理会发现报错

控制台打印的SQL语句是:

sql 复制代码
SELECT  info_id,user_name,status,ipaddr,login_location,browser,os,msg,login_time,
search_value,create_by,create_time,update_by,update_time,remark,params 
FROM sys_logininfor  LIMIT ?

可以发现我们的登录日志数据库表中就没有 search_value 后面的字段了,那么为什么这个sql语句会带呢?

是因为继承关系导致的,SysLogininfor 继承了 BaseEntity,BaseEntity的属性也被写入了SQL中

所以我们只需要给BaseEntity的不需要的属性上加上注解@TableField(exist = false)

完成上述操作后,再进行登录日志菜单的点击,就可以正常查询显示了。

3、多数据源

多数据源不是必须的,需求是让一个项目同时连接操作多个数据库。

  1. 对于一个请求(假如此请求需要查询数据库),我们首先是前端点击,然后前端给后端发请求,后端controller层拿到请求,controller把请求发给service层,service层把数据再递交给mapper层,mapper层在若依中是mybatis接管。

  2. 当mybatis看到数据库查询请求后,这时候代理模式就开始了,我们的mapper层的接口被代理,框架直接通过反射拿到当前的代理对象、方法、相关参数、DefaultSqlSession去执行相关查询。

  3. 重点来了,怎么查询?首先要干嘛?当然是得到当前数据库的连接Connection,然后通过此数据库连接进行执行相关查询。

  4. 我们自己写一个类DynamicDataSource(动态数据源的英文),实现DataSource接口,然后重写getConnection方法,我们可以在DynamicDataSource类中准备好几个连接,然后getConnection方法中根据情况获得不同的连接。

[!NOTE]

总结:如果需要切换多个数据源,只需要把@DataSource注解加到方法上或者类上,切面会把注解中的value读出来并设置到线程私有上下文中,然后在getConnection的时候读取线程私有的value来确定用哪个数据源。

我们来玩一下三个MySQL数据源

  1. 首先在DruidConfig中增加一个数据源
  1. 修改一下数据库的枚举
  1. 注入三个数据库到动态数据库
java 复制代码
@Bean
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource,
                                    DataSource slaveDataSource,
                                    DataSource thirdDataSource) {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
    targetDataSources.put(DataSourceType.SLAVE.name(),slaveDataSource);
    targetDataSources.put(DataSourceType.THIRD.name(),thirdDataSource);
    return new DynamicDataSource(masterDataSource, targetDataSources);
}
  1. 我们在某个方法上面加上注解@DataSource(DataSourceType.THIRD),就会使用第三个数据库,就完成了切库的操作。

4、事务

若依-Vue中的事务和SpringBoot中完全一致。

在方法或者类上可以假如@Transactional注解,只要该注解加上,方法中无论有多少个执行SQL的地方,只要执行遇到一个异常,所有的操作都将会回滚。

@Transactional注解默认是捕捉运行时异常,如果需要抓更大的异常,可以设置参数:

java 复制代码
@Transactional(rollbackFor = Exception.class)

5、操作日志

操作日志是记录用户的某些操作。

为什么要记录呢?

  1. 可能后续系统出现问题,需要回来找证据的时候会用到。

  2. 供管理员查看用户的行为。

  3. 记录正确或者错误记录,也方便程序员修改程序。

总而言之,操作日志有它存在的必要。但是操作日志多少也会消耗一些性能。我们所记录的操作日志,一般都是用户对数据库进行增删改的记录。对于查询,用户并没有更改数据库,所以往往不会去记录。

老生常谈AOP。操作日志是通过下面的类实现的:

切面类会切Log注解,意思是要某个方法有操作日志,只需要手动加上@Log注解。IDEA可以帮我们看到我们切了哪个方法。

以新增部门方法为例,处理完请求后执行会拿到切入点、拿到@Log()注解、拿到请求返回的结果:

以用户修改菜单为例,我们只需要加上@Log(title = "用户管理", businessType = BusinessType.UPDATE)

当我们对用户信息进行修改的时候,就可以在操作日志菜单中看到

6、数据权限

所谓数据权限,就是不同的人对数据的权限不同。举个例子,一个学校的校长能看到学生管理系统的全部学生的信息,进行管理维护;而一个老师只能维护管理系统中自己班的学生,别的人根本看不到。

一般来说,但凡是一个管理类的系统,都有着数据方面的权限。ry中的数据权限是AOP切入,修改SQL实现的,让SQL中拼接一些关于权限的SQL来实现。

可以看到DataScopeAspect切面类,切的是注解@DataScope,处理请求前执行会拿到切入点、拿到@DataScope()注解。


我们来实操一下:

首先随便建立一个表,注意,表字段必须有user_id,不然怎么和用户关联,不和用户表关联谈什么数据权限?

比如说,有这样一个需求:

  • 作为一个高校的研究生,有可能要每周收青年大学习的截图。那么我现在需要学生自己去我的网站上面上传截图,并且用户自己只能看到自己上传的截图,但是管理员可以看到所有的截图。这样一个需求,我们马上安排。

SQL语句如下:

sql 复制代码
-- 创建kuang_picture表
CREATE TABLE `kuangstudy_vue`.`kuang_picture`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `uri` varchar(500) NULL COMMENT '图片uri',
  `user_id` bigint(20) NULL COMMENT '关联用户user_id',
  `create_by` varchar(50) NULL COMMENT '创建人',
  `create_time` datetime NULL COMMENT '创建时间',
  `update_by` varchar(50) NULL COMMENT '更新人',
  `update_time` datetime NULL COMMENT '更新时间',
  `remark` varchar(500) NULL COMMENT '备注',
  PRIMARY KEY (`id`)
);

这里我就直接使用若依的代码生成器来生成页面,只需要注意图片的URI显示类型是图片上传就好。

然后我们使用两个普通用户分别上传图片,发现这俩人竟然可以看到别人的图片。

这就不合理了,我们要自己开始编码,首先要填充 user_id,不然谈什么数据权限?填充之前表里面这么显示的:

我们开始编码填充:

然后重启项目,我们再次使用普通角色上传图片,就会连带着 user_id 显示

那么我们开始进行数据权限的编码,需要在查询的SQL上进行改动,我们对查询图片列表的sql进行改动:

我们给 kuang_picture 、 sys_user 、 sys_dept 三张表分别起别名为 kud ,由于 sys_user 中也有 create_by等字段,所以我们要指定查询的是哪张表的user_idcreate_by等字段。

所以我们加上查询指定的表名.id、表名.uri等等

最重要的一步:记得在sql最后加上 ${params.dataScope}

最后在方法上加上注解@DataScope,d表示部门表的别名,u表示用户表的别名。

这样就实现了我们使用 Gitee 登录的用户上传图片只有 Gitee 登录的用户可以看到, ry 登录的用户上传图片只有 ry 用户可以看到,管理员可以看到所有人的图片。

7、服务监控

从服务监控的前端页面展示看到,有CPU信息、内存信息、服务器信息、Java虚拟机信息和磁盘信息。

后端代码如上,就是调用API封装成Server对象,将信息返回给前端。

7.1、服务监控前端

当打开服务监控的页面时,页面会有一个加载的 loading

可能有小伙伴问,为啥先getList,后loading的,为啥loading还能走到?

因为getList里面有Promise的异步请求操作。异步不会阻塞。此外,getList方法的接口还睡了一秒钟,所以正常来说getList后执行完毕。

7.2、栅格布局

我们自己来仿照若依的服务监控来写一个栅格布局,首先新建菜单:

  1. 之后去views/kuang下新建kuang_server.vue
vue 复制代码
<template>
  <div class="kuang-contain">
    <el-row :gutter="30">
      <el-col :span="12">
        <el-card>
          <div slot="header">
            <span><i class="el-icon-cpu"></i> CPU</span>
            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
          </div>
          <div>
            <el-descriptions :column="2">
              <el-descriptions-item label="核心数" v-if="server.cpu"><el-tag size="medium">{{ server.cpu.cpuNum }}核</el-tag></el-descriptions-item>
              <el-descriptions-item label="系统使用率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.sys }}%</el-tag></el-descriptions-item>
              <el-descriptions-item label="用户使用率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.used }}%</el-tag></el-descriptions-item>
              <el-descriptions-item label="当前等待率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.wait }}%</el-tag></el-descriptions-item>
              <el-descriptions-item label="当前空闲率"v-if="server.cpu"><el-tag size="small">{{ server.cpu.free }}%</el-tag></el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div slot="header">
            <span><i class="el-icon-tickets"></i> 服务器内存</span>
            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
          </div>
          <div>
            <el-descriptions :column="2">
              <el-descriptions-item label="总内存" v-if="server.mem"><el-tag size="medium">{{ server.mem.total }}G</el-tag></el-descriptions-item>
              <el-descriptions-item label="已用内存"v-if="server.mem"><el-tag size="small">{{ server.mem.used }}G</el-tag></el-descriptions-item>
              <el-descriptions-item label="剩余内存"v-if="server.mem"><el-tag size="small">{{ server.mem.free }}G</el-tag></el-descriptions-item>
              <el-descriptions-item label="使用率"v-if="server.mem"><el-tag size="small">{{ server.mem.usage }}%</el-tag></el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
      </el-col>

      <el-col :span="6">
        <el-card>
          <div slot="header">
            <span><i class="el-icon-tickets"></i> JVM内存</span>
            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
          </div>
          <div>
            <el-descriptions :column="2">
              <el-descriptions-item label="总内存" v-if="server.mem"><el-tag size="medium">{{ server.jvm.total }}M</el-tag></el-descriptions-item>
              <el-descriptions-item label="已用内存"v-if="server.mem"><el-tag size="small">{{ server.jvm.used }}M</el-tag></el-descriptions-item>
              <el-descriptions-item label="剩余内存"v-if="server.mem"><el-tag size="small">{{ server.jvm.free }}M</el-tag></el-descriptions-item>
              <el-descriptions-item label="使用率"v-if="server.mem"><el-tag size="small">{{ server.jvm.usage }}%</el-tag></el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
      </el-col>
    </el-row>




    <el-row class="kuang-row">
      <el-col>
        <el-card>
          <div slot="header">
            <span><i class="el-icon-monitor"></i> 服务器信息</span>
            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
          </div>
          <div>
            <el-descriptions :column="2">
              <el-descriptions-item label="服务器IP" v-if="server.sys"><el-tag size="medium">{{ server.sys.computerIp }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="服务器名称"v-if="server.sys"><el-tag size="small">{{ server.sys.computerName }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="系统架构"v-if="server.sys"><el-tag size="small">{{ server.sys.osArch }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="操作系统"v-if="server.sys"><el-tag size="small">{{ server.sys.osName }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="项目路径"v-if="server.sys"><el-tag size="medium">{{ server.sys.userDir }}</el-tag></el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <el-row class="kuang-row">
      <el-col>
        <el-card>
          <div slot="header">
            <span>Java虚拟机</span>
            <el-button style="float: right; padding: 3px 0" type="text">操作按钮</el-button>
          </div>
          <div>
            <el-descriptions :column="3">
              <el-descriptions-item label="Java名称"  v-if="server.jvm"><el-tag size="medium">{{ server.jvm.name }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="服务启动时间" v-if="server.jvm"><el-tag size="small">{{ server.jvm.startTime }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="jdk安装路径" v-if="server.jvm"><el-tag size="small">{{ server.jvm.home }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="jdk版本" v-if="server.jvm"><el-tag size="small">{{ server.jvm.version }}</el-tag></el-descriptions-item>
              <el-descriptions-item label="最大可用内存数" v-if="server.jvm"><el-tag size="small">{{ server.jvm.max }}M</el-tag></el-descriptions-item>
              <el-descriptions-item label="运行时长" v-if="server.jvm"><el-tag size="medium">{{ server.jvm.runTime }}</el-tag></el-descriptions-item>
            </el-descriptions>
          </div>
        </el-card>
      </el-col>
    </el-row>


  </div>
</template>

<script>
import {getServer} from "@/api/monitor/server";


export default {
  name: "kuang_server",
  created() {
    this.getList();
    this.openLoading();
  },
  data(){
    return {
      server: [],
    }
  },
  methods: {
    /** 查询服务器信息 */
    getList() {
      getServer().then(response => {
        this.server = response.data;
        console.log(this.server);
        this.$modal.closeLoading();
      });
    },
    // 打开加载层
    openLoading() {
      this.$modal.loading("正在加载服务监控数据,请稍候!");
    }
  }
}
</script>

<style scoped>
.kuang-contain{
  margin: 30px;
}

.kuang-row{
  margin-top: 30px;
}

</style>

仿造若依写一个监控页面就完成了。

8、Swagger

原生swagger不但很丑,而且参数配置的请求list还有必填项很难绕过!所以我们来换个皮肤:

  1. 在跟pom.xml中引入
xml 复制代码
<!--Swagger增强-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-ui</artifactId>
    <version>${knife4j-spring-ui.version}</version>
</dependency>
  1. 在admin的pom.xml中继承
xml 复制代码
<!--Swagger增强-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-ui</artifactId>
</dependency>
  1. 编写前端vue文件
vue 复制代码
<template>
  <i-frame :src="url" />
</template>
<script>
import iFrame from "@/components/iFrame/index";
export default {
  name: "kuang_swagger",
  components: { iFrame },
  data() {
    return {
      url: process.env.VUE_APP_BASE_API + "/doc.html"
    };
  },
};
</script>
  1. 添加菜单

如此便大功告成

我们来看一下SwaggerConfig,启用Swagger,哪些接口暴露给Swagger展示呢?我们可以使用注解,也可以指定扫描哪个包中的注解,也可以扫描所有的api。最后还设置了一个请求前缀pathMapping,在 yml 中配置为 /dev-api,意思是说请求项目的/abc接口实际上需要请求/dev-api/abc,这一点需要特别注意!

同时在类中可以看到Swagger文档的标题、描述设置等。

那么我们如何使用Swagger呢?我们想要自己的API展示到Swagger里面该怎么做呢?很简单,我们在自己包也引入关于Swagger的依赖

xml 复制代码
<!-- swagger3-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
</dependency>

<!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
<dependency>
    <groupId>io.swagger</groupId>
    <artifactId>swagger-models</artifactId>
    <version>1.6.2</version>
</dependency>

<!--Swagger增强-->
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-ui</artifactId>
</dependency>

然后在自己的方法上加上注解@ApiOperation("xxxx")

然后就可以在Swagger中看到啦!

9、XSS脚本攻击

以若依系统为例,我们在修改用户的时候,企图在备注中植入 JavaScript 脚本,然后将会被ry的XSS脚本攻击拦截:

若依如何抵抗XSS攻击呢?

当xss.enabled为true的时候,会开启XSS攻击防护,

首先new一个过滤注册器FilterRegistrationBean,当请求来的时候会触发这个过滤器,注册器里面设置拦截路径,拦截路径也就是我们在yml中配置的/system/*,/monitor/*,/tool/*,给过滤器设置一个名字叫xssFilter,设置过滤器的执行顺序,优先级为最高,最终将yml中排除的链接初始化进过滤器,也就是排除的链接/system/notice不需要开启XSS防护。

  • 过滤器可以根据DispatcherType的类型选择是否执行
  • FilterRegistrationBean,这个类用于把我们的过滤器注册到servlet容器中
  • registration.addUrlPatterns用于设置拦截的URL
  • registration.setOrder用于设置优先级,数字越小优先级越高
  • registration.setInitParameters用于设置初始化参数,例如可以设置excludes(排除的URL,然而逻辑需要自己实现,默认不支持),接受参数类型为Map<String, String>

接着我们看看XSS过滤器到底干了什么

我们这里直接说结论:

  • init方法主要是为了处理排除的URL
  • handleExcludeURL方法用于看看URL是不是排除的URL,如果是GET和DELETE则直接不过滤;另外,如果所请求的URL在排除URL也直接不过滤
  • XSS过滤原理是重新包装了Request,重写了getParameterValues方法和getInputStream方法。
  • getParameterValues方法用于获取参数值的数组,如果存在值,那么就进行HTML过滤,并且排除前后空格。
  • getInputStream方法用于处理JSON类型,如果发现是JSON,那么会先HTML过滤,然后重新包装成流进行返回。

10、防止重复提交过滤

我们讲述过前端的防止重复提交,如果请求数据请求URL最近一次请求一致,并且请求间隔小于1000ms,就进行请求拦截,直接拒绝当前请求。

从我上面的描述,发现了一个bug,总有手快的人,喜欢点A按钮,然后立刻点B按钮,然后又立刻点A按钮。那么对于A按钮是重复提交了,但是又不满足前端判断重复请求的条件,于是重复请求进入了后端,这时候就需要后端再次校验,是不是重复请求。

后端过滤重复请求也是通过过滤器,并且和XSS过滤惊人的相似。

首先new一个注册器,然后注册一个重复过滤器,拦截所有路径,优先级为最低,注入到Spring容器中。

相关推荐
lsjweiyi15 小时前
极简AI工具箱网站开源啦!
opencv·开源·微信支付·支付宝支付·百度ai·极简ai工具箱·ai图像处理
开源社17 小时前
一场开源视角的AI会议即将在南京举办
人工智能·开源
FreeIPCC17 小时前
谈一下开源生态对 AI人工智能大模型的促进作用
大数据·人工智能·机器人·开源
海害嗨17 小时前
阿里巴巴官方「SpringCloudAlibaba全彩学习手册」限时开源!
学习·开源
生命是有光的17 小时前
【开源风云】从若依系列脚手架汲取编程之道(八)
开源
HuggingFace19 小时前
Halo 正式开源: 使用可穿戴设备进行开源健康追踪
开源·健康追踪
时光追逐者1 天前
.NET 9 中 LINQ 新增功能实操
开发语言·开源·c#·.net·.netcore·linq·微软技术
檀越剑指大厂1 天前
Linux本地部署开源项目OpenHands基于AI的软件开发代理平台及公网访问
linux·人工智能·开源
胜天半子_王二_王半仙2 天前
c++源码阅读__ThreadPool__正文阅读
开发语言·c++·开源
FIT2CLOUD飞致云2 天前
案例研究|阿特斯的JumpServer分布式部署和多组织管理实践
运维·开源·堡垒机·jumpserver