通用数据权限设计与实现(mybatis拦截器 + spring aop)

        哈喽~ 大家好,今天给大家分享一下平时在工作当中遇到的数据权限问题。

        本人是通过掘金的文章才理解的数据权限,推荐大家也去看一下

        在我们的业务系统中,除了菜单/功能权限外,还有一个非常重要的功能,就是数据权限。数据及权限管理,大多数采用的方案还是硬编码的方式,也就是将这种逻辑以if/else等方式与业务代码耦合在一起,按需做数据权限划分。本文这里采用的是另一种比硬编码相对优雅的方式:mybatis拦截器+spring aop。

   技术基础

  • mybatis拦截器

    使用mybatis的自定义拦截器,我们可以对待执行的sql进行一层过滤、包装,如做sql日志记录、追加数据权限相关的sql等。

  • spring aop

    在不改变原代码的基础上增加功能或者横向相同功能抽取,具体实现形式就是代理,经典原型就是事物控制和日志,我们这里用来做数据权限控制。

    设计思路

        数据权限粒度

        常规的业务系统,数据粒度主要分为如下四种:

  •                 全部数据权限:即不做权限区分,记录都可查。
  •                 仅本人数据权限:只能查看自己管辖的数据
  •                 部门数据权限:只能查看用户所在部门的数据
  •                 部门及以下数据权限:可以查看用户所在部门及下属部门的数据
  •                 自定义数据权限:用于一个用户对应多个部门的场景

   业务表设计规范

        要想让业务数据有归属,则相关的业务表要有相关的字段:dept_id和user_id,即部门id和用户id,,如文章表:

CREATE TABLE `cms_article` (

`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',

`category_id` bigint(20) unsigned DEFAULT NULL COMMENT '栏目id',

`title` varchar(120) NOT NULL COMMENT '标题',

`description` varchar(255) DEFAULT NULL COMMENT '描述',

`cover` varchar(255) DEFAULT NULL COMMENT '大图',

`author` varchar(30) DEFAULT NULL COMMENT '作者',

`source` varchar(64) DEFAULT NULL COMMENT '文章来源',

`sort` double(10,2) unsigned DEFAULT '10.00' COMMENT '排序',

`publish_time` datetime(3) DEFAULT NULL COMMENT '发布时间',

`is_publish` tinyint(1) DEFAULT NULL COMMENT '是否发布(1->否|NO,2->是|YES)', `content` mediumtext COMMENT '文本内容',

`dept_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属部门',

`user_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属用户',

`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',

`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',

`is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT '是否删除(1->未删除|NO,2->已删除|YES)', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

数据权限粒度与查询 

        全部数据

   select * from cms_article

        部门数据 

   -- 需要传入当前用户所属部门id

   select * from cms_article a where a.dept_id = ?

       部门及以下数据

  -- 需要传入当前用户所属部门id及下属部门id

  select * from cms_article a where a.dept_id in (?,?,?,?)

        仅本人数据

   -- 需要传入当前用户id

   select * from cms_article a where a.user_id = ?

       自定义数据权限

  -- 需要传入当前用户id

  select * from cms_article a where a.dept_id in(select dept_id from sys_role_dept rd left join  sys_user_role ur on rd.role_id=ur.role_id where ur.user_id=? group by dept_id)

        或者

  -- 需要传入录前用户角色id

  select * from cms_article a where a.dept_id in(select dept_id from sys_role_dept role_id in (?,?,?))

代码实现

        目录结构

        

├── mldong-framework 框架

        ├── mldong-commom-base 基础模块

                ├── src/main/java

                         └── com.mldong.common.dauth

                                 ├── DataScope.java

                                 ├── DataScopeConfig.java

                                 ├── DataScopeHelper.java

                                 ├── DataScopeInterceptor.java

                                 └── DataSopeAspect.java

                 ├── src/main/resource/META-INF/spring.factories

        核心代码说明 

        DataSope.java

        

package com.mldong.common.dauth;

import java.lang.annotation.*;

@Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME)

@Documented public

@interface DataScope {

        /**

        * 部门表的别名

        */

        public String deptAlias() default "";

        /**

        * 用户表的别名

        */

        public String userAlias() default "";

}

 数据权限注解,加上注解的方法就会自动追加数据权限相关sql。

       DataScopeHelper.java

        ThreadLocal线程数据共享工具,aop构造数据权限sql存入threadLocal,拦截器读取数据权限sql。

        

package com.mldong.common.dauth;

public class DataScopeHelper {
    private static final ThreadLocal<String> LOCAL_DATA_AUTH_SQL = new ThreadLocal();
    public static void setLocalDataAuthSql(String page) {
        LOCAL_DATA_AUTH_SQL.set(page);
    }
    public static String getLocalDataAuthSql() {
        return LOCAL_DATA_AUTH_SQL.get();
    }
    public static void clearDataAuthSql() {
        LOCAL_DATA_AUTH_SQL.remove();
    }
}

         DataSopeAspect.java

         aop实现,这里维护数据权限sql的生命周期,构造->销毁

        

package com.mldong.common.dauth;
@Aspect
@Component
public class DataSopeAspect {
    /**
     * 全部数据权限
     */
    public static final Integer DATA_SCOPE_ALL = 10;

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

    /**
     * 部门及以下数据权限
     */
    public static final Integer DATA_SCOPE_DEPT_AND_CHILD = 30;
    /**
     * 仅本人数据权限
     */
    public static final Integer DATA_SCOPE_SELF = 40;
    /**
     * 自定义数据权限
     */
    public static final Integer DATA_SCOPE_CUSTOM = 50;
    // 加入@DataScope注解的方法执行前执行-用于构造数据权限sql
    @Before("@annotation(dataScope)")
    public void dataScopeBefore(JoinPoint point, DataScope dataScope) throws Throwable     {
        // 超级管理员,不处理
        if(RequestHolder.isSuperAdmin()) {
            return;
        }
        // TODO 详见源码
    }
    
    
    // 加入@DataScope注解的方法执行完成后执行-用于销毁数据权限sql
    @After("@annotation(dataScope)")
    @AfterThrowing("@annotation(dataScope)")
    public void dataScopeAfter(JoinPoint point, DataScope dataScope) throws Throwable{
        if(StringTool.isNotEmpty(DataScopeHelper.getLocalDataAuthSql())) {
            // 执行完成,要清除当前权限Sql
            DataScopeHelper.clearDataAuthSql();
        }
    }

}

         DataScopeInterceptor.java

           数据权限mybatis拦截器

package com.mldong.common.dauth;

import com.mldong.common.tool.StringTool;
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.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;

@Intercepts({@Signature(
        type = org.apache.ibatis.executor.Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class DataScopeInterceptor implements Interceptor {
    private static final Logger logger= LoggerFactory.getLogger(DataScopeInterceptor.class);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String dataAuthSql = DataScopeHelper.getLocalDataAuthSql();
        // 不为空才处理
        if(StringTool.isNotEmpty(dataAuthSql)) {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement)args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds)args[2];
            ResultHandler resultHandler = (ResultHandler)args[3];
            Executor executor = (Executor)invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }
            String newSql = boundSql.getSql() + dataAuthSql ;
            BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());
            // 把新的查询放到statement里
            MappedStatement newMs = newMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
            for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                String prop = mapping.getProperty();
                if (boundSql.hasAdditionalParameter(prop)) {
                    newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                }
            }
            return executor.query(newMs, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
        } else {
            return invocation.proceed();
        }
    }

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

    @Override
    public void setProperties(Properties properties) {
        logger.debug(properties.toString());
    }

    /**
     * 定义一个内部辅助类,作用是包装 SQL
     */
    class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;
        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }

    }

    private MappedStatement newMappedStatement (MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new
                MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
}
 

         DataScopeConfig.java

package com.mldong.common.dauth;

import com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;

import javax.annotation.PostConstruct;
import java.util.List;

// 后添加先执行,所以要在mybatis分页插件之后配置拦截器
// @AutoConfigureAfter注解需与spring.factories配合才生效
@AutoConfigureAfter(PageHelperAutoConfiguration.class)
public class DataScopeConfig {
    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addDataAuthInterceptor() {
        DataScopeInterceptor interceptor = new DataScopeInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }
}

         spring.factories

          这里主要是调一下mybatis分页插件与自定义的数据权限插件的加载顺序

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration,\
com.mldong.common.dauth.DataScopeConfig

 使用样例

        在dao层中使用

@Repository
public interface CmsArticleDao {
    /**
     * 查询文章列表-数据权限
     * @param param
     * @return
     */
    @DataScope(deptAlias = "a", userAlias = "a")
    public List<CmsArticleWithExt> selectOnDataScope(CmsArticlePageParam param);
}
 

         在service层中使用

@Service
public class CmsArticleServiceImpl implements CmsArticleService{
    @DataScope(deptAlias = "a", userAlias = "a")
    @Override
    public CommonPage<CmsArticleWithExt> listOnDataScope2(CmsArticlePageParam param) {
        Page<CmsArticleWithExt> page =param.buildPage(true);
        cmsArticleDao.selectWithExt(param);
        return CommonPage.toPage(page);
    }
}

效果演示 :

        略 

小结:

本文使用mybatis的拦截器机制配合aop实现了全局的数据权限处理,基本上能满足大多数有数据权限需求的业务。当然,如果是更为复杂的场景,如涉及到字段权限等的,可能就需要更为细粒化的设计了,这里暂时先不展开。

推荐去:项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值