Springboot管理系统数据权限过滤(二)——SQL拦截器
- 若依(ruoyi)
- 时间:2024-04-29 22:31
- 1082人已阅读
🔔🔔🔔好消息!好消息!🔔🔔🔔
有需要的朋友👉:联系凯哥
上一节Springboot管理系统数据权限过滤——ruoyi实现方案对数据权限实现方案有了认识,本文将进一步优化权限过滤方案,实现对业务代码零入侵。
回顾上一章中权限方案:
主要是通过注解拦截,拼接好权限脚本后,放到对象变量里面,然后在SQL中拼接该变量;使业务代码被入侵了。
为了实现对业务零入侵,实则是在SQL编写的时候,希望通过框架实现权限脚本的自动拼接,而非人为添加。
本文权限控制需要达到的效果:
1.还是对组织进行权限控制;
2.去掉编写sql时拼接权限过滤参数;使权限代码0侵入;
步骤:
1. 搭建springboot框架,完成mybatisplus集成和swagger集成
pom.xml文件
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> </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.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 实现对数据库连接池的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <!-- 本示例,我们使用 MySQL --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- 实现对 MyBatis 的自动化配置 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- 引入 Swagger 依赖 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!-- 引入 Swagger UI 依赖,以实现 API 接口的 UI 界面 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> </dependencies>
application.yaml
spring: # datasource 数据源配置内容 datasource: url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 # mybatis-plus 配置内容 mybatis-plus: configuration: map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 global-config: db-config: id-type: auto # ID 主键自增 logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.luo.chengrui.labs.lab02.dataobject # 配置数据库实体包路径 # logging logging: level: # dao 开启 debug 模式 mybatis 输入 sql com: luo: chengrui: labs: debug
UserDao.java
package com.luo.chengrui.labs.lab02.dataobject; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; import java.util.Date; /** * @author * @version 1.0.0 * @description * @createTime 2023/07/20 */ @Data @Accessors(chain = true) @TableName(value = "users") public class UserDO { /** * 用户编号 */ private Long id; /** * 账号 */ private String username; /** * 密码(明文) * <p> * ps:生产环境下,千万不要明文噢 */ private String password; /** * 创建时间 */ private Date createTime; }
UserMapper.java
package com.luo.chengrui.labs.lab02.mapper; import com.luo.chengrui.labs.lab02.dataobject.UserDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface UserMapper { UserDO selectById(@Param("id") Integer id); List<UserDO> selectList(); }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.luo.chengrui.labs.lab02.mapper.UserMapper"> <sql id="FIELDS"> id , username </sql> <select id="selectById" parameterType="Integer" resultType="UserDO"> SELECT <include refid="FIELDS"/> FROM users WHERE id = #{id} </select> <select id="selectList" resultType="UserDo"> SELECT <include refid="FIELDS"/> FROM users </select> </mapper>
UserService.java
package com.luo.chengrui.labs.lab02.service; import com.luo.chengrui.labs.lab02.annotation.DataScope; import com.luo.chengrui.labs.lab02.dataobject.UserDO; import com.luo.chengrui.labs.lab02.mapper.UserMapper; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @author * @version 1.0.0 * @description * @createTime 2023/07/21 */ @Service public class UserService { @Autowired private UserMapper userMapper; private UserService self() { return (UserService) AopContext.currentProxy(); } /** * 方法未使用 @Transactional 注解,不会开启事务。 * 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的 @DS 注解,找到对应的数据源,执行操作。 * 这样一看,在未开启事务的情况下,我们已经能够自由的使用多数据源落。 */ public void method() { // 查询订单 UserDO user = userMapper.selectById(1); System.out.println(user); } @DataScope public void method01() { // 查询订单 UserDO user = userMapper.selectById(1); System.out.println(user); } @DataScope public List<UserDO> selectList() { return userMapper.selectList(); } }
UserController.java
package com.luo.chengrui.labs.lab02.controller; import com.luo.chengrui.labs.lab02.dataobject.UserDO; import com.luo.chengrui.labs.lab02.service.UserService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author * @version 1.0.0 * @description * @createTime 2023/07/17 */ @RestController @RequestMapping("/users") @Api(tags = "用户 API 接口") public class UserController { @Autowired UserService userService; @GetMapping("/list") @ApiOperation(value = "查询用户列表", notes = "目前仅仅是作为测试,所以返回用户全列表") public List<UserDO> list() { // 查询列表 List<UserDO> result = userService.selectList(); // 返回列表 return result; } }
SwaggerConfiguration.java
package com.luo.chengrui.labs.lab02.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * 访问地址:/swagger-ui.html * @author * @version 1.0.0 * @description * @createTime 2023/07/17 */ @Configuration @EnableSwagger2 public class SwaggerConfiguration { @Bean public Docket createRestApi() { // 创建 Docket 对象 return new Docket(DocumentationType.SWAGGER_2) // 文档类型,使用 Swagger2 .apiInfo(this.apiInfo()) // 设置 API 信息 // 扫描 Controller 包路径,获得 API 接口 .select() .apis(RequestHandlerSelectors.basePackage("com.luo.chengrui.labs.lab02.controller")) .paths(PathSelectors.any()) // 构建出 Docket 对象 .build(); } /** * 创建 API 信息 */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("测试接口文档示例") .description("我是一段描述") .version("1.0.0") // 版本号 .contact(new Contact("chengrui", "http://www.chengrui.cn", "chengrui@gmail.com")) // 联系人 .build(); } }
Lab0201Application.java
package com.luo.chengrui.labs.lab02; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author * @version 1.0.0 * @description * @createTime 2023/07/21 */ @SpringBootApplication @MapperScan(basePackages = "com.luo.chengrui.labs.lab02.mapper") public class Lab0201Application { public static void main(String[] args) { SpringApplication.run(Lab0201Application.class, args); } }
到此完成框架搭建,访问:http://localhost:8080/swagger-ui.html,可看到以下页面即为成功。
2. 配置sql拦截器
创建 MybatisDatabaseInterceptor .java 类,类中大部分代码是对sql的解析,对表名的解析,对where语句的解析,真正需要关注的逻辑只是少部分,本部分代码里暂未添加对权限的控制,在下一文章中添加。
该类继承JsqlParserSupport ,同时实现InnerInterceptor接口
JsqlParserSupport 用于解析sql语句,可以对sql进行改造;方法有:processSelect、processUpdate、processDelete等;
InnerInterceptor 在执行sql语句之前的拦截器,方法有:beforeQuery、beforeUpdate、beforePrepare等,执行这些方法中可调用JsqlParserSupport 类中的方法对sql进行解析。
通过实现以上5个方法即可对sql进行改造。
package com.luo.chengrui.labs.lab02.datapermission; 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.RequiredArgsConstructor; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.update.Update; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; /** * SQL拦截器, * 主要的 SQL 重写方法, * 主要是在执行SQL前拦截器,在执行之前可重写SQL */ @RequiredArgsConstructor public class MybatisDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { Logger logger = LoggerFactory.getLogger(MybatisDatabaseInterceptor.class); @Override // SELECT 场景 public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { logger.debug("MybatisDatabaseInterceptor .... beforeQuery"); if (!InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) { PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); //简单实现一个sql改造例子 mpBs.sql(String.format(" select * from (%s) t limit 0,1", mpBs.sql())); } } @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限) public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { logger.debug("MybatisDatabaseInterceptor .... beforePrepare"); PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); MappedStatement ms = mpSh.mappedStatement(); SqlCommandType sct = ms.getSqlCommandType(); if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) { return; } PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); mpBs.sql(this.parserMulti(mpBs.sql(), ms.getId())); } } @Override protected void processSelect(Select select, int index, String sql, Object obj) { logger.debug("MybatisDatabaseInterceptor .... processSelect"); // mpBs.sql(String.format(" select * from (%s) t limit 0,1", sql); } /** * update 语句处理 */ @Override protected void processUpdate(Update update, int index, String sql, Object obj) { logger.debug("MybatisDatabaseInterceptor .... processUpdate"); } /** * delete 语句处理 */ @Override protected void processDelete(Delete delete, int index, String sql, Object obj) { logger.debug("MybatisDatabaseInterceptor .... processDelete"); } }
将拦截器注入Mybatis拦截器队列
@Configuration public class DataPermissionConfiguration { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(List<DataPermissionRule> dataPermissionRule) { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); MybatisDatabaseInterceptor mybatisDatabaseInterceptor = new MybatisDatabaseInterceptor(); List<InnerInterceptor> inners = new ArrayList<>(mybatisPlusInterceptor.getInterceptors()); inners.add(0, mybatisDatabaseInterceptor); mybatisPlusInterceptor.setInterceptors(inners); return mybatisPlusInterceptor; } }
完成上面配置后,每执行一个SQL都会被我们定义的拦截器拦截了,执行sql查询,可以看到已经过了拦截器。