目录
1、定义Mybatis拦截器DataScopeInterceptor
业务场景:
根据业务需要,这里将角色按照数据范围做权限限定,提供三级权限分别为:
1、全部:可以查看所有的数据
2、自定义:按照组织架构,可以查看当前所匹配的组织架构数据
3、个人:仅能查看由自己创建,或者数据流转到自己节点的数据
思路:
1、定义Mybatis拦截器DataScopeInterceptor,用于每次拦截查询sql语句,附带数据范围权限sql条件
2、定义注解DataScope,用来声明哪些操作需要做范围限制
3、springboot装配该拦截器
注:这里如果有使用MybatisPlus的分页插件,需要保证执行顺序:DataScopeInterceptor > PaginationInterceptor
步骤:
1、定义Mybatis拦截器DataScopeInterceptor
/** * 数据权限拦截器 * ALL = 全部 * CUSTOMIZE = 自定义 * SELF = 个人 * * @author Shamee * @date 2021-04-16 */ @Slf4j @AllArgsConstructor @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class DataScopeInterceptor extends AbstractSqlParserHandler implements Interceptor { final private Function<String, Map<String, Object>> function; @Override @SneakyThrows public Object intercept(Invocation invocation) throws Throwable { LOGGER.info("mybatis sql注入..."); StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); this.sqlParser(metaObject); // 先判断是不是SELECT操作 MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) { return invocation.proceed(); } BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); String originalSql = boundSql.getSql(); com.ruijie.upc.app.common.annotation.DataScope dsAnnotation = isDataScope(mappedStatement.getId()); // 不含该注解,或者注解不开启DataScope校验 if (ObjectUtil.isNull(dsAnnotation) || !dsAnnotation.isDataScope()) { return invocation.proceed(); } String[] orgScopeNames = dsAnnotation.orgScopeNames(); String[] selfScopeNames = dsAnnotation.selfScopeNames(); String userId = ShiroUtils.getCurrentUserId(); List<String> areaIds = new ArrayList<>(); DataScopeType dsType = DataScopeType.SELF; if (CollectionUtil.isEmpty(areaIds)) { //查询当前用户的 角色 最小权限 Map<String, Object> result = function.apply(userId); if (result == null) { return invocation.proceed(); } Integer dataScopeType = (Integer) result.get("dataScopeType"); dsType = DataScopeType.get(dataScopeType); areaIds = (List<String>) result.get("areaIds"); } //查全部 if (DataScopeType.ALL.equals(dsType)) { return invocation.proceed(); } //查个人 if (DataScopeType.SELF.equals(dsType)) { if(selfScopeNames != null && selfScopeNames.length > 0){ String collect = Arrays.asList(selfScopeNames).stream().map(o -> { return "temp_data_scope." + o + "='" + userId + "'"; }).collect(Collectors.joining(" or ")); originalSql = "select * from (" + originalSql + ") temp_data_scope where (" + collect + ")"; } } //查其他 else if (orgScopeNames != null && orgScopeNames.length > 0) { String join = CollectionUtil.join(areaIds, ","); String collect = Arrays.asList(selfScopeNames).stream().map(o -> { return "temp_data_scope." + o + " in (" + join + ")"; }).collect(Collectors.joining(" or ")); originalSql = "select * from (" + originalSql + ") temp_data_scope where (" + collect + ")"; } metaObject.setValue("delegate.boundSql.sql", originalSql); return invocation.proceed(); } @Override public Object plugin(Object o) { if (o instanceof StatementHandler) { return Plugin.wrap(o, this); } return o; } @Override public void setProperties(Properties properties) { } /** * 校验是否含有DataScope注解 * @param namespace * @return * @throws ClassNotFoundException */ private com.ruijie.upc.app.common.annotation.DataScope isDataScope(String namespace) throws ClassNotFoundException { if(StrUtil.isBlank(namespace)){ return null; } //获取mapper名称 String className = namespace.substring(0, namespace.lastIndexOf(".")); //获取方法名 String methodName = namespace.substring(namespace.lastIndexOf(".") + 1, namespace.length()); //获取当前mapper 的方法 Method[] methods = Class.forName(className).getMethods(); Optional<Method> first = Arrays.asList(methods).stream().filter(method -> method.getName().equals(methodName)).findFirst(); com.ruijie.upc.app.common.annotation.DataScope annotation = first.get().getAnnotation(com.ruijie.upc.app.common.annotation.DataScope.class); return annotation; } }
2、定义注解DataScope
/** * 数据权限校验注解 * @author Shamee * @date 2021-04-27 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataScope { /** * 是否开启DataScope校验,默认是 * @return */ boolean isDataScope() default true; /** * 限制范围的字段名称 (除个人外),暂时限定到省区 */ String[] orgScopeNames() default {"province_id"}; /** * 限制数据流装,范围是个人时的字段 */ String[] selfScopeNames() default {"created_by"}; }
由于为了兼容每个表命名字段不一致问题,这里采用传参的方式,由业务开发人员自由传参
再定义一个枚举:
/** * 数据范围枚举 * @author Shamee * @date 2021-04-16 */ @Getter @AllArgsConstructor @JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum DataScopeType implements IEnum<Integer> { /** * ALL="全部" */ ALL(1, "全部"), /** * CUSTOMIZE=“自定义” */ CUSTOMIZE(2, "自定义"), /** * SELF="个人" */ SELF(3, "个人") ; private Integer value; private String text; @Override public Integer getValue() { return value; } public static DataScopeType get(Integer val) { return match(val, null); } public static DataScopeType get(String val) { return match(val, null); } public static DataScopeType match(String val, DataScopeType def) { for (DataScopeType enm : DataScopeType.values()) { if (enm.name().equalsIgnoreCase(val)) { return enm; } } return def; } public static DataScopeType match(Integer val, DataScopeType def) { if (val == null) { return def; } for (DataScopeType enm : DataScopeType.values()) { if (val.equals(enm.getValue())) { return enm; } } return def; } public String getCode(){ return this.name(); } }
3、springboot装配该拦截器
/** * 配置mybatis信息 * @author Shamee * @date 2021-04-28 */ @Configuration @Slf4j public class MybatisAutoConfiguration { @Resource private List<SqlSessionFactory> sqlSessionFactoryList; /** * 这里使用构造回调的方式提高DataScope拦截器的执行顺序, * 执行顺序必须:DataScopeInterceptor> PaginationInterceptor */ @PostConstruct public void addMybatisInterceptors() { DataScopeInterceptor dataScopeInterceptor = dataScopeInterceptor(); for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) { sqlSessionFactory.getConfiguration().addInterceptor(dataScopeInterceptor); } } public DataScopeInterceptor dataScopeInterceptor() { return new DataScopeInterceptor((userId) -> SpringUtils.getBean(UserService.class).getDataScopeById(userId)); } }
注:
1、这里由于公司基础架构决定,这里没办法使用@Order来提高执行顺序,故采用@PostConstruct方式来处理,比较粗暴。
2、getDataScopeById方法为数据库按照业务规则拉取角色所匹配到的数据范围,如自定义则为匹配到的组织架构数据
4、使用
public interface SheetSpecialProjectDao extends BaseMapper<SheetSpecialProject> { @DataScope(selfScopeNames = {"created_by", "service_representative"}) IPage<SheetSpecialProjectPageDto> querySpecialProjectPage(IPage pageInput); }
5、附加说明
1、Mybatis拦截器(插件)是采用代理的方式,装载到InterceptorChain中,这里的执行顺序会与配置的顺序相反来执行,即@Order越大,越优先执行,与Spring相反。具体可以查看mybatis官网或者源码。
2、插件类型:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
我们这里需要解析sql并重组,所以使用StatementHandler。具体查看:mybatis – MyBatis 3 | 配置