mybatis拦截器+CCJSqlParser实现解耦数据权限

mybatis拦截器+CCJSqlParser实现解耦数据权限
转载:https://www.cnblogs.com/gongdianpeng/p/12532542.html
前言

从工作以来经手了好多个从0-1的项目,所以也写了很多很多次权限相关的代码,但每次的数据权限实现都不理想,每接入一个新的功能页面都要针对各个接口进行数据过滤,由其是一些不清楚权限设计的同学想写个功能,还要去弄明白权限的那一堆事才可以,然后过滤的逻辑就会耦合在各个业务代码中合,简直就是被代码支配的恐惧。那有什么好的办法能做好解耦呢
方案与实现

数据权限无非就是通过sql,过滤掉无权限的数据,以及拦截那些无权限数据的操作。那我们直接拦截sql,将拦截语句拼装进去不就行啦~

使用自定义注解来标识哪些Mapper方法需要被拦截
创建mybatis intercept拦截器,检测方法是否有自定义注解
使用CCJSqlParser解析并改写SQL
覆盖原SQL

数据表设计

CREATE TABLE data_group_ref (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
data_id bigint(20) NOT NULL DEFAULT ‘0’ COMMENT ‘数据id’,
data_type tinyint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘数据类型’,
group_id bigint(20) NOT NULL DEFAULT ‘0’ COMMENT ‘用户组id’,

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘数据与用户组的对应关系表’;

在小数据量下,我们只需要一个数据与用户组的关系表即可。

data_id 对应数据表的主键id
data_type 是数据类型标识,因为系统可能会有很多数据表需要做权限,每个表都创一个关系表,就会出现表爆炸,所以全放一个关系表里用type来区分就好
group_id 是对应的用户组主键id ,每一个数据可以对应多个组,那就会有多条记录

自定义注解

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAuthSelect {
//数据类型, data_group_ref表中的 data_type 字段值
int type() default 0;
}

这里我只列出type用来告知拦截器需要过滤的数据类型,大家可以根据自己的业务进行扩展
拦截器

针对拦截器的原理,网上有好多资料,我就不再反复阐述,大家可参考
https://blog.csdn.net/weixin_39494923/article/details/91534658

@Intercepts({@Signature(method = “query”, type = Executor.class,
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
@Component
public class DataAuthSelectIntercept implements Interceptor {

private static final Logger logger = LoggerFactory.getLogger(DataAuthSelectIntercept.class);

@Autowired
HttpServletRequest request;

@Override
public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
    //没自定义注解直接按通过算
    DataAuthSelect dataAuth = getDataAuth(mappedStatement);
    if (dataAuth == null) {
        return invocation.proceed();
    }
    //没登录是异常
    UserInfo user = (UserInfo) request.getSession().getAttribute("userInfo");
    if (user == null) {
        throw new Exception("获取用户登录信息失败");
    }
    //超级管理员不过滤
    if (user.isSuperAdmin()) {
        return invocation.proceed();
    }

    //根据自己的业务写更多的过滤判断.....

    //如果获取用户组失败,或者为全部权限,则直接通过
    String groupStr = getGroupStr(user);
    if (StringUtils.isBlank(groupStr)) {
        return invocation.proceed();
    }
    //拼装sql(这里是关键!!!)
    BoundSql boundSql = mappedStatement.getBoundSql(invocation.getArgs()[1]); 
    String orgSql = boundSql.getSql(); //获取到当前需要被执行的SQL
    String authSql = makeSql(orgSql, groupStr, dataAuth); //进行数据权限过滤组装 
    //替换
    MappedStatement newStatement = newMappedStatement(mappedStatement, new BoundSqlSqlSource(boundSql));
    MetaObject msObject = MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory());
    msObject.setValue("sqlSource.boundSql.sql", authSql);
    invocation.getArgs()[0] = newStatement;
    logger.debug("baseSql:{} authSql:{}", orgSql, authSql);
    return invocation.proceed();
}

/**
* 通过反射获取mapper方法是否加了自定义注解
*/
private DataAuthSelect getDataAuth(MappedStatement mappedStatement) throws ClassNotFoundException {
    DataAuthSelect dataAuth = null;
    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[] methods = cls.getMethods();
    for (Method method : methods) {
        if (method.getName().equals(methodName) && method.isAnnotationPresent(DataAuthSelect.class)) {
            dataAuth = method.getAnnotation(DataAuthSelect.class);
            break;
        }
    }
    return dataAuth;
}

/**
* 获取当前登录的用户配置的用户组信息 (group_id)
*/
private String getGroupStr(UserInfo user) {
    //分局自己的业务逻辑实现
    return "1,2,3,4,5";
}

/**
* 核心代码: 将原SQL 进行解析并拼装 一个子查询  id in ( 数据权限过滤SQL ) 
*/
private String makeSql(String sql, String groupStr, DataAuthSelect dataAuth) throws JSQLParserException {
    CCJSqlParserManager parserManager = new CCJSqlParserManager();
    Select select = (Select) parserManager.parse(new StringReader(sql));
    PlainSelect plain = (PlainSelect) select.getSelectBody();
    Table fromItem = (Table) plain.getFromItem();
    //有别名用别名,无别名用表名,防止字段冲突报错
    String mainTableName = fromItem.getAlias() == null ? fromItem.getName() : fromItem.getAlias().getName();
    //构建子查询
    String dataAuthSql = mainTableName + ".id in ( select data_id from data_group_ref where " + mainTableName + ".id = data_id and data_type = " + dataAuth.type() + " and group_id in (" + groupStr + ")" + ")";
    if (plain.getWhere() == null) {
        plain.setWhere(CCJSqlParserUtil.parseCondExpression(dataAuthSql));
    } else {
        plain.setWhere(new AndExpression(plain.getWhere(), CCJSqlParserUtil.parseCondExpression(dataAuthSql)));
    }
    return select.toString();
}

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();
}


private class BoundSqlSqlSource implements SqlSource {
    private BoundSql boundSql;

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

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

至此我们已经完成了数据权限所有的代码编写。
一起看下原始sql与拼装后sql的区别

select id,name from test where is_deleted = 0
select id,name from test where is_deleted = 0 and id in ( select data_id from data_group_ref where test.id = data_id and data_type = 1 and group_id in (“1,2,3”) )

为什么使用子查询不用join? 因为子查询兼容更强,需要改写的sql更少,如果用join 原sql是单查询,那组装时还要对 select的字段进行加别名,能少做点事就少做点吧~
使用与规范

让我们一起来体验下解耦后的快了吧,我们只需一行代码,在对应需要做权限的mapper中加一个注解!
对应的sql就会自动被增加数据权限的过滤,不需要了解权限的设计,不需要自己反复的拼接那条一样的sql。

@Select(value = "SELECT * FROM lcp_rule WHERE id = #{id} and is_deleted = 0")
@DataAuthSelect(type = 1)
Rule selectByIdAuth(Serializable id);

虽然我们只定义了对sql的拦截,但是我们却能够实现 列表、查看、编辑、删除 等等常见数据权限限制!
列表与查看不用多说,本身就是查询。 编辑与删除,是update与delete ,我们虽然没有拦截,但通常在进行修改与删除之前,我们需要进行一个 getbyid的查询来确保数据存在。一但进行这个查询,不就又命中我们的拦截,如果返回空数据,c层直接返回无权限即可~

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值