利用mybatis-plus和切面实现数据权限控制

AI2周前发布 beixibaobao
12 0 0

前言:主要是通过在查询sql的方法增加注解标识该sql适用的权限控制字段,以及在切面中通过获取当前登录用户的数据权限范围,将数据权限范围和sql适用的权限控制字段通过上下文传递到mybatis-plus的sql拦截器中,动态生成数据权限的条件,将其拼接到查询sql中。

一、定义权限注解,上下文和切面

1、权限注解,这里可以根据自身业务需要进行调整

import java.lang.annotation.*;
/**
 * 数据权限注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DataScope {
    /**
     * 部门字段别名(表字段名)
     */
    String orgAlias() default "";
    /**
     * 用户字段别名(表字段名,支持多个字段,用“:”隔开)
     */
    String userAlias() default "";
    /**
     * 项目字段别名(表字段名)
     * @return
     */
    String projectAlias() default "";
}

2、上下文封装

/**
 * 数据权限范围上下文封装
 */
@Data
public class DataScopeContext {
    private final String orgAlias;
    private final String userAlias;
    private final String projectAlias;
    public DataScopeContext(String orgAlias, String userAlias, String projectAlias) {
        this.orgAlias = orgAlias;
        this.userAlias = userAlias;
        this.projectAlias = projectAlias;
    }
}
/**
 * 数据权限上下文
 */
public class DataScopeHolder {
    //存储权限信息
    private static final ThreadLocal<List<DataScopeDTO>> PERMISSION_HOLDER = new ThreadLocal<>();
    //存储注解配置
    private static final ThreadLocal<DataScopeContext> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setPermissions(List<DataScopeDTO> permissions) {
        PERMISSION_HOLDER.set(permissions);
    }
    public static List<DataScopeDTO> getPermissions() {
        return PERMISSION_HOLDER.get();
    }
    public static void setContext(DataScopeContext context) {
        CONTEXT_HOLDER.set(context);
    }
    public static DataScopeContext getContext() {
        return CONTEXT_HOLDER.get();
    }
    public static void clear() {
        PERMISSION_HOLDER.remove();
        CONTEXT_HOLDER.remove();
    }
}

3、数据权限DTO 根据自身业务需求进行调整

@Data
public class DataScopeDTO {
    /**
     * 数据权限类型 1 人员  2 组织  3 项目
     */
    private String type;
    /**
     * 范围类型 1 仅本人 2 指定人员 3 指定采购组 4 仅本人所在组织 5 指定组织 6 全部组织 7 指定项目 8 指定项目类型 9 指定项目所属组织 10 全部项目
     */
    private String scopeType;
    /**
     * 范围列表
     */
    private List<String> scopeList;
}
[{
    "type":1,
    "scopeType":1,
    "scopeList": []
},
{
    "type":2,
    "scopeType":5,
    "scopeList": ["123","234","456"]
}]
/**
 * 数据权限类型枚举
 */
@Getter
public enum DataScopeTypeEnum {
    PERSONAL("person_data_scope", "人员数据权限"),
    ORG("org_data_scope", "组织数据权限"),
    PROJECT("project_data_scope", "项目数据权限"),
    ;
    private String code;
    private String name;
    DataScopeTypeEnum(String code, String name){
        this.code = code;
        this.name = name;
    }
}
/**
 * 范围类型枚举
 */
@Getter
public enum ScopeTypeEnum {
    ONLY_SELF("1", "仅本人"),
    APPOINT_PERSON("2", "指定人员"),
    APPOINT_GROUP("3", "指定采购组"),
    ONLY_SELF_ORG("4", "仅本人所在组织"),
    APPOINT_ORG("5", "指定组织"),
    ALL_ORG("6", "全部组织"),
    APPOINT_PROJECT("7", "指定项目"),
    APPOINT_PROJECT_TYPE("8", "指定项目类型"),
    APPOINT_PROJECT_ORG("9", "指定项目所属组织"),
    ALL_PROJECT("10", "全部项目"),
    ONLY_SELF_ORG_AND_CHILDREN("11", "仅本人所在组织及子组织"),
    ONLY_SELF_ORG_AND_PARENT("12", "仅本人所在组织及以上"),
    ALL_PERSON("13", "所有人员"),
    ;
    private String code;
    private String name;
    ScopeTypeEnum(String code, String name){
        this.code = code;
        this.name = name;
    }
}

4、切面

这里获取用户数据权限是通过工具类实现,可以结合自身业务需求进行调整。

/**
 * 数据权限范围切面
 */
@Component
@Aspect
@RefreshScope
public class DataScopeAspect {
    @Autowired
    private SystemProperties systemProperties;
    public DataScopeAspect() {
    }
    @Pointcut("@annotation(cn.iiot.myth.starter.common.annotation.DataScope)")
    public void dataScopePointCut(){
    }
    @Around("dataScopePointCut()")
    public Object handleDataScope(ProceedingJoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        DataScope dataScope = null;
        if(Objects.nonNull(method)){
            dataScope = method.getAnnotation(DataScope.class);
        }
        try {
            if(Objects.nonNull(dataScope)){
                // 获取当前用户的数据权限信息
                UserBaseInfo userBaseInfo = AuthUtils.getUser();
                //白名单不做数据权限控制
                if(CollUtil.isNotEmpty(systemProperties.getAdminList()) && systemProperties.getAdminList().contains(userBaseInfo.getUid())){
                    return joinPoint.proceed();
                }
                //获取用户的数据权限集 自行实现
                List<DataScopeDTO> resultList = UserDataScopeUtil.getUserDataScope();
                DataScopeHolder.setPermissions(resultList);
                //存储注解配置信息
                DataScopeContext context = DataScopeHolder.getContext();
                if(Objects.nonNull( context)){
                    context = new DataScopeContext(dataScope.orgAlias(), dataScope.userAlias(), dataScope.projectAlias());
                }
                DataScopeHolder.setContext(context);
            }
            return joinPoint.proceed();
        } finally {
            // 4. 清除上下文
            DataScopeHolder.clear();
        }
    }
}

二、拦截器定义

1、sql拦截器,主要用于动态构建权限sql

public interface QueryInterceptor extends Ordered {
    /**
     * 拦截处理
     *
     * @param executor
     * @param ms
     * @param parameter
     * @param rowBounds
     * @param resultHandler
     * @param boundSql
     */
    void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql);
    /**
     * 排序
     *
     * @return int
     */
    @Override
    default int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}
@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})
})
@Slf4j
public class DataScopeInterceptor implements QueryInterceptor {
    /**
     * 动态构建权限 SQL
     *
     * @param originSql        原始 SQL
     * @param deptAlias        部门字段别名(从注解获取)
     * @param userAlias        用户字段别名(从注解获取)
     * @param dataScopeDTOList 组织权限列表
     * @param projectAlias     项目字段别名(从注解获取)
     */
    private String buildPermissionSql(String originSql, String deptAlias, String userAlias,
                                      String projectAlias,List<DataScopeDTO> dataScopeDTOList) {
        StringBuilder finalSql = new StringBuilder(originSql);
        // 动态构建条件
        StringBuilder conditions = new StringBuilder(" 1=1 AND (");
        Boolean shouldOr = false;
        // 人员权限条件
        for(DataScopeDTO dataScopeDTO : dataScopeDTOList){
            log.info("权限类型:{}", JSONUtil.toJsonStr(dataScopeDTO));
            if(DataScopeTypeEnum.PERSONAL.getCode().equals(dataScopeDTO.getType()) && StrUtil.isNotBlank(userAlias)){
                String[] userAliasArr = userAlias.split(StrUtil.COLON);
                for(String userAliasItem : userAliasArr){
                    if (Objects.nonNull(dataScopeDTO) && CollUtil.isNotEmpty(dataScopeDTO.getScopeList())
                            && StrUtil.isNotBlank(userAliasItem) && dataScopeDTO.getScopeList().contains(OrmConstants.ALL)){
                        String personCondition = String.format(" %s 1 = 1 "
                                ,shouldOr ? "OR" : "");
                        conditions.append(personCondition);
                        shouldOr = true;
                    } else if (Objects.nonNull(dataScopeDTO) && CollUtil.isNotEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(userAliasItem)) {
                        if (dataScopeDTO.getScopeList().size() > 200) {
                            // 使用临时表语法处理大量数据
                            StringBuilder tempTableSql = new StringBuilder();
                            tempTableSql.append(shouldOr ? " OR " : "")
                                    .append(userAliasItem)
                                    .append(" IN (SELECT id FROM (VALUES ");
                            List<String> scopeList = dataScopeDTO.getScopeList();
                            for (int i = 0; i < scopeList.size(); i++) {
                                if (i > 0) {
                                    tempTableSql.append(", ");
                                }
                                tempTableSql.append("ROW('").append(scopeList.get(i)).append("')");
                            }
                            tempTableSql.append(") AS t(id))");
                            conditions.append(tempTableSql);
                        } else {
                            // 原有的 IN 语法
                            String personCondition = String.format(" %s %s IN ('%s') ",
                                    shouldOr ? "OR" : "",
                                    userAliasItem,
                                    String.join("','", dataScopeDTO.getScopeList()));
                            conditions.append(personCondition);
                        }
                        shouldOr = true;
                    } else if (Objects.nonNull(dataScopeDTO) && CollUtil.isEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(userAliasItem)) {
                        String personCondition = String.format(" %s %s != %s "
                                ,shouldOr ? "OR" : "",userAliasItem,userAliasItem);
                        conditions.append(personCondition);
                        shouldOr = true;
                    }
                }
            }else if(DataScopeTypeEnum.ORG.getCode().equals(dataScopeDTO.getType())){
                // 部门权限条件
                if (Objects.nonNull(dataScopeDTO) && CollUtil.isNotEmpty(dataScopeDTO.getScopeList())
                        && StrUtil.isNotBlank(deptAlias) && dataScopeDTO.getScopeList().contains(OrmConstants.ALL)){
                    String personCondition = String.format(" %s 1 = 1 "
                            ,shouldOr ? "OR" : "");
                    conditions.append(personCondition);
                    shouldOr = true;
                } else if (Objects.nonNull(dataScopeDTO) && !StrUtil.equals(ScopeTypeEnum.ALL_ORG.getCode(),dataScopeDTO.getScopeType())
                        && CollUtil.isNotEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(deptAlias)) {
                    if (dataScopeDTO.getScopeList().size() > 200) {
                        // 使用临时表语法处理大量数据
                        StringBuilder tempTableSql = new StringBuilder();
                        tempTableSql.append(shouldOr ? " OR " : "")
                                .append(deptAlias)
                                .append(" IN (SELECT id FROM (VALUES ");
                        List<String> scopeList = dataScopeDTO.getScopeList();
                        for (int i = 0; i < scopeList.size(); i++) {
                            if (i > 0) {
                                tempTableSql.append(", ");
                            }
                            tempTableSql.append("ROW('").append(scopeList.get(i)).append("')");
                        }
                        tempTableSql.append(") AS t(id))");
                        conditions.append(tempTableSql);
                    } else {
                        // 原有的 IN 语法
                        String personCondition = String.format(" %s %s IN ('%s') ",
                                shouldOr ? "OR" : "",
                                deptAlias,
                                String.join("','", dataScopeDTO.getScopeList()));
                        conditions.append(personCondition);
                    }
                    shouldOr = true;
                }else if(Objects.nonNull(dataScopeDTO) && !StrUtil.equals(ScopeTypeEnum.ALL_ORG.getCode(),dataScopeDTO.getScopeType())
                        && CollUtil.isEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(deptAlias)){
                    String personCondition = String.format(" %s %s != %s "
                            ,shouldOr ? "OR" : "",deptAlias,deptAlias);
                    conditions.append(personCondition);
                    shouldOr = true;
                }
            }else if(DataScopeTypeEnum.PROJECT.getCode().equals(dataScopeDTO.getType())){
                // 项目权限条件
                if (Objects.nonNull(dataScopeDTO) && CollUtil.isNotEmpty(dataScopeDTO.getScopeList())
                        && StrUtil.isNotBlank(projectAlias) && dataScopeDTO.getScopeList().contains(OrmConstants.ALL)){
                    String personCondition = String.format(" %s 1 = 1 "
                            ,shouldOr ? "OR" : "");
                    conditions.append(personCondition);
                    shouldOr = true;
                } else if(Objects.nonNull(dataScopeDTO) && !StrUtil.equals(ScopeTypeEnum.ALL_PROJECT.getCode(), dataScopeDTO.getScopeType())
                        && CollUtil.isNotEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(projectAlias)){
                    if (dataScopeDTO.getScopeList().size() > 200) {
                        // 使用临时表语法处理大量数据
                        StringBuilder tempTableSql = new StringBuilder();
                        tempTableSql.append(shouldOr ? " OR " : "")
                                .append(projectAlias)
                                .append(" IN (SELECT id FROM (VALUES ");
                        List<String> scopeList = dataScopeDTO.getScopeList();
                        for (int i = 0; i < scopeList.size(); i++) {
                            if (i > 0) {
                                tempTableSql.append(", ");
                            }
                            tempTableSql.append("ROW('").append(scopeList.get(i)).append("')");
                        }
                        tempTableSql.append(") AS t(id))");
                        conditions.append(tempTableSql);
                    } else {
                        // 原有的 IN 语法
                        String personCondition = String.format(" %s %s IN ('%s') ",
                                shouldOr ? "OR" : "",
                                projectAlias,
                                String.join("','", dataScopeDTO.getScopeList()));
                        conditions.append(personCondition);
                    }
                }else if (Objects.nonNull(dataScopeDTO) && !StrUtil.equals(ScopeTypeEnum.ALL_PROJECT.getCode(), dataScopeDTO.getScopeType())
                        && CollUtil.isEmpty(dataScopeDTO.getScopeList()) && StrUtil.isNotBlank(projectAlias)){
                    String personCondition = String.format(" %s %s != %s "
                            ,shouldOr ? "OR" : "",projectAlias,projectAlias);
                    conditions.append(personCondition);
                    shouldOr = true;
                }
            }
        }
        if(CollUtil.isEmpty(dataScopeDTOList)){
            String alias = deptAlias;
            if(StrUtil.isBlank(alias) && StrUtil.isNotBlank(userAlias)){
                alias = userAlias.split(StrUtil.COLON)[0];
            }
            if(StrUtil.isBlank(alias) && StrUtil.isNotBlank(projectAlias)){
                alias = projectAlias;
            }
            String sql = String.format(" %s != %s ",alias, alias);
            conditions.append(sql);
        }
        conditions.append(" )");
        // 智能插入 WHERE 位置
        return insertWhereClause(finalSql.toString(), conditions.toString());
    }
    /**
     * 智能插入 WHERE 条件
     */
    private String insertWhereClause(String sql, String condition) {
        String regex = "(?i)\bwhere\b";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(sql);
        // 找到 WHERE 关键词位置
        if (matcher.find()) {
            int position = matcher.end();
            return sql.substring(0, position) + " " + condition + " AND " + sql.substring(position);
        } else {
            // 没有 WHERE 关键词则添加
            int groupByPos = sql.toUpperCase().indexOf("GROUP BY");
            int orderByPos = sql.toUpperCase().indexOf("ORDER BY");
            int insertPos = Math.min(
                    groupByPos != -1 ? groupByPos : Integer.MAX_VALUE,
                    orderByPos != -1 ? orderByPos : Integer.MAX_VALUE
            );
            if (insertPos != Integer.MAX_VALUE) {
                return sql.substring(0, insertPos) + " WHERE " + condition + " " + sql.substring(insertPos);
            } else {
                return sql + " WHERE " + condition;
            }
        }
    }
    @Override
    public void intercept(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        //检查是否设置了权限上下文
        if (DataScopeHolder.getPermissions() == null) {
            return;
        }
        //从上下文获取当前注解配置
        DataScopeContext context = DataScopeHolder.getContext();
        if (context == null) {
            return;
        }
        //动态构建 SQL
        String finalSql = buildPermissionSql(
                boundSql.getSql(),
                context.getOrgAlias(),
                context.getUserAlias(),
                context.getProjectAlias(),
                DataScopeHolder.getPermissions()
        );
        //替换原始 SQL
        ReflectUtils.setFieldValue(boundSql, "sql", finalSql);
    }
}

2、装配拦截器

  /**
     * 分页拦截器
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public MybatisPlusInterceptor mybatisPlusInterceptor(ObjectProvider<QueryInterceptor[]> queryInterceptors, OrmConfigProperties ormConfigProperties) {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 配置分页拦截器
        MythPaginationInterceptor paginationInterceptor = new MythPaginationInterceptor();
        // 配置自定义查询拦截器
        QueryInterceptor[] queryInterceptorArray = new QueryInterceptor[1];
        //数据权限拦截器
        queryInterceptorArray[0] = new DataScopeInterceptor();
        AnnotationAwareOrderComparator.sort(queryInterceptorArray);
        paginationInterceptor.setQueryInterceptors(queryInterceptorArray);
        paginationInterceptor.setMaxLimit(ormConfigProperties.getPageLimit());
        paginationInterceptor.setOptimizeJoin(ormConfigProperties.getOptimizeJoin());
        interceptor.addInnerInterceptor(paginationInterceptor);
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

三、使用

在对应查询sql的方法上添加对应注解

如原始sql: select id,name,code,org_code,create_by from order_bill where code = 'xxx' ;

拼接后的sql会是

select id,name,code,org_code,create_by from order_bill where code = 'xxx' and 1=1 and ( org_code in ('123','321') or create_by in ('10011','10022')) ;

    @DataScope(orgAlias = "org_code",userAlias = "t.create_by")
    @Override
    public <T> PageResult<T> pageList(QueryParam req, Class<T> tClass) {
        MPJLambdaWrapper<Example> queryWrapper = wrapper.getWrapper(req,  tClass);
        IPage<T> page = new Page<>(req.getPageNo(), req.getLimit());
        IPage<T> result = ExampleMapper.selectJoinPage(page,tClass, queryWrapper);
        if (null == result) {
            return null;
        }
        return PageResult.<T>builder().records(result.getRecords()).total(result.getTotal()).build();
    }
© 版权声明

相关文章