MyBatis Plus 拦截器实现数据权限控制

news2025/1/20 10:58:56

一、介绍

上篇文章介绍的MyBatis Plus 插件实际上就是用拦截器实现的,MyBatis Plus拦截器对MyBatis的拦截器进行了包装处理,操作起来更加方便

二、自定义拦截器

2.1、InnerInterceptor

MyBatis Plus提供的InnerInterceptor接口提供了如下方法,主要包括:在查询之前执行,在更新之前执行,在SQL准备之前执行
在这里插入图片描述

2.2、编写简易拦截器

package com.xx.config;

import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.springframework.stereotype.Component;

import java.sql.Connection;

/**
 * @author aqi
 * @date 2023/5/17 15:07
 */
@Slf4j
@Component
public class TestInterceptor extends JsqlParserSupport implements InnerInterceptor {

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        // 这里固定这么写就可以了
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
            return;
        }
        PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
        mpBs.sql(parserMulti(mpBs.sql(), null));
    }

    /**
     * 该方法由JsqlParserSupport提供,主要用于通过API的方式操作SQL
     * 思路:通过API构建出新的条件,并将新的条件和之前的条件拼接在一起
     */
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        // 解析SQL
        SelectBody selectBody = select.getSelectBody();
        PlainSelect plainSelect = (PlainSelect) selectBody;

        // 构建eq对象
        EqualsTo equalsTo = new EqualsTo(new Column("name"), new StringValue("tom"));
        // 将原来的条件和新构建的条件合在一起
        AndExpression andExpression = new AndExpression(plainSelect.getWhere(), equalsTo);
        // 重新封装where条件
        plainSelect.setWhere(andExpression);
    }


    @Override
    protected void processInsert(Insert insert, int index, String sql, Object obj) {
        insert.getColumns().add(new Column("name"));
        ((ExpressionList) insert.getItemsList()).getExpressions().add(new StringValue("tom"));
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        update.addUpdateSet(new Column("name"), new StringValue("tom"));
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        // 删除新增条件和查询一样,不做演示
    }
}


2.3、将拦截器添加到MyBatis Plus拦截器中

package com.xx.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author aqi
 * @date 2023/5/15 14:05
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化Mybatis Plus拦截器
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TestInterceptor());
        return interceptor;
    }
}

2.4、编写测试用例

@Test
    void save() {
        AirlinesInfo airlinesInfo = new AirlinesInfo();
        airlinesInfo.setInfo("remark");
        airlinesInfoService.save(airlinesInfo);
    }

    @Test
    void update() {
        AirlinesInfo airlinesInfo = new AirlinesInfo();
        airlinesInfo.setId(1L);
        airlinesInfo.setInfo("remark, remark");
        airlinesInfoService.updateById(airlinesInfo);
    }

    @Test
    void select() {
        airlinesInfoService.list();
    }

2.5、执行结果

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三、自定义拦截器实现数据权限控制

3.1、编写拦截器

package com.xx.config;

import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.xx.entity.Permission;
import com.xx.utils.ExpressionUtils;
import com.xx.utils.UserUtils;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.springframework.stereotype.Component;

import java.sql.Connection;

/**
 * @author xiaxing
 */
@Slf4j
@Component
public class DataScopeInterceptor extends JsqlParserSupport implements InnerInterceptor {

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        log.info("[DataScopeInterceptor]beforePrepare...");
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || sct == SqlCommandType.SELECT) {
            if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
                return;
            }
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            mpBs.sql(parserMulti(mpBs.sql(), null));
        }
    }

    /**
     * 查询
     */
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        PlainSelect plainSelect = (PlainSelect) selectBody;

        // 获取表名/别名(如果是关联查询是取第一个join左侧的表名/别名)
        String tableName = ExpressionUtils.getTableName(plainSelect);

        // 构建用户权限控制条件
        Expression userPermissionExpression = this.buildUserPermissionSql(tableName);
        if (null != userPermissionExpression) {
            // 将sql原本就有得where条件和新构建出来的条件拼接起来
            plainSelect.setWhere(ExpressionUtils.appendExpression(plainSelect.getWhere(), userPermissionExpression));
        }

    }

    /**
     * 构建用户权限控制条件
     * @param tableName 表名/别名(join查询左侧表名)
     */
    private Expression buildUserPermissionSql(String tableName) {
        // 获取当前用户信息(这里的数据都是模拟的,实际上可能得从缓存或者session中获取)
        Permission permission = UserUtils.getUserPermission();
        return null != permission ? ExpressionUtils.buildInSql(tableName + "."  + permission.getField(), permission.getValue()) : null;
    }

}

3.2、编写构建SQL工具类

package com.xx.utils;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.PlainSelect;

import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author aqi
 * @date 2023/5/17 10:16
 * @describe JSqlParser工具类,用于通过API的方式操作SQL语句
 */
public class ExpressionUtils {

    /**
     * 构建in sql
     * @param columnName 字段名称
     * @param params 字段值
     * @return InExpression
     */
    public static InExpression buildInSql(String columnName, Set<String> params) {
        // 把集合转变为JSQLParser需要的元素列表
        ItemsList itemsList = new ExpressionList(params.stream().map(StringValue::new).collect(Collectors.toList()));
        // 创建IN表达式对象,传入列名及IN范围列表
        return new InExpression(new Column(columnName), itemsList);
    }

    /**
     * 构建eq sql
     * @param columnName 字段名称
     * @param value 字段值
     * @return EqualsTo
     */
    public static EqualsTo buildEq(String columnName, String value) {
       return new EqualsTo(new Column(columnName), new StringValue(value));
    }

    /**
     * 获取表名/别名
     * @param plainSelect plainSelect
     * @return 表名/别名
     */
    public static String getTableName(PlainSelect plainSelect) {
        // 获取别名
        Table table= (Table) plainSelect.getFromItem();
        Alias alias = table.getAlias();
        return null == alias ? table.getName() : alias.getName();
    }

    /**
     * 将2个where条件拼接到一起
     * @param where 条件
     * @param appendExpression 待拼接条件
     * @return Expression
     */
    public static Expression appendExpression(Expression where, Expression appendExpression) {
        return null == where ? appendExpression : new AndExpression(where, appendExpression);
    }
}

3.3、模拟用户信息工具类

package com.xx.utils;

import com.xx.config.Globle;
import com.xx.entity.Permission;
import com.xx.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

/**
 * @author aqi
 * @date 2023/5/17 14:20
 */
@Slf4j
public class UserUtils {

    public static User currentUser;

    static {
        // 构建测试数据
        List<Permission> permissionList = new ArrayList<>();
        // demo/test接口权限
        Permission permission = new Permission();
        permission.setField("id");
        permission.setUri("/demo/test");
        Set<String> set = new HashSet<>();
        set.add("1");
        set.add("2");
        set.add("3");
        permission.setValue(set);
        permissionList.add(permission);


        // demo/test1接口权限
        Permission permission1 = new Permission();
        permission1.setField("id");
        permission1.setUri("/demo/test1");
        Set<String> set1 = new HashSet<>();
        set1.add("4");
        set1.add("5");
        set1.add("6");
        permission1.setValue(set1);
        permissionList.add(permission1);


        User user = new User();
        user.setPermissionList(permissionList);
        user.setTenantId("1");
        currentUser = user;
    }

    public static Permission getUserPermission() {
        User currentUser = Globle.currentUser;
        String uri = UserUtils.getUri();
        List<Permission> permissionList = currentUser.getPermissionList();
        return permissionList.stream().filter(e -> Objects.equals(e.getUri(), uri)).findFirst().orElse(null);
    }

    /**
     * 获取本次请求的uri
     * @return uri
     */
    private static String getUri() {
        // 获取此次请求的uri
        String uri = "";
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (null != requestAttributes) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            uri = request.getRequestURI();
        }
        log.info("[DataScopeInterceptor]此次请求uri:{}", uri);
        return uri;
    }
}


3.4、将拦截器添加到MyBatis Plus蓝机器中

package com.xx.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author aqi
 * @date 2023/5/15 14:05
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化Mybatis Plus拦截器
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new DataScopeInterceptor());
        return interceptor;
    }
}

3.5、测试

package com.xx.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.pagehelper.PageHelper;
import com.xx.entity.AirlinesInfo;
import com.xx.service.AirlinesInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author aqi
 * @date 2023/5/18 11:01
 */
@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    @Resource
    private AirlinesInfoService airlinesInfoService;

    @GetMapping("/test")
    public void test() {
        log.info("进入test接口,测试权限控制在基础的sql语句是否能生效");
        airlinesInfoService.list();
        // 执行结果:(SELECT * FROM airlines_info WHERE state = 0 AND airlines_info.id IN ('1', '2', '3'))
    }

    @GetMapping("/test1")
    public void test1() {
        log.info("进入test1接口,测试权限控制在使用MyBatis Plus 的分页插件之后能否生效");
        Page<AirlinesInfo> page = new Page<>(1, 5);
        airlinesInfoService.page(page, new QueryWrapper<AirlinesInfo>().eq("name", "tom"));
        // 执行结果:(SELECT * FROM airlines_info WHERE state = 0 AND (name = ?) AND airlines_info.id IN ('4', '5', '6') LIMIT ?)
    }

    @GetMapping("/test2")
    public void test2() {
        log.info("进入test2接口,测试权限控制在使用PageHelper之后能否生效");
        PageHelper.startPage(1, 5);
        airlinesInfoService.list(new LambdaQueryWrapper<AirlinesInfo>().eq(AirlinesInfo::getName, "tom"));
        // 执行结果:(SELECT * FROM airlines_info WHERE state = 0 AND (name = ?) AND airlines_info.id IN ('7', '8', '9') LIMIT ?)
    }

    @GetMapping("/test3")
    public void test3() {
        log.info("进入test3接口,测试权限控制在使用自定义复杂关联查询之后能否生效");
        airlinesInfoService.innerSql();
        // 原始SQL:(select * from airlines_info t1 INNER JOIN t_config on t1.id = t_config.id where t1.name = 'tom' and t_config.name = 'jack' limit 5)
        // 执行结果:(SELECT * FROM airlines_info t1 INNER JOIN t_config ON t1.id = t_config.id WHERE t1.name = 'tom' AND t_config.name = 'jack' AND t1.id IN ('11', '12', '10') LIMIT 5)
    }

    @GetMapping("/test4")
    public void test4() {
        log.info("进入test4接口,测试该接口没有设计权限限制是否可以不生效");
        airlinesInfoService.list();
        // 执行结果:(SELECT * FROM airlines_info WHERE state = 0)
    }


}

四、结论

通过测试可以看出不论在什么情况下都可以正常的对权限进行控制

注意:上面部分代码使用的是MyBatis Plus 3.5.3版本,并且使用的JSqlParser部分API已经不推荐使用,但是我没有找到最新的API应该怎么写

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

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

相关文章

SpringCloud 集成 Eureka Server

SpringCloud 集成 Eureka Server 1 pom.xml2 application.yml3 appliction.java4 启动 Eureka的优点&#xff1a; 简单易用&#xff1a;Eureka设计简单&#xff0c;容易上手和部署。 高可用性&#xff1a;Eureka支持高可用性配置&#xff0c;通过使用多个Eureka服务器实例来提…

IEEE Robotics and Automation Letters(RA-L)与ICRA投稿

一 总体感受 RAL不愧未短平快的论文&#xff0c;从接收论文、送审和复审都相当快&#xff0c;我的两个多月出最终接收结果&#xff0c;期刊官网规定6个月内出最终结果。作为现在IEEE主推的短文&#xff0c;限制在8页以内&#xff0c;在6页以上时超页费为175刀/页&#xff0c;目…

平台使用篇 | 批处理(bat)脚本使用教程(二)

导读 BAT脚本提供了一种快速且有效的自动化方式&#xff0c;使用户能够更轻松地处理大量的任务&#xff0c;并且可以根据需要自由地调整和修改脚本。本讲简要介绍了批处理技术及其常用命令。本篇教程主要对批处理技术的语言特点和编程思路进行重点讲解。 RflySim平台更多学习资…

Flutter-Drawer使用

drawer Drawer是Android开发中Material风格常用的设计&#xff0c;就是我们常说的“抽屉”效果&#xff0c;一个从侧边栏拉出来的导航面板。 在Flutter使用Material风格&#xff0c;最为常用的组件之一就是Scaffold了&#xff1b;Scaffold的drawer属性是一个Widget类型的组件&…

Red Hat重置root密码

目录 前言 1、使用rd.break参数重置root密码 2、使用安装盘重置root密码 前言 我们有时会忘记linux系统的root密码&#xff0c;有的不会重置密码只能重置系统了&#xff0c;下面介绍两种重置root密码的方法 1、使用rd.break参数重置root密码 1、启动系统&#xff0c;并在…

将有序数组转换为二叉树

md这个破CSDN模板怎么没了&#xff0c;编辑器也死难用&#xff0c;气死 1、题目 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不…

如何免费使用ChatGPT,提高开发效率?以开发者的角度ChatGPT能做什么?

一、如何免费使用ChatGPT 1&#xff09;登录openai官网https://openai.com/&#xff0c;注册账号后获取免费体验&#xff1b;但目前已经很难成功注册openai账号了&#xff0c;需要魔法上网的同时代理不能使用香港、俄罗斯等地区&#xff0c;需要国外邮箱和国外手机号&#xff…

企业用友NC软件被locked勒索病毒攻击,如何恢复nchome配置文件

近日&#xff0c;用友NC系统遭受了一次严重的勒索病毒攻击&#xff0c;导致许多企业的数据和配置文件被锁定。其中&#xff0c;NC Home配置文件也受到了影响&#xff0c;给企业带来了不小的损失。那么&#xff0c;在这种情况下&#xff0c;如何恢复NC Home配置文件呢&#xff1…

一个动作,直接盘活死仓库!实现效益增长200%!(附完整模板)

有人说&#xff1a;看一家工厂管理到不到位&#xff0c;看下他的仓库就知道了。 仓库作为企业存储和保管物料的重要场所&#xff0c;其管理的好坏直接影响着企业的生产与销售环节&#xff01; 我拜访过很多制造型企业&#xff0c;他们的仓库或多或少都存在以下问题—— 物料…

Hadoop的HDFS文件系统

Hadoop的HDFS文件系统 概述 Hadoop的HDFS文件系统是一种分布式文件系统&#xff0c;hadoop的核心组件之一。它的设计目标是能够在普通硬件上运行&#xff0c;并且能够处理大量的数据。HDFS采用了主从&#xff08;Master/Slave&#xff09;架构&#xff0c;其中有一个NameNode…

Adobe打印转PDF字体出错问题解决方案

错误现象 通过adobe pdf打印转换pdf时&#xff0c;不能够转换成功&#xff0c;只能弹出一个**.txt 打印——选择Adobe pdf——打印&#xff0c;txt中的内容如下&#xff1a; %%[ ProductName: Distiller ]%% %%[ Error: TT8E96441DtCID cannot be embedded because of licensi…

2023年美国大学生数学建模竞赛D题联合国可持续发展目标的优先次序解题全过程文档及程序

2023年美国大学生数学建模竞赛 D题 联合国可持续发展目标的优先次序 原题再现&#xff1a; 背景   联合国(UN)制定了17 项可持续发展目标(SDGs)。实现这些目标将最终改善世界各地许多人的 生活。这些目标不是相互独立的。因此&#xff0c;在某些目标上的积极收获通常会对其…

FM33A048B CRC

概述 循环冗余校验(Cyclic Redundancy Check&#xff0c;CRC)是最为常用的计算机和仪表数据通信的校验方法&#xff0c;FM33A048B中CRC计算单元为完全独立模块&#xff0c;通过软件控制可进行7816、I2C、UART和SPI模块有串行数据流接口的收发CRC计算和校验&#xff0c;也可进行…

二、搭建Kubernetes集群---2.1 搭建Kubernetes环境平台规划和部署方式介绍

二、搭建Kubernetes集群 2.1 搭建Kubernetes环境平台规划和部署方式介绍 2.1.1 搭建Kubernetes环境平台规划 如何安排Master、node。我们已经知道Kubernetes架构的组件共有两大部分&#xff0c;Master和node&#xff0c;因此这里的搭建我们可以分为两个部分&#xff0c;单Ma…

Vue3 + vite npm run build 后 html文件的srcipt标签不加type=module属性的js文件没有被打包

引言 &#xff1a;最近开发一个公司的官网项目&#xff0c;由于公司没有 UE&#xff0c; 领导就直接找了一个JQuery bootstrap 的项目模板要求在最快的时间里面把这个项目放到Vue框架里面。这个项目模板里面各种动画、图表都是引入的JQery插件&#xff0c;这就导致了&#xff…

vue通过缓存请求数据提高首界面展示效率

我在组件中编写了这样一段代码 <template><div class "appp"><button click "getUser">请求数据</button></div> </template><script> import axios from "axios"; export default {data() {retur…

SQL Tips汇集及常见问题

SQL Tips汇集及常见问题 表与数据 -- 创建并初始化部门表DROP TABLE IF EXISTS dept; CREATE TABLE dept (deptno int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 部门编号,dname varchar(15) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMM…

蓝桥杯青少组python:第十三届省赛第二场

选择题 1、十进制 55 55 55转换为十六进制是() A、 1101111 1101111 1101111 B、 313 313 313 C、 37 37 37 D、 67 67 67 2、下列关于函数的说法正确是&#xff08;&#xff09; A、函数的定义必须的程序的开头 B、函数定义后&#xff0c;其中的程序就可以自动运行 C、函数定…

如何让技术架构师具有预知未来业务发展的能力

大家好&#xff0c;今天我们来分享业务架构&#xff0c;但是我们并不是以产品经理角度讲述一个业务架构是什么以及如何做&#xff1f;而是以一个技术架构师的角度&#xff0c;讲述如何承接业务架构或在没有业务架构的时候&#xff0c;如何判断业务变化趋势而对系统架构提前做出…

【Linux】2.4 使用 git 命令行

git git —— 一个版本管理的工具。 &#xff08;gitee/github 底层都是git&#xff0c;只是不同的网页化的结果&#xff09; 有否&#xff1f;&#xff1a;查看是否有git——git --version [RoundBottleVM-12-2-centos ~]$ git --version git version 1.8.3.1安装&#xff…