基于springboot+注解+mybatis拦截器的数据权限控制

一、功能设计

基于角色的权限访问控制(Role-Base Access Control)
角色数据权限

(1)、需要做数据权限功能的表加上一个权限id字段,如部门id;
(2)、将部门分配给不同角色,再将角色分配给用户;
(3)、在mapper上添加注解,拦截指定的sql;

    /**
     * 根据条件分页查询用户列表
     * 
     * @param userVo 用户信息
     * @return 用户信息集合信息
     */
    @DataScope(scopeTableAlias = "d")
    public List<SysUser> selectUserList(UserVo userVo);

(4)、取到对应sql后将sql进行处理,如:

select * from t where t.部门id in ('部门1','部门2','部门3')

二、后端实现

1、创建数据权限注解类

DataScope 权限注解

/**
 * 数据权限过滤注解
 * 
 * @author ao
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
    /**
     *  表的别名
     */
    public String scopeTableAlias() default "";
}

2、创建自定义mybatis拦截器

DataScopeInterceptor 类

/**
 * Mybatis - 数据权限拦截器
 * @author ao
 */
@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}),
        }
)
@Component
public class DataScopeInterceptor implements Interceptor {
@Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    	// 初始化 tokenService 从redis获取用户信息
        TokenService tokenService = SpringBootUtils.getBean(TokenService.class);
        //获取mapper对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // 获取方法上的数据权限注解,如果没有注解,则直接通过(mybatis责任链模式)
        DataScope dataScope = getPermissionByDelegate(mappedStatement);
        if (dataScope == null) {
            log.info("----未添加注解,无需数据过滤----");
            return invocation.proceed();
        }
        log.info("----启用数据权限拦截器----");
        if (StringUtils.isEmpty(dataScope.scopeTableAlias())
                && StringUtils.isEmpty(dataScope.userAlias())
                && StringUtils.isEmpty(dataScope.deptAlias())){
            log.info("----未添加表别名,跳过数据过滤----");
            return invocation.proceed();
        }
        // 获取用户信息
        LoginUser loginUser = tokenService.getLoginUser();
        if (loginUser == null) {
            log.error("数据权限获取失败,当前用户信息为空!");
        }
        // 根据用户权限处理要拼接的sql
        String authSql = handleDataScope(loginUser,dataScope);
        //如果要拼接的sql为空,那直接返回
        if("".equals(authSql.trim())){
            log.info("拼接的sql为空");
            return invocation.proceed();
        }
        //原sql
        String sql = mappedStatement.getBoundSql(invocation.getArgs()[1]).getSql();
        log.info("原始sql:{}",sql);
        //处理sql拼接
        this.permissionSql(sql,authSql,invocation,dataScope);
        return invocation.proceed();
    }
    
	/**
     * 获取数据权限注解信息
     *
     * @param mappedStatement
     * @return
     */
    private DataScope getPermissionByDelegate(MappedStatement mappedStatement) {
        try {
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.substring(id.lastIndexOf(".") + 1);
            final Class<?> cls = Class.forName(className);
            final Method[] method = cls.getMethods();
            for (Method me : method) {
                // 判断mapper对象的方法是否存在权限注解
                if (me.getName().equals(methodName) && me.isAnnotationPresent(DataScope.class)) {
                    return AnnotationUtils.findAnnotation(me, DataScope.class);
                }
            }
        } catch (Exception e) {
            log.error("权限注解获取失败:",e);
        }
        return null;
    }
	
	private String handleDataScope(LoginUser loginUser,DataScope controllerDataScope)
    {
        // 根据用户权限拼装sql 这里只是举例,实际情况根据自己业务来
        return "(t.deptId in (1,2))";
    }

	/**
     * 权限sql包装
     * @param sql 原sql
     * @param authSql 数据权限sql
     * @param invocation
     */
    private void permissionSql(String sql,String authSql,Invocation invocation,DataScope dataScope) {
        final Object[] args = invocation.getArgs();
        BoundSql boundSql;
        //mapper对象
        MappedStatement statement = (MappedStatement) args[0];
        Object parameterObject = args[1];
        //由于逻辑关系,只会进入一次
        if (args.length == 4) {
            //4 个参数时
            boundSql = statement.getBoundSql(parameterObject);
        } else {
            //6 个参数时
            boundSql = (BoundSql) args[5];
        }
        MappedStatement newStatement = newMappedStatement(statement, new DataScopeSqlSource(boundSql));
        MetaObject msObject = MetaObject.forObject(newStatement, new DefaultObjectFactory(),
                new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
        //sql拼接
        if(!"".equals(authSql.trim())){
            Statement stmt = null;
            // 如果转换失败 直接嵌套sql(默认查询了权限字段)
            try {
                stmt = CCJSqlParserUtil.parse(sql);
                Select select = (Select) stmt;
                SelectBody selectBody = select.getSelectBody();
                //判断是否存在where
                if (((PlainSelect) selectBody).getWhere() == null) {
                    sql = sql + " where " +authSql;
                }else {
                    sql = sql + " and " + authSql;
                }
            } catch (JSQLParserException e) {
                sql = "select * from (" + sql + ") "+ dataScope.scopeTableAlias() +" where " +authSql;
            }
            log.info("拼接sql为:{}",sql);
        }

        //sql替换
        msObject.setValue("sqlSource.boundSql.sql", sql);
        args[0] = newStatement;
        if (args.length == 6) {
            // 6个参数时
            Object parameter = args[1];
            List<ParameterMapping> additionalParameters = boundSql.getParameterMappings();
            BoundSql dataScopeBoundSql = new BoundSql(newStatement.getConfiguration(), sql, additionalParameters, parameter);
            for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                String prop = mapping.getProperty();
                if (boundSql.hasAdditionalParameter(prop)) {
                    dataScopeBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                }
            }
            args[5] = dataScopeBoundSql;
        }
    }

	/**
     * MappedStatement包装
     * @param ms
     * @param newSqlSource
     * @return
     */
    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) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        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();
    }
}

DataScopeSqlSource 类:

public class DataScopeSqlSource implements SqlSource {

    private BoundSql boundSql;

    public DataScopeSqlSource(BoundSql boundSql) {
        this.boundSql = boundSql;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        return boundSql;
    }
}

3、注册自定义的拦截器

1、使用mybatis配置文件注册,在mybatis-config.xml中写入:

	<plugins>
		<plugin interceptor="com.xxx.interceptor.DataScopeInterceptor"></plugin>
	</plugins>

2、在mybatis配置类中设置:

	@Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        sessionFactory.setPlugins(DataScopeInterceptor);
        return sessionFactory.getObject();
    }

三、自定义拦截器未生效问题

1、 多个拦截器拦截类型导致:
拦截器的调用顺序分为两大种,第一种是拦截的不同对象,例如拦截 Executor 和 拦截 StatementHandler 就属于不同的拦截对象, 这两类的拦截器在整体执行的逻辑上是不同的,在 Executor 中的 query 方法执行过程中会调用StatementHandler。

所以StatementHandler 属于 Executor 执行过程中的一个子过程。 所以这两种不同类别的插件在配置时,一定是先执行 Executor 的拦截器,然后才会轮到 StatementHandler。所以这种情况下配置拦截器的顺序就不重要了,在 MyBatis 逻辑上就已经控制了先后顺序。

2、 拦截类型都为Executor

	<plugins>
		<plugin interceptor="com.xxx.interceptor.myInterceptor1"></plugin>
		<plugin interceptor="com.xxx.interceptor.myInterceptor2"></plugin>
		<plugin interceptor="com.xxx.interceptor.myInterceptor3"></plugin>
	</plugins>

执行顺序为:3>2>1>Executor>1>2>3(1>2>3是因为使用代理之后会还会有返回)

3、引入了pagehelper

当项目引入了pagehelper之后,debug发现mapper对象中pagehelper的分页拦截器始终在最后注册,导致自定义拦截器在pagehelper之后执行,最终分页数据和总计出现偏差。

自定义拦截器在pagehelper之前执行
通过查看PageHelperAutoConfiguration源码发现它有一个@AutoConfigureAfter(MybatisAutoConfiguration.class)注解,这表明他是在MybatisAutoConfiguration加载完成后,才执行自己的加载。
PageHelperAutoConfiguration

解决办法:
1、使用spring监听器注册自定义拦截器
(1)、创建配置类

@Component
public class MyBatisInterceptorConfig implements ApplicationListener<SqlsessionEvent> {

    private static final Logger log = LoggerFactory.getLogger(MyBatisInterceptorConfig.class);

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Override
    public void onApplicationEvent(SqlsessionEvent sqlsessionEvent) {
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            // 数据权限拦截器
            sqlSessionFactory.getConfiguration().addInterceptor(new DataScopeInterceptor());
        }
        log.info("mybatis自定义拦截器:DataScopeInterceptor");
    }
}

(2)、创建监听事件类

public class SqlsessionEvent extends ApplicationContextEvent {
    public SqlsessionEvent(ApplicationContext source) {
        super(source);
    }
}

(3)、在启动类中注册监听器

public class StartApplication
{
    public static void main(String[] args){
        ConfigurableApplicationContext context = SpringApplication.run(StartApplication.class, args);
        context.publishEvent(new SqlsessionEvent(context));
    }
}

这个时候再debug时发现pagehelper的分页拦截器已经在先注册了
pagehelper先注册
2、使用自动装配@Configuration+@AutoConfigureAfter
由于PageHelperAutoConfiguration有@AutoConfigureAfter(MybatisAutoConfiguration.class),有两种方式
(1)、启动类中排除PageHelperAutoConfiguration,手动注册PageHelper分页拦截器;

排除PageHelperAutoConfiguration

@SpringBootApplication(exclude = {PageHelperAutoConfiguration.class})
@MapperScan({"a.b.*.**.mapper"})
public class StartMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(StartMainApplication .class,args);
    }
}

自动装配类MyInterceptorAutoConfig

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@AutoConfigureAfter({MybatisAutoConfiguration.class})
public class MyInterceptorAutoConfig implements InitializingBean {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Override
    public void afterPropertiesSet() {
        for (SqlSessionFactory sessionFactory : this.sqlSessionFactoryList) {
            // 数据权限拦截器
            DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
            //PageHelper 分页插件
            PageInterceptor pageInterceptor = new PageInterceptor();
            org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
            if (!this.containsInterceptor(configuration, dataScopeInterceptor)) {
                configuration.addInterceptor(dataScopeInterceptor);
            }
            if (!this.containsInterceptor(configuration, pageInterceptor)) {
                configuration.addInterceptor(pageInterceptor);
            }
        }
    }

    private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
        try {
            return configuration.getInterceptors().contains(interceptor);
        } catch (Exception var) {
            return false;
        }
    }
}

(2)、在PageHelperAutoConfiguration加载完成后再加载自定义拦截器

自动装配类MyInterceptorAutoConfig

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@AutoConfigureAfter({PageHelperAutoConfiguration.class})
public class MyInterceptorAutoConfig implements InitializingBean {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Override
    public void afterPropertiesSet() {
        for (SqlSessionFactory sessionFactory : this.sqlSessionFactoryList) {
            // 数据权限拦截器
            DataScopeInterceptor dataScopeInterceptor = new DataScopeInterceptor();
            org.apache.ibatis.session.Configuration configuration = sessionFactory.getConfiguration();
            if (!this.containsInterceptor(configuration, dataScopeInterceptor)) {
                configuration.addInterceptor(dataScopeInterceptor);
            }
        }
    }

    private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
        try {
            return configuration.getInterceptors().contains(interceptor);
        } catch (Exception var) {
            return false;
        }
    }
}

注意:此处使用@AutoConfigureAfter时,需指明该类为自动装配类,而不是用户自定义配置类。
将自定义的自动装配类放到启动类非同级包下,在MATE-INF下新建spring.factories写入以下配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.autoconfigure.MyInterceptorAutoConfig

再进行debug测试:
自定义拦截器最后注册
这时可以发现自定义拦截器在MybatisPlus插件和PageHelper之后注册的了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值