Springboot Mybatis底层权限体系架构

Springboot Mybatis底层权限体系架构

通常项目都会有数据权限隔离的要求,比如:只能看本部门的数据、只能看经手过的数据、只能看分管条线的数据。比较简单的处理就是封装一些处理权限的方法,业务代码里每次查询数据的时候都调用这些封装的方法。当然你也可以不封装,每次查询都写一遍权限相关的逻辑。

我这里介绍一种更底层的封装方法,对业务代码的侵入较小。

1. 业务和情况介绍

业务需求千变万化,我的项目数据权限大概分为2种情况,2种情况是或关系

  • 按部门隔离。默认只能查看自己部门、兼职部门、分管部门的数据
  • 按业务分类隔离。如果分管某项业务,就可以查看这项业务的所有数据

项目初期没有规划这样的权限体系,关键字段命名也没有做约定,这给改造添加了一点点难度:

  • 要用的字段是部门、课程分类,原则上2个都是树形结构
  • 字段名字可能不一样:比如部门,有的是dept_id,有的是org_id
  • 存储方式可能不一样,有的表直接存了其中1个或2个字段,有个存了关联关系的字段

2. 整体思路

结合spring切面、MyBatis拦截器来实现

  • MyBatis支持自定义拦截器,拦截器提供了修改sql的方法
  • 定义一个切面,通过注解传递参数给切面用以处理字段名和存储方式的问题

拦截器要实现Mybatis的Interceptor接口,在拦截器里获取到的信息有限,所以我借助了切面。

  • 在切面里获取当前登录人的权限信息,并存入缓存
  • 在拦截器读取缓存里的权限信息,拼接权限sql

最终的sql是:

前置sql where (业务sql) and (部门隔离sql or 产品隔离sql or 自定义sql)

3. 切面实现

定义注解DataSegregate,有3个参数:

  • Org:部门id对应表别名和字段
  • ProductType: 产品分类id对应表别名和字段
  • SomeThing: 其他自定义sql
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSegregate {
    String Org() default "";
    String ProductType() default "";
    String SomeThing() default "";
}

定义切面DataSegregateAspect。在这里读取当前用户的权限:是否具有所有数据权限、权限模块配置的部门、权限系统配置的产品、兼职的部门,并存入缓存。 我这个项目使用的是session,所以直接存session了。各位看官如果参考我的方法,可以考虑几个优化点:

  • 存session改为存redis或者其他缓存工具,更为灵活和通用
  • 现在是每次查询都触发读取最新权限配置,消耗了系统算力。可以改为权限配置调整、兼职调整的时候触发
@Aspect
@Component
public class DataSegregateAspect {
    @Resource private IOrgService orgService;
    @Resource private IPriConfigService priConfigService;
    @Resource private IRoleService roleService;

    @Pointcut("@annotation(com.master.app.config.DataSegregate)")
    public void DataSegregatePointCut() {}

    @Before("DataSegregatePointCut()")
    public void doBefore(JoinPoint point) throws Throwable {
        handleDataScope(point);
    }

    protected void handleDataScope(final JoinPoint joinPoint) {
        //获得注解
        DataSegregate DataSegregate = getAnnotationByJoinPoint(joinPoint);
        if (DataSegregate == null) {
            return;
        }
        //所有权限判断。在拦截器判断所有数据权限
        String allDataRoleNames = "能查看所有数据的角色";
        ServiceResult<Boolean> allDataFlag = roleService.checkUserWithRoleNames(allDataRoleNames);
        Map map = new HashMap();
        if(allDataFlag.getData()){
            //设置所有权限
            map.put(SessionConst.CURRENT_USER_PRI_ALL_FLAG, true);
        }

        //读取当前用户权限
        List<PriConfig> priConfigList = priConfigService.getListByCurUser().getData();
        //配置部门
        List<String> configOrgList = priConfigList.stream().filter(u -> "org".equals(u.getTargetType())).map(PriConfig::getTargetId).collect(Collectors.toList());
        //兼职部门
        List<String> positionOrgList = orgService.getOrgListByCurrentUser().getData().stream().map(Org::getOrgId).collect(Collectors.toList());
        //配置产品
        List<String> productTypeList = priConfigList.stream().filter(u -> "productType".equals(u.getTargetType())).map(PriConfig::getTargetId).collect(Collectors.toList());
        

        map.put(SessionConst.CURRENT_USER_PRI_ORG_LIST, configOrgList);
        map.put(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST, positionOrgList);
        map.put(SessionConst.CURRENT_USER_POSITION_ORG_LIST, productTypeList);
        UserUtil.setUserDataSegregate(map);
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private DataSegregate getAnnotationByJoinPoint(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(DataSegregate.class);
        }
        return null;
    }

}

4. Mybatis拦截器实现

新建一个拦截器DataSegregateInterceptor。大致思路是把权限隔离的逻辑加入到sql里,最终的sql是:

前置sql where (业务sql) and (部门隔离sql or 产品隔离sql or 自定义sql)
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataSegregateInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        V_UserWithPosition curUser = UserUtil.getCurrentUser();
        if(curUser == null){
            return invocation.proceed();
        }
        // 拿到mybatis的一些对象
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String clazzName = mappedStatement.getId().substring(0, mappedStatement.getId().lastIndexOf("."));
        String methodName = mappedStatement.getId().substring(mappedStatement.getId().lastIndexOf(".") + 1, mappedStatement.getId().length());
        //判断所有数据权限
        Map priConfig = UserUtil.getUserDataSegregate();
        boolean allPrivilegeFlag = false;
        if(priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG) != null){
            allPrivilegeFlag = (Boolean)priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG);
        }
        if(allPrivilegeFlag){
            return invocation.proceed();
        }
        //是否开启数据权限
        Class<?> clazz = Class.forName(clazzName);
        DataSegregate annotation = null;
        //遍历方法
        for (Method method : clazz.getDeclaredMethods()) {
            //方法是否含有DataPermission注解,如果含有注解则将数据结果过滤
            if(method.getName().equals(methodName)){
                annotation = method.getAnnotation(DataSegregate.class);
            }
        }
        if (annotation == null){
            return invocation.proceed();
        }

        String sql = statementHandler.getBoundSql().getSql();

        // 解析并返回新的SQL语句,只处理查询sql
        if (mappedStatement.getSqlCommandType().toString().equals("SELECT")) {
            sql = getSql(sql, annotation, priConfig);
        }
        // 修改sql
        metaObject.setValue("delegate.boundSql.sql", sql);

        return invocation.proceed();

    }

    /**
     * 解析SQL语句,并返回新的SQL语句
     */
    private String getSql(String sql, DataSegregate annotation, Map priConfig) throws Exception{
        String condition = "";
        List<String> configOrgList = new ArrayList<>();
        List<String> positionOrgList = new ArrayList<>();
        List<String> productTypeList = new ArrayList<>();
        boolean allPrivilegeFlag = false;
        String orgAlia = annotation.Org();
        String productTypeAlias = annotation.ProductType();
        String someThing = annotation.SomeThing();

        if(priConfig.get(SessionConst.CURRENT_USER_PRI_ORG_LIST) != null){
            configOrgList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_PRI_ORG_LIST);
        }
        if(priConfig.get(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST) != null){
            productTypeList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_PRI_PRODUCT_TYPE_LIST);
        }
        if(priConfig.get(SessionConst.CURRENT_USER_POSITION_ORG_LIST) != null){
            positionOrgList = (List<String>)priConfig.get(SessionConst.CURRENT_USER_POSITION_ORG_LIST);
        }
        if(priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG) != null){
            allPrivilegeFlag = (Boolean)priConfig.get(SessionConst.CURRENT_USER_PRI_ALL_FLAG);
        }
        if(!allPrivilegeFlag && (configOrgList==null || configOrgList.size() == 0)
                && (productTypeList==null || productTypeList.size() == 0)
                && (positionOrgList==null || positionOrgList.size() == 0)
        ){
            throw new RuntimeException("您无权限查看数据!");
        }
        List<String> orgIdList = new ArrayList<>();
        if(configOrgList != null){
            orgIdList.addAll(configOrgList);
        }
        if(positionOrgList != null){
            orgIdList.addAll(positionOrgList);
        }

        StringBuilder sqlParams = new StringBuilder();
        orgIdList.forEach(p -> {
            sqlParams.append(String.format("'%s',", p));
        });
        if(sqlParams.length() > 0 && !StringUtils.isEmpty(orgAlia)){
            String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
            condition += (String.format(" %s in ( %s ) ", orgAlia, idStr));
        }
        sqlParams.setLength(0);
        productTypeList.forEach(p -> {
            sqlParams.append(String.format("'%s',", p));
        });
        if(sqlParams.length() > 0 && !StringUtils.isEmpty(productTypeAlias)){
            String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
            if(condition.length() == 0)
                condition = " and ";
            else
                condition += " or ";
            condition += (String.format(" %s in ( %s ) ", productTypeAlias, idStr));
        }

        //todo 添加自定义sql。自定义sql与校区、分类是OR关系
        if (!StringUtils.isEmpty(someThing) && sqlParams.length() > 0) {
            String idStr = sqlParams.delete(sqlParams.length() - 1, sqlParams.length()).toString();
            if(condition.length() > 0) condition += " or ";
            condition += " " + someThing.replaceAll("#org_id", idStr);
        }


        if (StringUtils.isEmpty(condition)) {
            throw new RuntimeException("您无权限查看数据!");
        }
        condition = "(" + condition + ")";
        Select select = (Select) CCJSqlParserUtil.parse(sql);
        PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
        //取得原SQL的where条件
        final Expression expression = plainSelect.getWhere();
        //增加新的where条件
        final Expression envCondition = CCJSqlParserUtil.parseCondExpression(condition);
        if (expression == null) {
            plainSelect.setWhere(envCondition);
        } else {
            AndExpression andExpression = new AndExpression(expression, envCondition);
            plainSelect.setWhere(andExpression);
        }
        return plainSelect.toString();
    }

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

使用

在需要数据隔离的mapper层方法上添加注解即可

@DataSegregate(Org = "o.org_id", ProductType = "c.product_type")
List<Map<String, Object>> findSomeThing(Map<String, Object> map);

其他

有不足之处或者其他思路,欢迎交流

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot中使用MyBatis实现数据权限控制的方法可以通过自定义拦截器来实现。首先,你可以创建一个自定义拦截器,比如命名为PowerInterceptor。这个拦截器需要实现Interceptor接口,并且使用@Intercepts注解来指定拦截的方法和参数。在这个拦截器中,你可以通过重写intercept方法来实现对SQL语句的处理。 在intercept方法中,你可以获取到方法的调用信息,并且可以对SQL进行修改或者添加额外的条件。比如,你可以在该方法中获取到StatementHandler对象,并通过调用getBoundSql方法来获取到SQL语句。然后,你可以根据自定义的数据权限规则来修改SQL语句,添加相应的条件。最后,通过调用invocation.proceed()方法来继续执行下一个拦截器或者目标方法。 为了将自定义拦截器应用到MyBatis中,你需要在拦截器中实现Plugin接口,并将其配置为一个Bean。在plugin方法中,你可以判断被代理对象是否是StatementHandler类型的,如果是的话,就使用Plugin.wrap方法对其进行包装,并将自定义拦截器传入。最后,你需要将该拦截器配置到MyBatis的配置文件中或者使用@MapperScan注解进行扫描。 这样,当MyBatis执行SQL语句时,自定义拦截器将会被触发,并对SQL进行相应的处理,从而实现数据权限控制。通过这种方式,你不需要在每个SQL语句中添加权限判断条件,避免了代码冗余和维护困难的问题。 希望以上信息对你有帮助!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值