java(springboot) mybatis 数据权限详细实现(图文)
噼里啪啦
来吧,整起,又一新功能,通用数据权限,注意是通用,通用的东西,反正挺烦的,(每天喝点茶,刷点文章,发表下博文,改下bug,写点crud多舒服的,啊哈哈,噗)
我还是第一次搞这玩意儿,因为之前做细节的数据权限都是直接写在代码里面的
好,开整,这篇文章我会写得详细一点,并且提供开源源码,全靠我自己设计,编码,一步步的敲出来的,很少的地方借鉴到了别人的东西,切看切珍惜,动动你的小手点个赞,点个收藏吧,写文章还真的挺累的。
一、啥子是数据权限?
嗯,数据权限?有些朋友可能会问了,“嗯,数据还有权限?”
没错,简单来讲:数据权限无非就是某人只能看到某些数据。
举个例子:张三登录了A系统,那么根据系统查询出来的张三所拥有的权限,比如张三有一个A部门的数据权限,
那么,在A系统中,张三只能看到A部门相关的数据。
二、做这个功能的思路是啷个一个样子的?
那好了,啷个才能实现这个功能呢?
别慌,我们先回忆一下我们在不做通用的情况下是怎么做数据权限的呢?
1.没有做通用数据权限
比如张三有A部门的权限:
String deptId = servie.getDeptByUser("张三");
//在xml里
select * from test_table
where dept_id = #{deptId}
如上面的代码所示,我们一般是通过直接写sql带条件的方式实现的,写起来非常的方便,但是代码多了就求了,万一有1W个mapper都需要这样做,那写到吐,好吧,给你搞个通用的。
2.实现的原理和思路
好,上面已经说到了痛点,那么我们只需要拦截住我们需要的sql,能根据获取到的用户数据权限,动态的拼接出我们想要的sql,再给他装回去,那这个问题就能解决了。
ok,按照这个步骤:用户登录→登录验证通过后获取到用户的所有权限信息→放入到redis中→做数据查询时拦截对应的sql→详细封装处理→执行新sql返回权限后数据
二、开整!
1.新建一个项目并添加依赖和工具类包
初始化
选择spring初始化,下一步下一步就可以了,名字随便取,先不用选依赖。
建完了在详细下面找到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>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.kbplus.demo.data</groupId><artifactId>permission</artifactId><version>0.0.1-SNAPSHOT</version><name>permission</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</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-web</artifactId></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.12</version></dependency><!-- 数据库--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.24</version></dependency><!-- mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><!--mybaits-plus生成代码的依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.0</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency><!-- 工具包 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.16</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.8</version><scope>provided</scope></dependency><!-- swagger --><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><!--fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.68</version></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency><!-- Token生成与解析--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
建库建表
添加数据库,并导入提供好的sql
名字随便取,字符集和排序规则选择我选择的就ok,比较通用的utf规则和排序规则,
简单介绍一下,utf8mb4是mysql的一种拓展字符集,可以存储一些特殊字符,utf8mb4_general_ci是兼容大多主流语言并同时比较高效的排序规则,如果你的项目有使用到少见的语言,比如俄语,可以使用utf8mb4_unicode_ci来提高精准性
导入完sql后,大概是这么一个结构。
添加公共配置,工具类
接着添加项目所需类包(源码里会提供),
大致结构如下:
2.书写简单的登录实现登录验证并保存用户信息
核心代码如下:
/*** 登录验证** @param username 用户名* @param password 密码* @return 结果*/@Overridepublic String login(String username, String password){User user1 = baseMapper.selectOne(new QueryWrapper<User>().eq("username", username));if(user1==null){throw new CustomException("用户不存在");}if(!password.equals(user1.getPassword())){throw new CustomException("密码不正确");}//这里使用mybaitis特性collection之类的好像会更快,各位有兴趣可以尝试Set<Role> allRoleByUserId = roleMapper.getAllRoleByUserId(user1.getId());UserDept userDept = userDeptMapper.selectOne(new QueryWrapper<UserDept>().eq("user_id", user1.getId()));Set<String> allPositionByUserId = positionMapper.getAllPositionByUserId(user1.getId());List<String> deptChildren = departmentService.getDeptChildren(userDept.getDeptId());Set<String> allDataPermissionByUserId = dataPermissionMapper.getAllDataPermissionByUserId(user1.getId());LoginUser loginUser = new LoginUser();loginUser.setUser(user1);loginUser.setTenantId(user1.getTenantId());loginUser.setRoles(allRoleByUserId);loginUser.setUserId(user1.getId());loginUser.setDeptId(userDept.getDeptId());loginUser.setPostIds(allPositionByUserId);loginUser.setDeptChildren(deptChildren);loginUser.setDataPermissions(allDataPermissionByUserId);String token = UUID.randomUUID().toString();loginUser.setToken(token);//存入redistokenService.setLoginUser(loginUser);// 生成tokenreturn tokenService.createToken(loginUser);}
在创建token的时候有这一步,以便可以通过token直接拿到想要的信息,不用去redis再查。
/*** 创建令牌* * @param loginUser 用户信息* @return 令牌*/public String createToken(LoginUser loginUser){Map<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, loginUser.getToken());claims.put("tenantId",loginUser.getTenantId());claims.put("id",loginUser.getUserId());claims.put("name",loginUser.getUser().getName());claims.put("postIds",loginUser.getPostIds());claims.put("organizationId",loginUser.getDeptId());return createToken(claims);}
3.添加基本数据并测试效果
1.添加用户数据,角色数据,权限数据等并关联:
大家可以使用sql一键导入,详情大家可以参考我的权限认证文章:待完善
我们使用user1账号登录后创建2条测试数据,再用user2登录创建2条测试数据,步骤如下:
2.使用user1模拟登录:
3.拿到返回的token去生成测试数据:
使用user2登录做同样的操作,让后可以看到表里有6条数据,null值的是我之前添加的不用在意~~,主要可以看到有4条是部门1的,2条是部门2的 我设置的权限分别是user1是部门1,user2是部门2,且部门2是部门1的子部门
4.测试效果:
使用user1成功返回6条数据,因为我现在给他的数据权限是拥有部门及子部门,所有能查到所有
使用user2登录,user2和user1拥有一样的角色,即拥有一样的权限,请求接口,发现只返回了2条数据,且都是dept2的,因为部门2是部门1的子集
目前系统支持以下类型的数据权限
ALL("1","拥有所有数据权限"),NONE("2","未拥有数据权限"),DEPT("3","拥有部门权限"),DEPT_CHILDREN("4","拥有部门权限及子权限"),POST("5","拥有职位权限"),
// POST_CHILDREN("6","拥有职位权限及子权限"),OWN("7","拥有自身权限"),
大家可以根据自己的需要进行测试
4.核心类解析
新建拦截机继承 InnerInterceptor (mybatis-plus的一个拦截器)
public class DataPermissionInterceptor implements InnerInterceptor
把拦截器注入进配置,这里注意添加拦截器的编写顺序,会影响到拦截器执行的先后顺序
package com.kbplus.demo.data.permission.config;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisPlusConfig {@Beanpublic DataPermissionInterceptor dataPermissionInterceptor() {return new DataPermissionInterceptor();}@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();paginationInnerInterceptor.setDbType(DbType.MYSQL);interceptor.addInnerInterceptor(dataPermissionInterceptor());interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}
核心拦截类:大致原理是先做有效性判断,包括是否属于管理员等,这里只拦截需要分页的查询,然后根据权限匹配,生成相对应的代码
package com.kbplus.demo.data.permission.config;import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.kbplus.demo.data.permission.entity.LoginUser;
import com.kbplus.demo.data.permission.entity.Role;
import com.kbplus.demo.data.permission.mapper.CommonMapper;
import com.kbplus.demo.data.permission.utils.SpringUtils;
import com.kbplus.demo.data.permission.utils.TokenService;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;/*** @author kbplus* @version v1.0* @date 2022-05-13 17:49:18*/
public class DataPermissionInterceptor implements InnerInterceptor {@Autowiredprivate TokenService tokenService;@Autowiredprivate HttpServletRequest httpServletRequest;@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql){String firstSql = boundSql.getSql();Field field = null;try {Select statement = (Select) CCJSqlParserUtil.parse(boundSql.getSql());if(!ifPage(parameter)){return;}LoginUser loginUser = tokenService.getLoginUser(httpServletRequest);if(loginUser==null){return;}Set<String> dataPermissions = loginUser.getDataPermissions();Set<Role> roles = loginUser.getRoles();for (Role role : roles) {if("admin".equals(role.getCode())){return;}}List<String> mainTables = getMainTable(statement);field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);if(dataPermissions==null || dataPermissions.contains(DataPermissionEnum.NONE.getCode())){String newSql = addWhereCondition(boundSql.getSql(), "1=2",DataPermissionEnum.NONE);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);return;}//获取公共mapper取字段是否存在CommonMapper commonMapper = SpringUtils.getBean(CommonMapper.class);String tenantTable = getFirstTableOnField("tenant_id", mainTables,commonMapper);if(tenantTable!=null) {//初始默认匹配租户id查询String tenantSql = addWhereCondition(boundSql.getSql(), tenantTable + ".tenant_id=" + loginUser.getTenantId(), DataPermissionEnum.NONE);//通过反射修改sql语句field.set(boundSql, tenantSql);System.out.println(tenantSql);}String organizationTable=null;if(dataPermissions.contains(DataPermissionEnum.DEPT.getCode())){organizationTable = getFirstTableOnField("create_user_organization_id", mainTables,commonMapper);if(organizationTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), organizationTable + ".create_user_organization_id=" + loginUser.getDeptId(), DataPermissionEnum.DEPT);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.DEPT_CHILDREN.getCode())){if(organizationTable==null){organizationTable = getFirstTableOnField("create_user_organization_id", mainTables,commonMapper);}if(organizationTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), getCondition(organizationTable, "create_user_organization_id", loginUser.getDeptChildren()), DataPermissionEnum.DEPT_CHILDREN);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.POST.getCode())){String postTable = getFirstTableOnField("tenant_id", mainTables,commonMapper);if(postTable!=null) {String sql = boundSql.getSql();if (sql.contains("where") || sql.contains("WHERE")) {sql = sql + " and ";} else {sql = sql + " where ";}String newSql = sql + getPositionCondition(postTable, "create_user_post_id", loginUser.getPostIds());//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}if(dataPermissions.contains(DataPermissionEnum.OWN.getCode())){String userTable = getFirstTableOnField("create_user", mainTables,commonMapper);if(userTable!=null) {String newSql = addWhereCondition(boundSql.getSql(), userTable + ".create_user=" + loginUser.getUserId(), DataPermissionEnum.OWN);//通过反射修改sql语句field.set(boundSql, newSql);System.out.println(newSql);}}} catch (JSQLParserException | NoSuchFieldException | IllegalAccessException e) {if(StringUtils.isNotEmpty(firstSql)&& ObjectUtils.isNotEmpty(field)){try {field.set(boundSql, firstSql);} catch (IllegalAccessException illegalAccessException) {illegalAccessException.printStackTrace();}}e.printStackTrace();}}/*** 获取拥有字段的指定第一张表** @param field* @return*/private String getFirstTableOnField(String field,List<String> tables,CommonMapper commonMapper) {if(CollectionUtil.isEmpty(tables))return null;for (String table : tables) {if(commonMapper.getFieldExists(table,field)>0)return table;}return null;}/*** 获取查询字段** @param selectBody* @return*/private List<SelectItem> getSelectItems(SelectBody selectBody) {if (selectBody instanceof PlainSelect) {return ((PlainSelect) selectBody).getSelectItems();}return null;}/*** 特殊处理 创建职业sql** @param tableName 表名* @param fieldName 字段名* @param ids 值* @return*/private String getPositionCondition(String tableName, String fieldName, Collection<String> ids) {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("(");for (String id : ids) {stringBuilder.append(tableName).append(".").append(fieldName).append(" like '%").append(id).append("%' or ");}stringBuilder.delete(stringBuilder.length()-3,stringBuilder.length()).append(")");return stringBuilder.toString();}/*** 生成where 条件字符串** @param tableName 表名* @param fieldName 字段名* @param ids 值* @return*/private String getCondition(String tableName, String fieldName, Collection<String> ids) {return tableName + "." + fieldName + " in (" + StringUtils.join(ids, ",") + ")";}/*** 获取tables的表名** @param statement* @return*/private List<String> getMainTable(Select statement) {TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();return tablesNamesFinder.getTableList(statement);}/*** 判断是否分页** @param selectBody* @return*/private Limit ifPage(SelectBody selectBody) {if (selectBody instanceof PlainSelect) {return ((PlainSelect) selectBody).getLimit();}return null;}/*** 判断是否分页** @param parameter* @return*/private boolean ifPage(Object parameter) {if(parameter instanceof String) return false;JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(parameter));return jsonObject.containsKey("page") ||jsonObject.containsKey("size") ||jsonObject.containsKey("current");}/*** 在原有的sql中增加新的where条件** @param sql 原sql* @param condition 新的and条件* @return 新的sql*/private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, List<String> detps,Set<String> posts) {try {Select select = (Select) CCJSqlParserUtil.parse(sql);PlainSelect plainSelect = (PlainSelect) select.getSelectBody();final Expression expression = plainSelect.getWhere();final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);if (Objects.isNull(expression)) {plainSelect.setWhere(envCondition);} else {if(DataPermissionEnum.NONE==dataPermissionEnum){AndExpression andExpression = new AndExpression(expression, envCondition);plainSelect.setWhere(andExpression);}else {OrExpression orExpression = new OrExpression(expression, envCondition);plainSelect.setWhere(orExpression);}}return plainSelect.toString();} catch (JSQLParserException e) {throw new RuntimeException(e);}}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum) {return addWhereCondition(sql,condition,dataPermissionEnum,null,null);}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, List<String> detps) {return addWhereCondition(sql,condition,dataPermissionEnum,detps,null);}private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, Set<String> posts) {return addWhereCondition(sql,condition,dataPermissionEnum,null,posts);}}
5.开源源码
这里给大家提供开源源码:java-springboot-mybatis-数据权限详细实现
编写不易,点个赞再走!
java(springboot) mybatis 数据权限详细实现(图文)相关推荐
- 基于javaweb的在线点餐系统(java+springboot+mybatis+vue+mysql+redis)
基于javaweb的在线点餐系统(java+springboot+mybatis+vue+mysql+redis) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:e ...
- Java程序员周末时间搞锭银行信息管理系统毕业设计(java+springboot+mybatis+mysql+vue+elementui)等实现 免费源码+论文答辩资料获取
Java程序员周末时间搞锭银行信息管理系统毕业设计(java+springboot+mybatis+mysql+vue+elementui)等实现 前言介绍: 在社会快速发展的影响下,银行继续发展,大 ...
- 基于javaweb+mysql的酒店管理系统(java+springboot+mybatis+beetl+layui)
基于javaweb+mysql的酒店管理系统(java+springboot+mybatis+beetl+layui) 运行环境 Java≥8.MySQL≥5.7 开发工具 eclipse/idea/ ...
- 基于java(springboot+mybatis)汽车信息管理系统设计和实现以及文档
java毕业设计项目<100套>推荐 主要实现技术:Java.springmvc.springboot.mysql.mybaits.jQuery.js.css等.使用eclipse/ide ...
- 基于javaweb的水果生鲜商城系统(java+springboot+mybatis+vue+mysql)
基于javaweb的水果生鲜商城系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:eclip ...
- Java SpringBoot+Mybatis Layui+JQuery+html微信公众号后台管理系统
Java SpringBoot+Mybatis Layui++html微信公众号后台管理系统 hddhln 玛雅资源 技术框架 开发语言:JAVA 数据库:MYSQL JAVA开发框架:SpringB ...
- 基于javaweb的精品养老院管理系统(java+springboot+mybatis+vue+mysql)
基于javaweb的精品养老院管理系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:ecli ...
- Java+Springboot+Mybatis+Mysql+Bootstrap+Maven实现网上商城系统
网上商城系统 一.系统介绍 1.软件环境 2.功能模块图 3.系统功能 4.数据库表 5.SQL语句 6.工程截图 二.系统展示 1.用户-浏览商品 2.用户-注册 3.用户-登录 4.用户-购物车管 ...
- 基于javaweb的平行志愿管理系统(java+springboot+mybatis+vue+mysql)
基于javaweb的平行志愿管理系统(java+springboot+mybatis+vue+mysql) 运行环境 Java≥8.MySQL≥5.7.Node.js≥10 开发工具 后端:eclip ...
最新文章
- linux awk 用一个或多个空格做分隔符
- 深夜,你的手机为谁开?
- (摘要)100个伟大的商业理念:理念34:企业社会责任
- boost::basic_thread_pool相关的测试程序
- iOS开源项目周报0323
- Matlab sumsqr函数
- c语言加减乘除计算程序,求一个计算加减乘除的C语言程序
- mysql ---- 多表设计
- tshark/wireshark抓包小结
- 使用路由器配置DHCP
- oracle查询小时差,ORACLE小时段 Connect By的查询,感觉还是有点难度的。
- Advanced Javascript outlining插件说明
- matlab画贝塞尔曲线给出图题,matlab练习程序(贝塞尔曲线)
- 用两个小样例来解释单例模式中的“双重锁定”
- facebook广告后台设置
- 蓝牙耳机蓝牙音箱出口加拿大亚马逊ICID认证周期费用
- 20190422每周精品之认知
- 一元二次求解matlab程序,规范MATLAB编程实例——求解一元二次方程
- 卷积神经网络膨胀卷积
- 基于retinex理论改进的低照度图像增强算法
热门文章
- 有包但import时pycharm提示No Modle name
- 分享 11 个常用的 Nginx 性能优化参数工作
- vue3中使用swiper完整版教程
- 2021年全球车辆到电网(V2G)收入大约33百万美元,预计2028年达到2008.7百万美元,2022至2028期间,年复合增长率CAGR为 79.9%
- Vue学习笔记1-什么是Vue
- 浏览器自己检测php代码,一个浏览器检查类_php
- 电脑技巧:分享五款办公文档密码解除小软件
- 如何给证件照换背景色并压缩到100K
- 微信中如何接入AI机器人才比较安全(不会收到警告或者f号)之第二步注入dll文件
- 基于AI深度学习的安全帽检测算法,如何应用在实际场景中?