springboot自定义注解+mybatis拦截器实现数据权限_数据规则生成where条件的方式实现数据权限-程序员宅基地

技术标签: spring boot  经验分享  java  其他  后端  

基本所有的系统都会涉及菜单访问权限和数据访问权限。对于菜单访问权限一般实现方式都差不多,用户登录时加载具有访问权限的菜单,然后进行展示,用户访问菜单时通过统一的拦截器服务器端再次判断是有具有访问权限,防止前端直接url越权访问;对于细粒度具体到按钮级别的访问控制,实现方式也差不多,一个具体到菜单访问url,一个具体到方法访问url,对于菜单访问权限设计实现此文不做过多介绍。对于数据权限,一般需要根据系统实际的业务场景进行设计实现,那些脱离业务谈数据权限都是扯淡的。参与的一个项目,需要实现如下数据权限,可以设置登录用户访问所有的数据、访问本部门的数据、访问本部门及子部门数据、访问与自己相关的数据,对此进行了设计实现,不多说,直接上代码。

一、设计思路

1、用户登录时获取到用户信息和所在部门信息,存取到缓存中

2、自定义数据权限注解,对于添加注解的方法解析拼接数据权限查询sql条件

3、自定义mybatis拦截器,对于需要数据权限过滤的重写查询sql,拼接上数据过滤sql查询条件,以达到数据访问控制的效果

说明:需要部门表上添加data_scope字段(1全部,2本部门,3部门及以下,4本人),代表数据权限范围;full_path字段,代表部门全路径,id拼接。所有的需要进行数据权限过滤的业务表添加dept_id字段,代表数据属于哪个部门;create_by字段,代表是谁创建的数据;否则不会进行数据权限过滤。

二、代码实现

1、数据权限自定义注解

/**
 * 数据权限过滤注解
 *
 * @author wangfenglei
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataAuthScope {
    /**
     * 部门表的别名
     */
    String deptAlias() default "";

    /**
     * 用户表的别名
     */
    String userAlias() default "";
}

2、数据权限切面,添加数据权限注解的方法执行切面方法

import net.wfl.framework.boot.model.vo.LoginUser;
import net.wfl.framework.boot.model.vo.SysDepartModel;
import net.wfl.user.api.UserApi;
import net.wfl.user.auth.aspect.annotation.DataAuthScope;
import net.wfl.user.auth.shiro.util.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 数据过滤处理
 *
 * @author wangfenglei
 */
@Aspect
@Component
public class DataAuthScopeAspect {
    public static final ThreadLocal DATA_AUTH_THREAD_LOCAL = new ThreadLocal();
    /**
     * 全部数据权限
     */
    public static final Integer DATA_SCOPE_ALL = 1;

    /**
     * 部门数据权限
     */
    public static final Integer DATA_SCOPE_DEPT = 2;

    /**
     * 部门及以下数据权限
     */
    public static final Integer DATA_SCOPE_DEPT_AND_CHILD = 3;

    /**
     * 仅本人数据权限
     */
    public static final Integer DATA_SCOPE_SELF = 4;

    @Autowired
    private UserApi userApi;

    @Before("@annotation(controllerDataAuthScope)")
    public void doBefore(JoinPoint point, DataAuthScope controllerDataAuthScope) throws Throwable {
        DATA_AUTH_THREAD_LOCAL.remove();
        handleDataAuthScope(point, controllerDataAuthScope);
    }

    @After("@annotation(controllerDataAuthScope)")
    public void doAfter(DataAuthScope controllerDataAuthScope) throws Throwable {
        //清空数据权限拼接SQL
        DATA_AUTH_THREAD_LOCAL.remove();
    }

    /**
     * 处理数据权限
     *
     * @param joinPoint     切面
     * @param dataAuthScope 数据权限
     */
    protected void handleDataAuthScope(final JoinPoint joinPoint, DataAuthScope dataAuthScope) {
        // 获取当前的用户
        LoginUser currentUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();

        if (null == currentUser) {
            return;
        }

        List<SysDepartModel> departModelList = currentUser.getDepartList();
        List<String> departIdList = new ArrayList<>();
        List<String> userNameList = new ArrayList<>();

        for (SysDepartModel departModel : departModelList) {
            if (DATA_SCOPE_ALL.equals(departModel.getDataScope())) {
                continue;
            } else if (DATA_SCOPE_DEPT.equals(departModel.getDataScope())) {
                departIdList.add(departModel.getId());
            } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(departModel.getDataScope())) {
                List<String> departIds = getChildDepartIdList(departModel);
                if (!CollectionUtils.isEmpty(departIds)) {
                    departIdList.addAll(departIds);
                }
            } else if (DATA_SCOPE_SELF.equals(departModel.getDataScope())) {
                userNameList.add(currentUser.getUsername());
            }
        }

        StringBuilder sqlString = new StringBuilder();
        //如果需要部门权限
        if (!CollectionUtils.isEmpty(departIdList)) {
            sqlString.append(StringUtils.format(" OR {}dept_id IN ({})", dataAuthScope.deptAlias(), getDepartSqlStr(departIdList)));
        } else if (!CollectionUtils.isEmpty(userNameList)) {
            sqlString.append(StringUtils.format(" OR {}createBy = {}", dataAuthScope.userAlias(), currentUser.getUsername()));
        }

        if (StringUtils.isNotBlank(sqlString.toString())) {
            //设置数据权限拼接sql
            DATA_AUTH_THREAD_LOCAL.set(" AND (" + sqlString.substring(4) + ")");
        }
    }

    /**
     * 获取本部门和子部门ID列表
     *
     * @param departModel 部门信息
     * @return 本部门和子部门ID列表
     */
    private static List<String> getChildDepartIdList(SysDepartModel departModel) {
        String id = departModel.getId();
        String fullPath = departModel.getFullPath();
        String childIds = fullPath.substring(fullPath.indexOf(id), fullPath.length());
        String[] childArr = childIds.split(",");
        return Arrays.asList(childArr);
    }

    /**
     * 拼接部门sql
     *
     * @param departIdList 部门ID列表
     * @return 拼接部门sql
     */
    private static String getDepartSqlStr(List<String> departIdList) {
        if (CollectionUtils.isEmpty(departIdList)) {
            return null;
        }

        StringBuilder sql = new StringBuilder("'");
        departIdList.forEach(data -> {
            sql.append(data).append("'").append(",");
        });

        sql.delete(sql.length() - 1, sql.length());
        return sql.toString();
    }

 说明:拼接的数据过滤查询sql条件存储到ThreadLocal中,在mybatis拦截器中使用

3、自定义mybatis拦截器实现

import lombok.extern.slf4j.Slf4j;
import net.wfl.user.auth.aspect.DataAuthScopeAspect;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.Properties;

/**
 * 数据权限sql查询条件,拼接新的sql,实现数据权限过滤
 *
 * @Author wangfenglei
 */
@Slf4j
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public class DataAuthInterceptor implements Interceptor {
    /**
     * 分组
     */
    private static final String GROUP_BY = "group by";
    /**
     * 排序
     */
    private static final String ORDER_BY = "order by";
    /**
     * 分组
     */
    private static final String LIMIT = "limit";
    /**
     * from
     */
    private static final String FROM = "from";

    /**
     * where
     */
    private static final String WHERE = "where";
    /**
     * 分隔符
     */
    private static final String SPLIT_CHARACTER = ",";

    /**
     * where条件拼接
     */
    private static final String WHERE_CONDITION = " where 1=1 ";

    @Value("${wfl.data-auth-table:null}")
    private  String dataAuthTable;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

        if (SqlCommandType.SELECT == sqlCommandType) {
            Object dataAuthSql = DataAuthScopeAspect.DATA_AUTH_THREAD_LOCAL.get();
            //如果添加数据权限
            if (null != dataAuthSql) {
                BoundSql boundSql = (BoundSql) invocation.getArgs()[5];
                //获取到原始sql语句
                String sql = boundSql.getSql();
                String mSql = sql;
                //多个空格替换为一个空格
                mSql = mSql.replaceAll("\\s{1,}", " ").toLowerCase();
                mSql = addWhere(mSql);
                //获取查询主表
                String queryTable = getQueryTable(mSql);
                //如果查询主表可以进行数据权限过滤
                if (dataAuthTable.indexOf(queryTable) >= 0) {
                    //重写sql
                    StringBuilder newSqlBuilder = new StringBuilder();
                    //重写sql语句 前面拼接数据权限语句
                    if (mSql.indexOf(GROUP_BY) > 0) {
                        newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(GROUP_BY)))
                                .append(dataAuthSql.toString())
                                .append(" ")
                                .append(mSql.substring(mSql.lastIndexOf(GROUP_BY), mSql.length()));
                    } else if (mSql.indexOf(ORDER_BY) > 0) {
                        newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(ORDER_BY)))
                                .append(dataAuthSql.toString())
                                .append(" ")
                                .append(mSql.substring(mSql.lastIndexOf(ORDER_BY), mSql.length()));
                    } else if (mSql.indexOf(LIMIT) > 0) {
                        newSqlBuilder.append(mSql.substring(0, mSql.lastIndexOf(LIMIT)))
                                .append(dataAuthSql.toString())
                                .append(" ")
                                .append(mSql.substring(mSql.lastIndexOf(LIMIT), mSql.length()));
                    } else if (mSql.indexOf(WHERE) > 0) {
                        newSqlBuilder.append(mSql)
                                .append(" ")
                                .append(dataAuthSql.toString());
                    } else {
                        newSqlBuilder.append(mSql)
                                .append(WHERE_CONDITION)
                                .append(dataAuthSql.toString());
                    }

                    log.debug("=====DataAuth sql rewrite:\nold sql:={}\nnew sql:={}", sql, newSqlBuilder.toString());

                    //通过反射修改sql语句
                    Field field = boundSql.getClass().getDeclaredField("sql");
                    field.setAccessible(true);
                    field.set(boundSql, newSqlBuilder.toString());
                }
            }
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 获取查询主表
     *
     * @param sql sql语句
     * @return 主表表名
     */
    private String getQueryTable(String sql) {
        String tempSql = "";
        if (sql.indexOf(WHERE) > 0) {
            tempSql = sql.substring(sql.indexOf(FROM), sql.indexOf(WHERE)).trim();
        } else {
            tempSql = sql.substring(sql.indexOf(FROM), sql.length()).trim();
        }

        String table = tempSql.split(" ")[1];
        return table.split(SPLIT_CHARACTER)[0].trim();
    }

    /**
     * 添加where关键字
     *
     * @param sql sql语句
     * @return sql
     */
    private String addWhere(String sql) {
        if (sql.indexOf(WHERE) >= 0) {
            return sql;
        }

        StringBuilder newSqlBuilder = new StringBuilder();
        if (sql.indexOf(GROUP_BY) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(GROUP_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(GROUP_BY), sql.length()));
        } else if (sql.indexOf(ORDER_BY) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(ORDER_BY)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(ORDER_BY), sql.length()));
        } else if (sql.indexOf(LIMIT) > 0) {
            newSqlBuilder.append(sql.substring(0, sql.lastIndexOf(LIMIT)))
                    .append(WHERE_CONDITION)
                    .append(sql.substring(sql.lastIndexOf(LIMIT), sql.length()));
        } else {
            newSqlBuilder.append(sql).append(" ").append(WHERE_CONDITION);
        }

        return newSqlBuilder.toString();
    }
}

说明: 原始sql不要通过以下方式获取,项目一般会集成mybatis-plus分页插件,这种方式获取会导致分页查询失效

Object parameter = invocation.getArgs()[1];
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
//获取到原始sql语句
String sql = boundSql.getSql();

需要通过如果方式获取,对于未集成mybatis-plus的查询是否也如此有待验证

 BoundSql boundSql = (BoundSql) invocation.getArgs()[5];
 //获取到原始sql语句
 String sql = boundSql.getSql();

三、此实现的不足

此设计实现通过用户所在部门的数据权限范围,自动生成数据过滤查询条件,然后通过sql拦截器组装数据过滤条件,最终达到数据权限控制的效果。

1、此实现在重写sql时未考虑union查询的情况;

2、对于超级复杂的sql也许会存在过滤失败的情况。

不过此实现面对开发人员使用,可以知道方法查询哪些数据,基本可以满足业务场景需要 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/guaotianxia/article/details/121653866

智能推荐

响应式编程实现异步RPC,提升xxl-job调度吞吐量-程序员宅基地

文章浏览阅读1.1k次。在xxl-job中,RPC即用于调度中心请求执行器执行job、kill job,也用于执行器请求调度中心主动注册、执行结果上报。xxl-job实现的RPC类似Feign框架,是基于http..._xxljob 用的什么协议

C++ Json到对象的自动序列化和反序列化工作_c++ json序列化和反序列化-程序员宅基地

文章浏览阅读555次,点赞17次,收藏22次。JSERIALIZE_DEF_OBJECTLIST(Person,Object,objectList) //接受json中的objectList对象数组,对象数组使用此宏定义。JSERIALIZE_DEF_OBJECTTYPE(Person,Son,son) //接受json中的son对象,对象成员使用此宏定义。//输出反序列化结果。

DOSBOX 0.74模拟器安装Windows 95_dosbox imgmount-程序员宅基地

文章浏览阅读7.8k次,点赞2次,收藏6次。DosBox本身带有5.0版的DOS系统,启动后虚拟一个Z盘存放有Dosbox特有的外部指令,如config.com、imgmount.com等,经测试,可以顺利安装各版本的windows 3.1系统,但是不能安装win95,需要用原版的dos镜像启动才能安装。1. 获取启动盘镜像文件 下载Win95启动软盘镜像文件,名为boot.img,放到DosBox 0.74的目录下。2. 制作硬盘镜像文件_dosbox imgmount

呼叫转移的普适性及编程实现_电话自动转移程序开发-程序员宅基地

文章浏览阅读53次。总结来说,呼叫转移是一种方便的电话通信功能,在编程中可以通过使用电话服务提供商的API来实现。然而,实际的实现可能因具体的服务提供商而有所不同,你需要参考相应的文档或与服务提供商联系以获取准确的实现细节。在函数内部,我们构建了一个API请求的有效载荷(payload),其中包含了原始电话号码和目标电话号码。在编程中,呼叫转移的实现涉及使用电话通信协议和相应的编程语言。需要注意的是,实际的呼叫转移功能的实现可能因电话服务提供商的不同而有所差异。首先,我们需要确保已经安装了Python的开发环境和相应的库。_电话自动转移程序开发

FLink聚合性能优化--MiniBatch分析_flink mini-batch-程序员宅基地

文章浏览阅读5.4k次,点赞4次,收藏15次。[@ TOC]一、MiniBatch的演进思路1、MiniBatch版本Flink 1.9.0 SQL(Blink Planner) 性能优化中一项重要的改进就是升级了微批模型,即 MiniBatch(也称作MicroBatch或MiniBatch2.0),在支持高吞吐场景发挥了重要作用。MiniBatch与早期的MiniBatch1.0在微批的触发机制略有不同。原理同样是缓存一定的数据后..._flink mini-batch

EasyExcel导入_easyexcel 对接multipartfile-程序员宅基地

文章浏览阅读808次,点赞6次,收藏6次。导入依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.1.6</version></dependency>Controllerimport java.text.ParseException;import org.springframework._easyexcel 对接multipartfile

随便推点

如何构建知识体系_网络智能知识体系的构建方法-程序员宅基地

文章浏览阅读286次。分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请轻击http://www.captainbed.net先说一件值得思考的事情:高考的时候大家都是一样的教科书,同一个教室,同样的老师辅导,时间精力基本差不多,可是最后别人考的是清华北大或者一本,而你的实力只能考个三本,为什么?当然这里主要是智商的影响,那么其他因素呢?智商解决的问题能不能后天用其他方式来补位一下?大家平时都看过很多方法论的文章,看的时候很爽觉得非常有用,但是一两周后基本还是老样子了。其中有很大_网络智能知识体系的构建方法

超全的数组去重12种方法_数组去重方法-程序员宅基地

文章浏览阅读2.7w次,点赞33次,收藏349次。前言数组去重,可以说是一个比较常见的面试题,今天来盘点一下都有哪些方法可以实现数组去重。方法1、双重for循环这是一个最笨的方法,双重循环。var arr = [1, 2, 3,4 ,5,6, 4, 3, 8, 1] // 数组去重: // 方法1: 双重for 循环 function newArrFn (arr) { // 创建一个新的空数组 let newArr = [] for(let i = 0;i<arr.length;i+_数组去重方法

2022考研日语71分自学经验贴;日语可以自学吗?-程序员宅基地

文章浏览阅读1.2k次,点赞3次,收藏5次。目录1 个人对考研日语的评价1 日语VS英语2 考研日语适合哪些人,什么时候开始3 找到可以选日语的院校专业的方法4 高考日语自学经历(供参考)4.1 学习过程4.2 必用资料5 考研日语自学+作文课经历(供参考)5.1 资料相关5.2 完型(20分)5.3 阅读(40分)5.4 翻译(15分)5.5 作文(25分)end实在受不了英语应试的折磨,高考和考研都用了203日语替换了英语(高考127分,考研估分65-70分)1 个人对考研日语的评价我是有了高考127分的基础(大概N3水平,N2擦线水平),

JVM性能优化 (一) 初识JVM-程序员宅基地

文章浏览阅读703次,点赞22次,收藏24次。到这里文章就讲完了,有疑问的兄弟可以在下面讨论或留言,也祝大家在今年开开心心,健健康康,能够拥有一份好工作,大家加油,我是牧小农!自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

通过手动给upx去壳简单了解逆向_upx脱壳机-程序员宅基地

文章浏览阅读1.7k次。对于像我这种想入门逆向的,这种方式真的可以培养兴趣,也从中学到了很多知识,我也不会仅仅止步于脱upx的。[外链图片转存中…(img-xkCBlSoD-1693021558445)]即可。对于像我这种想入门逆向的,这种方式真的可以培养兴趣,也从中学到了很多知识,我也不会仅仅止步于脱upx的。_upx脱壳机

Quartz定时任务调度cron 表达式时间格式(☆)_cron表达式 下午5点30-程序员宅基地

文章浏览阅读890次。cron 表达式的格式 Quartz Cron 表达式支持到七个域 名称 是否必须 允许值 特殊字符 秒 是 0-59 ..._cron表达式 下午5点30

推荐文章

热门文章

相关标签