Compare commits

...

6 Commits

Author SHA1 Message Date
zengzefeng 0dcc637ece 修改resource处理 2021-02-26 14:02:39 +08:00
zengzefeng c1d213d1d8 优化部分代码,增加注释 2021-02-24 10:29:08 +08:00
zengzefeng 6023473bcd 增加注释 2021-02-23 16:54:16 +08:00
zengzefeng 696e739b0d 优化部分代码,增加具体实现demo 2021-02-23 16:51:41 +08:00
YunaiV 45d192bcce 数据权限的 code review 2020-02-23 2021-02-23 10:00:07 +08:00
zengzefeng 36146a5e71 数据权限验证初版 2021-02-22 18:16:55 +08:00
25 changed files with 1215 additions and 5 deletions

View File

@ -0,0 +1,12 @@
package cn.iocoder.dashboard.framework.dataauth.core.annotion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAuth {
}

View File

@ -0,0 +1,12 @@
package cn.iocoder.dashboard.framework.dataauth.core.annotion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAuthIgnore {
}

View File

@ -0,0 +1,280 @@
package cn.iocoder.dashboard.framework.dataauth.core.component;
import cn.iocoder.dashboard.framework.dataauth.core.component.getter.DataGetter;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthConstants;
import cn.iocoder.dashboard.framework.dataauth.core.entity.SqlHandlerResult;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.LambdaUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.reflection.property.PropertyNamer;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* 用于数据权限限制的处理器抽象类
* <p>
* 后续要需要考虑
* 1需要多张表实现限制的
* 需要JOIN sys_dept t1 xxx JOIN sys_user t2 ON t1.xx = t2.xx
* 然后在where中通过t2.xxx = xxx进行数据限制
* 2无需join表的情况即需要限制的表中直接包含指定字段只需要加入WHERE条件即可
*
* @author zzf
* @date 11:05 2021/2/4
*/
@Slf4j
public abstract class AbstractDataAuthSqlHandler<T> {
/**
* 该表对应的实体类
*
* sys_role
* sys_user
*/
protected final Class<?> entityClass;
/**
* 该表对应的实体类
*/
protected final String tableName;
/**
* 表别名
*/
protected final String tableAlias;
/**
* 关联字段名
*/
protected final String relationColumnName;
/**
* where字段名
*/
protected final String whereColumnName;
/**
* 获取数据的对象
*/
protected final DataGetter<T> dataGetter;
/**
* sql中的表达式
*/
protected final Expression operator;
protected <R> AbstractDataAuthSqlHandler(Class<?> entityClass,
String tableAlias,
DataGetter<T> dataGetter,
SFunction<R, ?> relationFieldFunction,
SFunction<R, ?> whereFieldFunction,
Expression operator) {
this.entityClass = entityClass;
this.tableAlias = tableAlias;
this.dataGetter = dataGetter;
this.operator = operator;
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
Assert.notNull(tableInfo, DataAuthConstants.notExistsTableInfo(entityClass.getSimpleName()));
this.tableName = tableInfo.getTableName();
// 获取relationColumnName
String relationFieldName = PropertyNamer.methodToProperty(LambdaUtils.resolve(relationFieldFunction).getImplMethodName());
if (relationFieldName.equals(tableInfo.getKeyProperty())) {
this.relationColumnName = tableInfo.getKeyColumn();
} else {
Optional<String> columnNameOptional = tableInfo.getFieldList().stream()
.filter(s -> s.getProperty().equals(relationFieldName))
.map(TableFieldInfo::getColumn)
.findFirst();
if (columnNameOptional.isPresent()) {
this.relationColumnName = columnNameOptional.get();
} else {
throw new IllegalArgumentException(DataAuthConstants.notExistsFieldInfo(relationFieldName));
}
}
// 获取whereFieldName
String whereFieldName = PropertyNamer.methodToProperty(LambdaUtils.resolve(whereFieldFunction).getImplMethodName());
if (whereFieldName.equals(tableInfo.getKeyProperty())) {
this.whereColumnName = tableInfo.getKeyColumn();
} else {
Optional<String> whereFiledOptional = tableInfo.getFieldList().stream()
.filter(s -> s.getProperty().equals(whereFieldName))
.map(TableFieldInfo::getColumn)
.findFirst();
if (whereFiledOptional.isPresent()) {
this.whereColumnName = whereFiledOptional.get();
} else {
throw new IllegalArgumentException(DataAuthConstants.notExistsFieldInfo(whereFieldName));
}
}
}
/**
* 进行数据权限限制的SQL处理
*/
public SqlHandlerResult doParse(Select selectStatement, Class<?> entityClass) {
T data = dataGetter.getData();
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
//判断数据是否为空且为空时是否处理
if (data == null) {
if (parseIfDataAbsent()) {
//空数据权限处理
EqualsTo equalsTo = new EqualsTo();
equalsTo.setRightExpression(new LongValue(1));
equalsTo.setLeftExpression(new LongValue(0));
//在where子句中添加 1 = 0 条件, and 原有条件
AndExpression andExpression = new AndExpression(equalsTo, selectBody.getWhere());
selectBody.setWhere(andExpression);
}
return SqlHandlerResult.nonData(parseIfDataAbsent());
} else {
if (dataIsFull(data)) {
// 拥有全部权限此处不对SQL做任何处理
return SqlHandlerResult.hadAll();
} else {
// 正常处理
Table fromItem = (Table) selectBody.getFromItem();
aliasHandle(selectBody, fromItem);
joinHandle(entityClass, selectBody, fromItem);
whereHandle(selectBody);
}
}
return SqlHandlerResult.handle();
}
/**
* 别名处理
* 如果from的表没有别名手动给他加个别名并给所有 查询列和 where条件中的列增加别名前缀
*/
public void aliasHandle(PlainSelect selectBody, Table fromItem) {
if (fromItem.getAlias() == null || fromItem.getAlias().getName() == null) {
fromItem.setAlias(new Alias(DataAuthConstants.DEFAULT_ALIAS));
selectBody.getSelectItems().forEach(s ->
s.accept(new ExpressionVisitorAdapter() {
@Override
public void visit(Column column) {
column.setTable(fromItem);
}
})
);
Expression where = selectBody.getWhere();
if (where != null) {
where.accept(new ExpressionVisitorAdapter() {
@Override
public void visit(Column column) {
column.setTable(fromItem);
}
});
}
}
}
/**
* JOIN子句处理
* <p>
* 将数据限制表通过JOIN的方式加入
*/
public void joinHandle(Class<?> entityClass, PlainSelect selectBody, Table fromItem) {
// 假如要实现 LEFT JOIN t2 ON t2.id2 = t1.id1
Table limitTable = getLimitTable();
Join join = new Join();// JOIN
join.setLeft(true);// LEFT JOIN
join.setRightItem(limitTable);// LEFT JOIN t2
EqualsTo equalsTo = new EqualsTo();// =
Column limitRelationColumn = new Column(relationColumnName);// id2
limitRelationColumn.setTable(limitTable);// t2.id2
Column targetRelationColumn = new Column(DataAuthCache.getFieldName(getId(), entityClass));// id1
targetRelationColumn.setTable(fromItem);// t1.id1
equalsTo.setLeftExpression(limitRelationColumn);
equalsTo.setRightExpression(targetRelationColumn);// t2.id2 = t1.id1
join.setOnExpression(equalsTo);// LEFT JOIN t2 ON t2.id2 = t1.id1
List<Join> joins = selectBody.getJoins();
if (joins == null) {
selectBody.setJoins(Collections.singletonList(join));
} else {
joins.add(join);
}
}
/**
* WHERE子句处理
* <p>
* 添加数据权限条件
*/
public void whereHandle(PlainSelect selectBody) {
Column whereColumn = new Column(whereColumnName);
whereColumn.setTable(getLimitTable());
setOperatorValue();
AndExpression andExpression = new AndExpression(operator, selectBody.getWhere());
selectBody.setWhere(andExpression);
}
protected Table getLimitTable() {
Table limitTable = new Table(this.tableName);
limitTable.setAlias(new Alias(this.tableAlias));
return limitTable;
}
/**
* SqlHandler唯一标识用于{@link DataAuthStrategy 中使用}
*/
abstract public int getId();
/**
* 如果数据为空是否处理SQL
* <p>
* 如果为true则表示返回空数据系统将增加 1 = 0条件
* 如果为false则不改变sql
*/
protected abstract boolean parseIfDataAbsent();
/**
* 插入数据限制条件的值
* {@link #operator} {@code operator.setRightExpression(getData())}
* 这个方法可以写成默认的对所有类型进行遍历并实现所有类型的插入方法
* 但是好麻烦要考虑 = IN LIKE(三种) 等表达式还有不同的数据类型balabala一大堆
*/
protected abstract void setOperatorValue();
/**
* 判断该数据是否属于全部权限数据
*
* @param data 数据权限数据
*/
protected abstract boolean dataIsFull(T data);
}

View File

@ -0,0 +1,130 @@
package cn.iocoder.dashboard.framework.dataauth.core.component;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthConstants;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.LambdaUtils;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.http.util.Asserts;
import org.apache.ibatis.reflection.property.PropertyNamer;
import java.util.*;
/**
* 添加数据权限限制配置的辅助工具
*
* @author zzf
* @date 2021/2/4 15:35
*/
@Accessors(chain = true)
public class DataAuthCacheHelper {
/**
* 该表对应的实体类
*/
@Setter
private Set<Class<?>> targetEntityClassSet;
/**
* 数据权限限制处理器
*/
@Setter
private AbstractDataAuthSqlHandler<?> sqlHandler;
/**
* 对象表 用于与数据权限限制的表关联 的字段名
*/
private String targetFieldName;
/**
* 保存上面三个参数的对应关系map
* sqlHandlerId : targetClass : fieldName map
* <p>
* 用于在处理SQL时根据对应关系修改SQL
*/
private final Map<AbstractDataAuthSqlHandler<?>, Map<Class<?>, String>> sqlHandler2TargetClass2FieldNameMap = new HashMap<>();
public DataAuthCacheHelper addTargetEntityClass(Class<?> targetEntityClass) {
if (this.targetEntityClassSet == null) {
this.targetEntityClassSet = new HashSet<>();
}
this.targetEntityClassSet.add(targetEntityClass);
return this;
}
public DataAuthCacheHelper addTargetEntityClass(Collection<Class<?>> targetEntityClassList) {
if (this.targetEntityClassSet == null) {
this.targetEntityClassSet = new HashSet<>();
}
this.targetEntityClassSet.addAll(targetEntityClassList);
return this;
}
public <T> DataAuthCacheHelper setTargetFieldName(SFunction<T, ?> function) {
targetFieldName = PropertyNamer.methodToProperty(LambdaUtils.resolve(function).getImplMethodName());
return this;
}
/**
* 绑定前三个参数的信息生成 sqlHandler2TargetClass2FieldNameMap 数据
*/
public DataAuthCacheHelper bind() {
valueCheck();
Map<Class<?>, String> targetClass2FieldNameMap = new HashMap<>();
targetEntityClassSet.forEach(entityClass -> {
//直接从MP的缓存中获取实体类对应的表信息查询实体类字段对应的表字段名
TableInfo tableInfo = TableInfoHelper.getTableInfo(entityClass);
//TableInfo中 主键和其他字段分别存储因此这里要分别判断
if (targetFieldName.equals(tableInfo.getKeyProperty())) {
targetClass2FieldNameMap.put(entityClass, tableInfo.getKeyColumn());
} else {
Optional<String> relationColumnOptional = tableInfo.getFieldList().stream()
.filter(s -> s.getProperty().equals(targetFieldName))
.map(TableFieldInfo::getColumn)
.findFirst();
if (relationColumnOptional.isPresent()) {
targetClass2FieldNameMap.put(entityClass, relationColumnOptional.get());
} else {
throw new IllegalArgumentException(DataAuthConstants.notExistsFieldInfo(targetFieldName));
}
}
});
sqlHandler2TargetClass2FieldNameMap.put(sqlHandler, targetClass2FieldNameMap);
reset();
return this;
}
/**
* 前三个参数数据验证
*/
public void valueCheck() {
Asserts.notNull(this.targetEntityClassSet, "targetEntityClassList can not be null");
Asserts.notNull(this.sqlHandler, "sqlHandlerList can not be null");
Asserts.notNull(this.targetFieldName, "targetFieldName can not be null");
}
/**
* 重置前三个参数的数据
*/
public void reset() {
targetEntityClassSet = null;
sqlHandler = null;
targetFieldName = null;
}
/**
* 加入到缓存中
*/
public void cache() {
sqlHandler2TargetClass2FieldNameMap.forEach((key, val) -> {
DataAuthCache.addSqlHandler(key);
DataAuthCache.addSqlHandlerId2TargetClass2FieldNameMap(key.getId(), val);
});
}
}

View File

@ -0,0 +1,95 @@
package cn.iocoder.dashboard.framework.dataauth.core.component;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache;
import cn.iocoder.dashboard.framework.dataauth.core.util.ResourceUtils;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.Select;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.Set;
import java.util.TreeSet;
/**
* 数据权限拦截器
*
* @author zzf
* @date 11:05 2021/2/4
*/
@Slf4j
public class DataAuthInterceptor implements InnerInterceptor {
//限制策略
private final DataAuthStrategy dataAuthStrategy;
private final Set<String> needAuthId = new TreeSet<>();
private final Set<String> notNeedAuthId = new TreeSet<>();
public DataAuthInterceptor(DataAuthStrategy dataAuthStrategy) {
this.dataAuthStrategy = dataAuthStrategy;
}
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
String id = ms.getId();
String resource = ResourceUtils.getResourceKey(ms.getResource());
String methodName;
if (id.contains(".")) {
methodName = id.substring(id.lastIndexOf(".") + 1);
} else {
methodName = id;
}
if (needAuthCheck(resource, methodName, id)) {
log.warn("sql before parsed: " + boundSql.getSql());
Class<?> entityClass = DataAuthCache.getEntityClassByResource(resource);
try {
Select selectStatement = (Select) CCJSqlParserUtil.parse(boundSql.getSql());
dataAuthStrategy.doParse(selectStatement, entityClass);
ReflectUtil.setFieldValue(boundSql, "sql", selectStatement.toString());
log.warn("sql after parsed: " + boundSql.getSql());
} catch (JSQLParserException e) {
throw new SQLException("sql role limit fail: sql parse fail.");
}
}
}
/**
* 判断是否需要数据权限限制
*/
private boolean needAuthCheck(String resource, String methodName, String id) {
if (needAuthId.contains(id)) {
return true;
}
if (notNeedAuthId.contains(id)) {
return false;
}
boolean result;
if (DataAuthCache.isNeedAuthResource(resource)) {
result = !DataAuthCache.isNotNeedAuthMethod(resource, methodName);
} else {
result = DataAuthCache.isNeedAuthMethod(resource, methodName);
}
if (result) {
needAuthId.add(id);
} else {
notNeedAuthId.add(id);
}
return result;
}
}

View File

@ -0,0 +1,61 @@
package cn.iocoder.dashboard.framework.dataauth.core.component;
import cn.iocoder.dashboard.framework.dataauth.core.annotion.DataAuth;
import cn.iocoder.dashboard.framework.dataauth.core.annotion.DataAuthIgnore;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache;
import cn.iocoder.dashboard.framework.dataauth.core.util.ResourceUtils;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
/**
* 数据权限限制SqlInjector
* <p>
* 重写mp的mapper扫描逻辑
* 在扫描mapper后检查是否需要数据权限限制并缓存这些信息
*
* @author zzf
* @date 2021/2/5 15:02
*/
@Slf4j // TODO FROM 芋艿 to zzf类名可能需要在想想哈 DONE
public class DataAuthSqlInjector extends DefaultSqlInjector {
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
String resource = ResourceUtils.getResourceKey(mapperClass);
// TODO FROM 芋艿 to zzf复数 DONE
Set<String> needAuthMethodSet = new HashSet<>();
Set<String> notNeedAuthMethodSet = new HashSet<>();
if (mapperClass.isAnnotationPresent(DataAuth.class)) { // TODO FROM 芋艿 to zzf可以考虑使用 isAnnotationPresent 方法简洁 DONE
DataAuthCache.addNeedAuthResource(resource);
for (Method method : mapperClass.getMethods()) {
needAuthMethodSet.add(method.getName());
}
}
for (Method method : mapperClass.getMethods()) {
if (method.isAnnotationPresent(DataAuth.class)) {
needAuthMethodSet.add(method.getName());
}
if (method.isAnnotationPresent(DataAuthIgnore.class)) {
notNeedAuthMethodSet.add(method.getName());
}
}
if (needAuthMethodSet.size() > 0) {
DataAuthCache.addNeedAuthMethod(resource, needAuthMethodSet);
DataAuthCache.addResource2EntityClassMap(resource, extractModelClass(mapperClass));
}
if (notNeedAuthMethodSet.size() > 0) {
DataAuthCache.addNotNeedAuthMethod(resource, notNeedAuthMethodSet);
}
super.inspectInject(builderAssistant, mapperClass);
}
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.dashboard.framework.dataauth.core.component;
import net.sf.jsqlparser.statement.select.Select;
/**
* 数据权限限制的处理策略
* 比如
* 先通过岗位限制如果岗位不合适再通过角色限制再通过其他字段限制等等
*
* 每种限制方式应该有对应的{@link AbstractDataAuthSqlHandler}
*
* @author zzf
* @date 17:54 2021/1/29
*/
public interface DataAuthStrategy {
void doParse(Select selectStatement, Class<?> entityClass);
}

View File

@ -0,0 +1,14 @@
package cn.iocoder.dashboard.framework.dataauth.core.component.getter;
/**
* 获取数据抽象对象
*
* @author zzf
* @date 2021/2/18 11:05
*/
public interface DataGetter<R> {
R getData();
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.dashboard.framework.dataauth.core.component.getter;
import java.util.function.Supplier;
/**
* 通过静态方法获取数据权限限制数据
*
* @author zzf
* @date 2021/2/18 11:05
*/
public class StaticDataGetter<R> implements DataGetter<R> {
private final Supplier<R> supplier;
public StaticDataGetter(Supplier<R> supplier) {
this.supplier = supplier;
}
@Override
public R getData() {
return supplier.get();
}
}

View File

@ -0,0 +1,26 @@
package cn.iocoder.dashboard.framework.dataauth.core.component.getter;
import java.util.function.Function;
/**
* 通过执行对象方法获取数据权限限制数据
*
* @author zzf
* @date 2021/2/18 11:05
*/
public class TargetDataGetter<T, R> implements DataGetter<R> {
private final Function<T, R> function;
private final T target;
public TargetDataGetter(T target, Function<T, R> function) {
this.function = function;
this.target = target;
}
@Override
public R getData() {
return function.apply(target);
}
}

View File

@ -0,0 +1,143 @@
package cn.iocoder.dashboard.framework.dataauth.core.entity;
import cn.iocoder.dashboard.framework.dataauth.core.component.AbstractDataAuthSqlHandler;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import org.apache.ibatis.mapping.MappedStatement;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 数据权限限制的缓存信息存储类
* TODO FROM 芋艿 to zzf最好不要暴露原始的数据结构出去 DONE
*
* @author zzf
* @date 2021/2/5 15:02
*/
public class DataAuthCache {
/**
* 需要权限限制的resource集合 from {@link MappedStatement#getResource()}
*/
private final static Set<String> needAuthResourceSet = new HashSet<>();
/**
* 需要权限限制的 resource:方法名集合 map
*/
private final static Map<String, Set<String>> needAuthMethodMap = new HashMap<>();
/**
* 不需要权限限制的 resource:方法名集合 map
*/
private final static Map<String, Set<String>> notNeedAuthMethodMap = new HashMap<>(32);
/**
* resource:对应表实体类Class对象 map
*/
private final static Map<String, Class<?>> resource2EntityClassMap = new HashMap<>(32);
/**
* 数据权限限制sql处理器
*/
private final static Map<Integer, AbstractDataAuthSqlHandler<?>> sqlHandlerMap = new HashMap<>(8);
/**
* sql处理器id : 需要限制的表对应的类 : 该表用于 与数据权限限制的表做表关联 的列名
*/
private final static Map<Integer, Map<Class<?>, String>> sqlHandlerId2TargetClass2FieldNameMap = new HashMap<>(8);
/**
* 添加需要权限限制的resource
*/
public static void addNeedAuthResource(String needAuthResource) {
needAuthResourceSet.add(needAuthResource);
}
/**
* 是否是 需要权限限制的resource
*/
public static boolean isNeedAuthResource(String resource) {
return needAuthResourceSet.contains(resource);
}
/**
* 添加需要权限限制的resource:方法名
*/
public static void addNeedAuthMethod(String resource, Set<String> methodSet) {
needAuthMethodMap.put(resource, methodSet);
}
/**
* 是否是 需要权限限制的方法
*/
public static boolean isNeedAuthMethod(String resource, String methodName) {
Set<String> needAuthMethod = DataAuthCache.needAuthMethodMap.get(resource);
return ObjectUtils.isNotEmpty(needAuthMethod) && needAuthMethod.contains(methodName);
}
/**
* 添加需要权限限制的resource:方法名
*/
public static void addNotNeedAuthMethod(String resource, Set<String> methodSet) {
notNeedAuthMethodMap.put(resource, methodSet);
}
/**
* 是否是 不需要权限限制的方法
*/
public static boolean isNotNeedAuthMethod(String resource, String methodName) {
Set<String> methodSet = DataAuthCache.notNeedAuthMethodMap.get(resource);
return ObjectUtils.isNotEmpty(methodSet) && methodSet.contains(methodName);
}
/**
* 添加 resource: 对应实体类类对象 关联关系
*/
public static void addResource2EntityClassMap(String resource, Class<?> entityClass) {
resource2EntityClassMap.put(resource, entityClass);
}
/**
* 通过resource获取其对应实体类
*/
public static Class<?> getEntityClassByResource(String resource) {
return resource2EntityClassMap.get(resource);
}
/**
* 添加数据权限限制sql处理器
*/
public static void addSqlHandler(AbstractDataAuthSqlHandler<?> sqlHandler) {
sqlHandlerMap.put(sqlHandler.getId(), sqlHandler);
}
/**
* 添加数据权限限制sql处理器id获取对应处理器
*/
public static AbstractDataAuthSqlHandler<?> getSqlHandlerById(Integer id) {
return sqlHandlerMap.get(id);
}
/**
* 添加数据权限限制sql处理器
*/
public static void addSqlHandlerId2TargetClass2FieldNameMap(Integer sqlHandlerId,
Map<Class<?>, String> targetClass2RelationColumnMap) {
sqlHandlerId2TargetClass2FieldNameMap.put(sqlHandlerId, targetClass2RelationColumnMap);
}
/**
* 根据sqlHandlerId和targetEntityClass 获取 其用于表关联的列名
*
* @param sqlHandlerId SqlHandler的id
* @param targetEntityClass 需要限制的表对应的类对象
* @return 该表用于 与限制表做表关联的字段名
*/
public static String getFieldName(Integer sqlHandlerId, Class<?> targetEntityClass) {
Map<Class<?>, String> classStringMap = sqlHandlerId2TargetClass2FieldNameMap.get(sqlHandlerId);
return classStringMap.get(targetEntityClass);
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.dashboard.framework.dataauth.core.entity;
import org.slf4j.helpers.MessageFormatter;
/**
* 数据权限限制常量类
*
* @author zzf
* @date 2021/2/22 14:21
*/
public interface DataAuthConstants {
String DEFAULT_ALIAS = "df_als";
String ERROR_MSG_NO_TABLE_INFO = "entity class [{}] TableInfo cannot be null, make sure MP can load that mapper.";
String ERROR_MSG_NO_FIELD_INFO = "can not found field [{}] corresponding field name.";
static String notExistsTableInfo(String entityClassName) {
return MessageFormatter.format(ERROR_MSG_NO_TABLE_INFO, entityClassName).getMessage();
}
static String notExistsFieldInfo(String fieldName) {
return MessageFormatter.format(ERROR_MSG_NO_FIELD_INFO, fieldName).getMessage();
}
}

View File

@ -0,0 +1,69 @@
package cn.iocoder.dashboard.framework.dataauth.core.entity;
import lombok.Getter;
import lombok.Setter;
/**
* 数据权限sql解析后的结果
*
* @author zzf
* @date 2021/2/5 8:48
*/
@Setter
@Getter
public class SqlHandlerResult {
/**
* 是否拥有全部权限
*/
private Boolean hadAll;
/**
* 是否没有权限
*/
private Boolean hadNon;
/**
* 是否解析过sql
* <p>
* hadAll = true || hadNon = true时
* 即使不需要修改sqlparsed = true
*/
private Boolean parsed;
public static SqlHandlerResult hadAll() {
SqlHandlerResult result = new SqlHandlerResult();
result.setHadAll(true);
result.setHadNon(false);
result.setParsed(false);
return result;
}
public static SqlHandlerResult hadNon() {
SqlHandlerResult result = new SqlHandlerResult();
result.setHadAll(false);
result.setHadNon(true);
result.setParsed(true);
return result;
}
public static SqlHandlerResult nonData(boolean parsed) {
SqlHandlerResult result = new SqlHandlerResult();
result.setHadAll(false);
result.setHadNon(parsed);
result.setParsed(parsed);
return result;
}
public static SqlHandlerResult handle() {
SqlHandlerResult result = new SqlHandlerResult();
result.setHadAll(false);
result.setHadNon(false);
result.setParsed(true);
return result;
}
private SqlHandlerResult() {
}
}

View File

@ -0,0 +1,58 @@
package cn.iocoder.dashboard.framework.dataauth.core.util;
import com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder;
import org.apache.ibatis.builder.annotation.MapperAnnotationBuilder;
/**
* mybatis mapper resource相关工具
* <p>
* 在mybatis中每个mapper方法对应一个字符串类型的resource
* resource的值有两种生成方式
* 1当该方法没有对应的xml实现时{@link MapperAnnotationBuilder} at line 109 , {@link MybatisMapperAnnotationBuilder} at line 84
* 2当该方法有对应的xml实现时{@link MapperAnnotationBuilder} at line 166, {@link MybatisMapperAnnotationBuilder} at line 161
* 这两种我们都需要拦截为了方便后续操作我们直接统一处理
*
* @author zzf
* @date 2021/2/26 11:40
*/
public class ResourceUtils {
private static final String MAPPER_SUFFIX = ".java (best guess)";
private static final String XML_SUFFIX = ".xml";
/**
* 根据mybatis提供的resource 获取 DataAuth过程中需要的resourceKey
*
* @param resource 根据mybatis提供的resource
* @return DataAuth过程中需要的resourceKey
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthInterceptor
*/
public static String getResourceKey(String resource) {
String fileSeparator = System.getProperties().getProperty("file.separator");
resource = resource.replace("\\", fileSeparator);
if (resource.contains(MAPPER_SUFFIX)) {
if (resource.contains(fileSeparator)) {
resource = resource.substring(resource.lastIndexOf(fileSeparator) + 1);
}
return resource.substring(0, resource.indexOf(MAPPER_SUFFIX));
} else if (resource.contains(XML_SUFFIX)) {
if (resource.contains(fileSeparator)) {
resource = resource.substring(resource.lastIndexOf(fileSeparator) + 1);
}
return resource.substring(0, resource.indexOf(XML_SUFFIX));
}
throw new IllegalArgumentException("invalid resource: " + resource);
}
/**
* 根据mapper类 获取 DataAuth过程中需要的resourceKey
*
* @param mapperClass mapper类类对象
* @return DataAuth过程中需要的resourceKey
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthSqlInjector
*/
public static String getResourceKey(Class<?> mapperClass) {
return mapperClass.getSimpleName();
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.dashboard.framework.dataauth.impl;
import cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthCacheHelper;
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 初始化添加数据权限限制配置
*
* @author zzf
* @date 2020/12/21 11:45
*/
@Component
public class DataAuthCacheInit implements ApplicationListener<ContextRefreshedEvent> {
@Resource
private SysPermissionService permissionService;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
DataAuthCacheHelper helper = new DataAuthCacheHelper()
.setSqlHandler(new MyRoleDataAuthSqlHandler(permissionService))
.addTargetEntityClass(SysUserDO.class)
.setTargetFieldName(SysUserDO::getId)
.bind();
helper.cache();
}
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.dashboard.framework.dataauth.impl;
import cn.iocoder.dashboard.framework.dataauth.core.component.AbstractDataAuthSqlHandler;
import cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthStrategy;
import cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache;
import net.sf.jsqlparser.statement.select.Select;
/**
* 自定义数据权限sql处理策略类
*
* @author zzf
* @date 17:54 2021/1/29
*/
public class MyDataAuthStrategy implements DataAuthStrategy {
@Override
public void doParse(Select selectStatement, Class<?> entityClass) {
AbstractDataAuthSqlHandler<?> dataAuthSqlHandler = DataAuthCache.getSqlHandlerById(SqlHandlerIdEnum.ROLE.getId());
dataAuthSqlHandler.doParse(selectStatement, entityClass);
}
}

View File

@ -0,0 +1,72 @@
package cn.iocoder.dashboard.framework.dataauth.impl;
import cn.iocoder.dashboard.framework.dataauth.core.component.AbstractDataAuthSqlHandler;
import cn.iocoder.dashboard.framework.dataauth.core.component.getter.TargetDataGetter;
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.ValueListExpression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.schema.Column;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 角色数据权限限制sql处理器
*
* @author zzf
* @date 2021/2/19 15:58
*/
public class MyRoleDataAuthSqlHandler extends AbstractDataAuthSqlHandler<Set<Long>> {
public MyRoleDataAuthSqlHandler(SysPermissionService permissionService) {
super(SysUserDO.class,
"dr_usr",
new TargetDataGetter<>(permissionService, SysPermissionService::getCurrentUserDataPermission),
SysUserDO::getId,
SysUserDO::getDeptId,
new InExpression());
}
@Override
public int getId() {
return SqlHandlerIdEnum.ROLE.getId();
}
@Override
protected boolean parseIfDataAbsent() {
return false;
}
@Override
protected void setOperatorValue() {
InExpression operator = (InExpression) this.operator;
Column whereColumn = new Column(whereColumnName);
whereColumn.setTable(getLimitTable());
operator.setLeftExpression(whereColumn);
ValueListExpression listExpression = new ValueListExpression();
Set<Long> data = dataGetter.getData();
if (data != null) {
List<Expression> expressions = new ArrayList<>();
data.forEach(s -> {
LongValue stringValue = new LongValue(s);
expressions.add(stringValue);
});
listExpression.setExpressionList(new ExpressionList(expressions));
operator.setRightExpression(listExpression);
}
}
@Override
protected boolean dataIsFull(Set<Long> data) {
return false;
}
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.dashboard.framework.dataauth.impl;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限sql处理器id枚举类
*
* @author zzf
* @date 2021/2/19 15:51
*/
@Getter
@AllArgsConstructor
public enum SqlHandlerIdEnum {
ROLE(1);
final int id;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.dashboard.framework.dataauth;
/**
* 数据权限限制SQL自动注入
* <p>
* 1利用mp的mapper扫描将带有需要限制和不需要限制的类/方法信息缓存
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthSqlInjector
* <p>
* 2利用mp的拦截器对执行的方法拦截并通过1中的信息判断是否需要进行限制
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthInterceptor
* <p>
* 3配置数据权限限制的处理器以及处理策略
* @see cn.iocoder.dashboard.framework.dataauth.core.component.AbstractDataAuthSqlHandler
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthStrategy
* <p>
* 4在系统初始化完成后加载权限限制的表字短信并全局缓存
* @see cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthCacheHelper
* @see cn.iocoder.dashboard.framework.dataauth.core.entity.DataAuthCache
* <p>
*
* 使用
* 1配置
* @see cn.iocoder.dashboard.framework.dataauth.impl.DataAuthCacheInit
* @see cn.iocoder.dashboard.framework.dataauth.impl.MyDataAuthStrategy
* @see cn.iocoder.dashboard.framework.dataauth.impl.MyRoleDataAuthSqlHandler
* 2添加@DataAuth注解
* @see cn.iocoder.dashboard.modules.system.dal.mysql.user.SysUserMapper#selectPage(cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserPageReqVO, java.util.Collection)
* 3登录后请求对应接口即可, 日志中有输出前后sql
*/

View File

@ -1,5 +1,9 @@
package cn.iocoder.dashboard.framework.mybatis.config; package cn.iocoder.dashboard.framework.mybatis.config;
import cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthInterceptor;
import cn.iocoder.dashboard.framework.dataauth.core.component.DataAuthSqlInjector;
import cn.iocoder.dashboard.framework.dataauth.impl.MyDataAuthStrategy;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -19,8 +23,19 @@ public class MybatisConfiguration {
@Bean @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 // 分页插件
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 数据权限限制
mybatisPlusInterceptor.addInnerInterceptor(new DataAuthInterceptor(new MyDataAuthStrategy()));
return mybatisPlusInterceptor; return mybatisPlusInterceptor;
} }
/**
* 数据权限限制中 用于扫描mapper加载需要做权限限制的数据的类/方法
*/
@Bean
public ISqlInjector sqlInjector() {
return new DataAuthSqlInjector();
}
} }

View File

@ -1,6 +1,8 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.user; package cn.iocoder.dashboard.modules.system.dal.mysql.user;
import cn.iocoder.dashboard.common.pojo.PageResult; import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.dataauth.core.annotion.DataAuth;
import cn.iocoder.dashboard.framework.dataauth.core.annotion.DataAuthIgnore;
import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserExportReqVO; import cn.iocoder.dashboard.modules.system.controller.user.vo.user.SysUserExportReqVO;
@ -27,6 +29,7 @@ public interface SysUserMapper extends BaseMapperX<SysUserDO> {
return selectOne(new QueryWrapper<SysUserDO>().eq("email", email)); return selectOne(new QueryWrapper<SysUserDO>().eq("email", email));
} }
@DataAuth
default PageResult<SysUserDO> selectPage(SysUserPageReqVO reqVO, Collection<Long> deptIds) { default PageResult<SysUserDO> selectPage(SysUserPageReqVO reqVO, Collection<Long> deptIds) {
return selectPage(reqVO, new QueryWrapperX<SysUserDO>() return selectPage(reqVO, new QueryWrapperX<SysUserDO>()
.likeIfPresent("username", reqVO.getUsername()) .likeIfPresent("username", reqVO.getUsername())

View File

@ -106,4 +106,11 @@ public interface SysPermissionService extends SecurityPermissionFrameworkService
*/ */
void processUserDeleted(Long userId); void processUserDeleted(Long userId);
/**
* 获取当前用户的数据权限
*
* @return 当前用户的数据权限
*/
Set<Long> getCurrentUserDataPermission();
} }

View File

@ -48,6 +48,15 @@ public interface SysRoleService {
*/ */
List<SysRoleDO> listRolesFromCache(Collection<Long> ids); List<SysRoleDO> listRolesFromCache(Collection<Long> ids);
/**
* 获得指定角色的数据权限集合
*
* @param ids 角色编号数组
* @return 角色数据权限数据
*/
Set<Long> listDataScopeDeptIdsByRoleIdsFromCache(Collection<Long> ids);
/** /**
* 判断角色数组中是否有管理员 * 判断角色数组中是否有管理员
* *

View File

@ -249,6 +249,12 @@ public class SysPermissionServiceImpl implements SysPermissionService {
// TODO 实现我 // TODO 实现我
} }
@Override
public Set<Long> getCurrentUserDataPermission() {
Set<Long> roleIds = SecurityUtils.getLoginUserRoleIds();
return roleService.listDataScopeDeptIdsByRoleIdsFromCache(roleIds);
}
@Override @Override
public boolean hasPermission(String permission) { public boolean hasPermission(String permission) {
return hasAnyPermissions(permission); return hasAnyPermissions(permission);

View File

@ -2,6 +2,7 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum; import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.dashboard.common.pojo.PageResult; import cn.iocoder.dashboard.common.pojo.PageResult;
@ -11,8 +12,8 @@ import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRole
import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRolePageReqVO;
import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleUpdateReqVO; import cn.iocoder.dashboard.modules.system.controller.permission.vo.role.SysRoleUpdateReqVO;
import cn.iocoder.dashboard.modules.system.convert.permission.SysRoleConvert; import cn.iocoder.dashboard.modules.system.convert.permission.SysRoleConvert;
import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMapper;
import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO; import cn.iocoder.dashboard.modules.system.dal.dataobject.permission.SysRoleDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.permission.SysRoleMapper;
import cn.iocoder.dashboard.modules.system.enums.permission.RoleCodeEnum; import cn.iocoder.dashboard.modules.system.enums.permission.RoleCodeEnum;
import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum; import cn.iocoder.dashboard.modules.system.enums.permission.SysRoleTypeEnum;
import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer; import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysRoleProducer;
@ -53,7 +54,7 @@ public class SysRoleServiceImpl implements SysRoleService {
/** /**
* 角色缓存 * 角色缓存
* key角色编号 {@link SysRoleDO#getId()} * key角色编号 {@link SysRoleDO#getId()}
* * <p>
* 这里声明 volatile 修饰的原因是每次刷新时直接修改指向 * 这里声明 volatile 修饰的原因是每次刷新时直接修改指向
*/ */
private volatile Map<Long, SysRoleDO> roleCache; private volatile Map<Long, SysRoleDO> roleCache;
@ -137,6 +138,30 @@ public class SysRoleServiceImpl implements SysRoleService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public Set<Long> listDataScopeDeptIdsByRoleIdsFromCache(Collection<Long> ids) {
Set<Long> dataScopeDeptIdSet = new HashSet<>();
if (ObjectUtil.isEmpty(ids)) {
return dataScopeDeptIdSet;
}
// TODO FROM 芋艿 to zzf是不是遍历 ids roleCache 获取会好点因为一个人拥有的角色少
// roleCache.values() = List<SysRoleDO>, 遍历ids没法直接与之比较还是得遍历roleCache.values()才能比较
roleCache.values()
.forEach(roleDO -> {
if (ids.contains(roleDO.getId())) {
dataScopeDeptIdSet.addAll(roleDO.getDataScopeDeptIds());
}
});
//TODO 模拟数据后续删除
if (dataScopeDeptIdSet.size() == 0) {
dataScopeDeptIdSet.add(1L);
dataScopeDeptIdSet.add(2L);
dataScopeDeptIdSet.add(3L);
dataScopeDeptIdSet.add(4L);
}
return dataScopeDeptIdSet;
}
@Override @Override
public boolean hasAnyAdmin(Collection<SysRoleDO> roleList) { public boolean hasAnyAdmin(Collection<SysRoleDO> roleList) {
if (CollectionUtil.isEmpty(roleList)) { if (CollectionUtil.isEmpty(roleList)) {
@ -237,13 +262,13 @@ public class SysRoleServiceImpl implements SysRoleService {
/** /**
* 校验角色的唯一字段是否重复 * 校验角色的唯一字段是否重复
* * <p>
* 1. 是否存在相同名字的角色 * 1. 是否存在相同名字的角色
* 2. 是否存在相同编码的角色 * 2. 是否存在相同编码的角色
* *
* @param name 角色名字 * @param name 角色名字
* @param code 角色额编码 * @param code 角色额编码
* @param id 角色编号 * @param id 角色编号
*/ */
private void checkDuplicateRole(String name, String code, Long id) { private void checkDuplicateRole(String name, String code, Long id) {
// 1. name 名字被其它角色所使用 // 1. name 名字被其它角色所使用