业务场景:
根据业务需要,这里将角色按照数据范围做权限限定,提供三级权限分别为:
1、全部:可以查看所有的数据
2、自定义:按照组织架构,可以查看当前所匹配的组织架构数据
3、个人:仅能查看由自己创建,或者数据流转到自己节点的数据
思路:
1、定义Mybatis拦截器DataScopeInterceptor,用于每次拦截查询sql语句,附带数据范围权限sql条件
2、定义注解DataScope,用来声明哪些操作需要做范围限制
3、springboot装配该拦截器
注:这里如果有使用MybatisPlus的分页插件,需要保证执行顺序:DataScopeInterceptor > PaginationInterceptor
步骤:
1、定义Mybatis拦截器DataScopeInterceptor
代码语言:javascript复制/**
* 数据权限拦截器
* 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
代码语言:javascript复制/**
* 数据权限校验注解
* @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"};
}
由于为了兼容每个表命名字段不一致问题,这里采用传参的方式,由业务开发人员自由传参
再定义一个枚举:
代码语言:javascript复制/**
* 数据范围枚举
* @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装配该拦截器
代码语言:javascript复制/**
* 配置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、使用
代码语言:javascript复制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)