# 数据权限

在角色上可以定义角色的数据权限,多个角色会合并数据权限范围最大的。目前实现以下类型数据权限:

  • 全部
  • 本部门
  • 本部门及以下
  • 本人

需要其他类型可以自行扩展,只需要维护框架暴露的权限sql即可,系统模块不在控制范围内。

# 实现原理

采用Mybatis 拦截器注入sql,一般有两种做法:

  • SQL改写:基于Sql(jsqlparser、druid sql parser) 解析器将过滤sql插入到合适的位置;
  • 参数上下文:XML 中手动放入变量${dsf},dsf由拦截器在参数上下文放入值;

第一种改写sql较为简单,解析sql,比较耗性能,需要处理缓存、复杂sql 等一系列问题。

第二种更简单,代码生成时候再查询、更新、删除后加入${dsf},然后拦截器中放入即可,更加灵活,性能更好。👍

本框架采用了第二种,由于使用的PageHelper 的插件,PageHelper 对Map参数的处理采用putAll的方式,无法把我们做的WrapContextMap中的数据完全读出,所以针对有分页上下文的,采用手动放入的方式。

package com.seezoon.dao.framework.authority;

import java.util.HashMap;
import java.util.Objects;
import java.util.Properties;

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.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.SimpleTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.pagehelper.PageHelper;
import com.seezoon.dao.framework.entity.PageCondition;

/**
 * @author hdf
 */
@Intercepts({
    @Signature(type = 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}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class DataAuthorityInterceptor implements Interceptor {

    protected final Logger logger = LoggerFactory.getLogger(DataAuthorityInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        DataAuthority dataAuthority = DataAuthorityLoader.getInstance();
        String dsf = null;
        if (Objects.isNull(dataAuthority)) {
            logger.info("DataAuthority Spi not found, disable data authority");
        } else {
            dsf = dataAuthority.getDsf();
        }
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement)args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        // skip insert
        if (sqlCommandType.equals(SqlCommandType.INSERT)) {
            return invocation.proceed();
        }
        Object parameter = (Object)args[1];

        // 成本最低的方式 这里主要是和PageHelper 拦截器针对参数处理不一样,PageHelper 对map类型(WrapContextMap) 直接用了putAll,
        // 如果要兼容需要把parameter 都放到map中,对简单类型字段
        if (sqlCommandType.equals(SqlCommandType.SELECT) && null != PageHelper.getLocalPage() && null != parameter
            && parameter instanceof PageCondition) {
            ((PageCondition)parameter).setDsf(dsf);
        }

        WrapContextMap wrapContextMap = new WrapContextMap(parameter);
        // 自定义附件参数,多租户也可以自定义参数
        wrapContextMap.put("dsf", dsf);
        args[1] = wrapContextMap;
        return invocation.proceed();
    }

    @Override
    public void setProperties(Properties properties) {
        // 需要插件传参时候使用
    }

    /**
     * 在参数上下中自动加入数据权限过滤的字符串 ${dsf}
     *
     * @see <a>https://blog.csdn.net/lqzkcx3/article/details/80820327</a>
     *      参考{@link org.apache.ibatis.scripting.xmltags.DynamicContext}中{@code ContextMap} 的实现
     */
    class WrapContextMap extends HashMap<String, Object> {

        private final Object parameter;
        private final MetaObject metaObject;

        public WrapContextMap(Object parameter) {
            this.parameter = parameter;
            if (null != parameter) {
                this.metaObject = SystemMetaObject.forObject(parameter);
            } else {
                this.metaObject = null;
            }
        }

        @Override
        public Object get(Object key) {
            String strKey = (String)key;
            if (super.containsKey(strKey)) {
                return super.get(strKey);
            }

            if (null == parameter) {
                return null;
            }

            if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                return parameter;
            }

            if (metaObject != null) {
                Object object = metaObject.getValue(strKey);
                return object;
            }

            return null;
        }
    }
}

# SQL改写的方式

改写SQL后继续交给Mybatis的调用链,这里做个笔记。

 BoundSql boundSql = ms.getBoundSql(parameter);
                 String sql = boundSql.getSql();
                 log.info("rewrite sql, origin sql: [{}], new sql: [{}]", sql, newSql);
                 BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql,
                          boundSql.getParameterMappings(), boundSql.getParameterObject());
                  // copy原始MappedStatement的各项属性
                  MappedStatement.Builder builder =
                          new MappedStatement.Builder(mappedStatement.getConfiguration(), mappedStatement.getId(),new WarpBoundSqlSqlSource(newBoundSql), mappedStatement.getSqlCommandType());
                  builder.cache(mappedStatement.getCache()).databaseId(mappedStatement.getDatabaseId())
                          .fetchSize(mappedStatement.getFetchSize())
                          .flushCacheRequired(mappedStatement.isFlushCacheRequired())
                          .keyColumn(StringUtils.join(mappedStatement.getKeyColumns(), ','))
                          .keyGenerator(mappedStatement.getKeyGenerator())
                          .keyProperty(StringUtils.join(mappedStatement.getKeyProperties(), ','))
                          .lang(mappedStatement.getLang()).parameterMap(mappedStatement.getParameterMap())
                          .resource(mappedStatement.getResource()).resultMaps(mappedStatement.getResultMaps())
                          .resultOrdered(mappedStatement.isResultOrdered())
                          .resultSets(StringUtils.join(mappedStatement.getResultSets(), ','))
                          .resultSetType(mappedStatement.getResultSetType()).statementType(mappedStatement.getStatementType())
                          .timeout(mappedStatement.getTimeout()).useCache(mappedStatement.isUseCache());
                  MappedStatement newMappedStatement = builder.build();
                  // 将新生成的MappedStatement对象替换到参数列表中
                  args[0] = newMappedStatement;

# 拦截器的顺序问题

为了清晰的定义顺序,没有使用pagehelper-spring-boot-starter,采用配置mybatis-config.xml添加分页拦截器和数据权限拦截器。

关于分页拦截器的特殊点PageHelper拦截器文档 (opens new window)