# java-springboot-mybatis-数据权限详细实现 **Repository Path**: wxgchat/data-permission ## Basic Information - **Project Name**: java-springboot-mybatis-数据权限详细实现 - **Description**: java(springboot) mybatis 数据权限详细实现(图文),原文博客地址https://blog.csdn.net/cyy9487/article/details/124823962,步骤书写得非常详细 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 43 - **Created**: 2022-06-30 - **Last Updated**: 2022-06-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README  --- # 噼里啪啦 来吧,整起,又一新功能,通用数据权限,注意是通用,通用的东西,反正挺烦的,(每天喝点茶,刷点文章,发表下博文,改下bug,写点crud多舒服的,啊哈哈,噗) 我还是第一次搞这玩意儿,因为之前做细节的数据权限都是直接写在代码里面的 好,开整,这篇文章我会写得详细一点,并且提供开源源码,全靠我自己设计,编码,一步步的敲出来的,很少的地方借鉴到了别人的东西,切看切珍惜,动动你的小手点个赞,点个收藏吧,写文章还真的挺累的。 --- # 一、啥子是数据权限? 嗯,数据权限?有些朋友可能会问了,"嗯,数据还有权限?" 没错,简单来讲:**数据权限无非就是某人只能看到某些数据。** **举个例子**:张三登录了A系统,那么根据系统查询出来的张三所拥有的权限,比如张三有一个A部门的数据权限, 那么,在A系统中,张三只能看到A部门相关的数据。 # 二、做这个功能的思路是啷个一个样子的? 那好了,啷个才能实现这个功能呢? 别慌,我们先回忆一下我们在不做通用的情况下是怎么做数据权限的呢? ## 1.没有做通用数据权限 比如张三有A部门的权限: ```java String deptId = servie.getDeptByUser("张三"); //在xml里 select * from test_table where dept_id = #{deptId} ``` 如上面的代码所示,我们一般是通过直接写sql带条件的方式实现的,写起来非常的方便,但是代码多了就求了,万一有1W个mapper都需要这样做,那写到吐,好吧,给你搞个通用的。 ## 2.实现的原理和思路 好,上面已经说到了痛点,那么我们只需要拦截住我们需要的sql,能根据获取到的用户数据权限,动态的拼接出我们想要的sql,再给他装回去,那这个问题就能解决了。 ok,按照这个步骤:**用户登录→登录验证通过后获取到用户的所有权限信息→放入到redis中→做数据查询时拦截对应的sql→详细封装处理→执行新sql返回权限后数据** --- # 二、开整! ## 1.新建一个项目并添加依赖和工具类包 ![在这里插入图片描述](https://img-blog.csdnimg.cn/3c1253ee0c0242c29f7fe68b09736c2c.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/e361d53ea9d041978f0256a3eaba3ff5.png) ### 初始化 选择spring初始化,下一步下一步就可以了,名字随便取,先不用选依赖。 建完了在详细下面找到pom.xml,添加如下依赖 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.4.RELEASE com.kbplus.demo.data permission 0.0.1-SNAPSHOT permission Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.apache.httpcomponents httpclient 4.5.12 mysql mysql-connector-java 8.0.28 com.alibaba druid-spring-boot-starter 1.1.24 com.baomidou mybatis-plus-boot-starter 3.4.0 com.baomidou mybatis-plus-generator 3.4.0 org.apache.commons commons-lang3 3.9 cn.hutool hutool-all 5.7.16 org.projectlombok lombok 1.18.8 provided io.springfox springfox-swagger2 2.9.2 io.springfox springfox-swagger-ui 2.9.2 com.alibaba fastjson 1.2.68 org.springframework.boot spring-boot-starter-data-redis redis.clients jedis io.jsonwebtoken jjwt 0.9.1 org.springframework.boot spring-boot-maven-plugin ``` ### 建库建表 添加数据库,并导入提供好的sql ![在这里插入图片描述](https://img-blog.csdnimg.cn/06b6ddd78e9b4179832ff9a3a9074ccf.png) 名字随便取,字符集和排序规则选择我选择的就ok,比较通用的utf规则和排序规则, 简单介绍一下,utf8mb4是mysql的一种拓展字符集,可以存储一些特殊字符,utf8mb4_general_ci是兼容大多主流语言并同时比较高效的排序规则,如果你的项目有使用到少见的语言,比如俄语,可以使用utf8mb4_unicode_ci来提高精准性 ![在这里插入图片描述](https://img-blog.csdnimg.cn/89a4d3ceb27a49a4897138d593a60eae.png) 导入完sql后,大概是这么一个结构。 ### 添加公共配置,工具类 接着添加项目所需类包(源码里会提供), 大致结构如下: ![在这里插入图片描述](https://img-blog.csdnimg.cn/87b4faa1a99b41468b1a947ff1a18551.png) ## 2.书写简单的登录实现登录验证并保存用户信息 核心代码如下: ```java /** * 登录验证 * * @param username 用户名 * @param password 密码 * @return 结果 */ @Override public String login(String username, String password) { User user1 = baseMapper.selectOne(new QueryWrapper().eq("username", username)); if(user1==null){ throw new CustomException("用户不存在"); } if(!password.equals(user1.getPassword())){ throw new CustomException("密码不正确"); } //这里使用mybaitis特性collection之类的好像会更快,各位有兴趣可以尝试 Set allRoleByUserId = roleMapper.getAllRoleByUserId(user1.getId()); UserDept userDept = userDeptMapper.selectOne(new QueryWrapper().eq("user_id", user1.getId())); Set allPositionByUserId = positionMapper.getAllPositionByUserId(user1.getId()); List deptChildren = departmentService.getDeptChildren(userDept.getDeptId()); Set 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); //存入redis tokenService.setLoginUser(loginUser); // 生成token return tokenService.createToken(loginUser); } ``` 在创建token的时候有这一步,以便可以通过token直接拿到想要的信息,不用去redis再查。 ```java /** * 创建令牌 * * @param loginUser 用户信息 * @return 令牌 */ public String createToken(LoginUser loginUser) { Map 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一键导入,详情大家可以参考我的权限认证文章:[待完善](https://blog.csdn.net/cyy9487) 我们使用user1账号登录后创建2条测试数据,再用user2登录创建2条测试数据,步骤如下: **2.使用user1模拟登录:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/6ab178cd5032498e965d784ed8497a26.png) **3.拿到返回的token去生成测试数据:** ![在这里插入图片描述](https://img-blog.csdnimg.cn/7319aeb32b6a478aa6fa9dac7fa561c1.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/a58b6e65c1d2423287349bec17146c46.png) 使用user2登录做同样的操作,让后可以看到表里有6条数据,null值的是我之前添加的不用在意~~,主要可以看到有4条是部门1的,2条是部门2的 我设置的权限分别是user1是部门1,user2是部门2,且部门2是部门1的子部门 ![在这里插入图片描述](https://img-blog.csdnimg.cn/6fdb35d8bb99432eb5242487035adfa3.png) **4.测试效果:** 使用user1成功返回6条数据,因为我现在给他的数据权限是拥有部门及子部门,所有能查到所有 ![在这里插入图片描述](https://img-blog.csdnimg.cn/81f2e31b3dcf442ea94c7ead32f6331a.png) 使用user2登录,user2和user1拥有一样的角色,即拥有一样的权限,请求接口,发现只返回了2条数据,且都是dept2的,因为部门2是部门1的子集 ![在这里插入图片描述](https://img-blog.csdnimg.cn/8536e34212084713b5b9db001d2f34c1.png) 目前系统支持以下类型的数据权限 ```java ALL("1","拥有所有数据权限"), NONE("2","未拥有数据权限"), DEPT("3","拥有部门权限"), DEPT_CHILDREN("4","拥有部门权限及子权限"), POST("5","拥有职位权限"), // POST_CHILDREN("6","拥有职位权限及子权限"), OWN("7","拥有自身权限"), ``` 大家可以根据自己的需要进行测试 ## 4.核心类解析 新建拦截机继承 InnerInterceptor (mybatis-plus的一个拦截器) ```java public class DataPermissionInterceptor implements InnerInterceptor ``` 把拦截器注入进配置,这里注意添加拦截器的编写顺序,会影响到拦截器执行的先后顺序 ```java 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 { @Bean public DataPermissionInterceptor dataPermissionInterceptor() { return new DataPermissionInterceptor(); } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setDbType(DbType.MYSQL); interceptor.addInnerInterceptor(dataPermissionInterceptor()); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } } ``` 核心拦截类:大致原理是先做有效性判断,包括是否属于管理员等,这里只拦截需要分页的查询,然后根据权限匹配,生成相对应的代码 ```java 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 { @Autowired private TokenService tokenService; @Autowired private HttpServletRequest httpServletRequest; @Override public 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 dataPermissions = loginUser.getDataPermissions(); Set roles = loginUser.getRoles(); for (Role role : roles) { if("admin".equals(role.getCode())){ return; } } List 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 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 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 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 ids) { return tableName + "." + fieldName + " in (" + StringUtils.join(ids, ",") + ")"; } /** * 获取tables的表名 * * @param statement * @return */ private List 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 detps,Set 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 detps) { return addWhereCondition(sql,condition,dataPermissionEnum,detps,null); } private String addWhereCondition(String sql, String condition, DataPermissionEnum dataPermissionEnum, Set posts) { return addWhereCondition(sql,condition,dataPermissionEnum,null,posts); } } ``` ## 5.开源源码 这里给大家提供开源源码:[java-springboot-mybatis-数据权限详细实现](https://gitee.com/kbplus/data-permission) 编写不易,点个赞再走!