拦截器
import cn.hutool.core.util.ReflectUtil;
import com.manager.enums.PositionStatusEnum;
import com.manager.util.LoginUserUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
/**
* mybatis 拦截顺序Executor -> StatementHandler->ParameterHandler->ResultSetHandler
* 要在分页插件之前完成sql语句的修改 应拦截Executor
* @author
*/
@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}),
})
public class DataAuthInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
//只拦截加注解的方法
DataAuth dataAuth = this.getDataScope(ms);
if (Objects.isNull(dataAuth)) {
return invocation.proceed();
}
//只针对投资经理岗位做数据权限,如果多岗位则不控制
List<Integer> positionStatus = LoginUserUtil.getPositionStatus();
if (CollectionUtils.isEmpty(positionStatus) || !positionStatus.contains(PositionStatusEnum.PRODUCT_MANAGER.getCodeValue())
|| (positionStatus.contains(PositionStatusEnum.PRODUCT_MANAGER.getCodeValue()) && positionStatus.size() > 1)){
return invocation.proceed();
}
if (StringUtils.isBlank(LoginUserUtil.getProductIds())){
return invocation.proceed();
}
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
//---------------这里就是拷贝的CachingExecutor或者BaseExecutor的代码
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
//-------------本来一般情况下是执行invocation.proceed(),继续执行拦截方法,但这里直接执行这个方法,相
//-------------当于替换了CachingExecutor或者BaseExecutor原来的实现。
String originalSql = boundSql.getSql();
originalSql = this.modifyOrgSql(originalSql);
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), originalSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//解决mybatis分页foreach 参数失效问题:There is no getter for property named ‘__frch_ _0’ in 'class
//得知新版需要用到additionalParameters参数,也就是说不论哪个版本,value都是从metaParameters获得,
//在老版本中使用metaParameters新版中使用了additionalParameters判断是否有值。而分页插件没有注入该参数,用ref加入该参数(注释的代码),问题解决
/* if (ReflectUtil.getFieldValue(boundSql,"additionalParameters") != null){
Object metaParameters = ReflectUtil.getFieldValue(boundSql, "additionalParameters");
ReflectUtil.setFieldValue(newBoundSql,"additionalParameters",metaParameters);
}*/
// todo ReflectUtil方法在有物理分页的情况下foreach #{item}可以正常取值,但是在没有物理分页的情况下foreach #{item}取值为null,故修改为以下方式
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
}
/**
* 通过反射获取mapper方法是否加了数据拦截注解
*/
private DataAuth getDataScope(MappedStatement mappedStatement) throws ClassNotFoundException {
DataAuth dataAuth = null;
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
final Class<?> cls = Class.forName(className);
final Method[] methods = cls.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName) && method.isAnnotationPresent(DataAuth.class)) {
dataAuth = method.getAnnotation(DataAuth.class);
break;
}
}
return dataAuth;
}
/**
* 根据权限点拼装对应sql
* @return 拼装后的sql
*/
private String modifyOrgSql(String originalSql){
return "select * from (" + originalSql + ") temp_data_scope where temp_data_scope.productId in (".concat(LoginUserUtil.getProductIds()).concat(")");
}
}
LoginUserUtil是一个线程对象,本次采用的方案是将所有的权限数据在验证token的拦截器处理好,并放入线程对象中的属性productIds,在该自定义拦截器中只需要去来直接用即可。具体的权限逻辑根据自己业务实现
自定义注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 产品权限注解
* @author
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataAuth {
}
配置自定义拦截器
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.manager.productrole.DataAuthInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.manager.dao","com.manager.module.**.dao"})
public class MybatisPlusConfig {
/**
* 分页
**/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
configuration.addInterceptor(new com.github.pagehelper.PageInterceptor());
configuration.setMapUnderscoreToCamelCase(false);
};
}
//由于拦截器加载顺序1>2>3执行顺序就会变成3>2>1,所以自定义拦截器应当放在分页拦截器之后加载
@Bean
public DataAuthInterceptor dataAuthInterceptor(){
return new DataAuthInterceptor();
}
自定义注解的使用
自定义注解使用在mapper的方法上
public interface ProductDocumentMapper extends BaseMapper<ProductDocument> {
@DataAuth
List<ProductDocumentListRsp> getList(ProductDocumentListReq productDocumentListReq);
}
总结:期间遇到这个报错:There is no getter for property named ‘__frch_ _0’ in 'class
解决方案:这个报错的问题是由于mybatis foreach标签使用报错
#{item}这个写法本身没问题,由于分页插件的问题
可以改成:${item} //有sql注入的风险,不推荐
#{list[${index}]}
最终解决方案:
修改代码费时费力,还容易出错,最终解决方案
if (ReflectUtil.getFieldValue(boundSql,"additionalParameters") != null){
Object metaParameters = ReflectUtil.getFieldValue(boundSql, "additionalParameters");
ReflectUtil.setFieldValue(newBoundSql,"additionalParameters",metaParameters);
}
参考:
如何在PageHelper之前拦截sql_a361117441的博客-优快云博客_sql拦截
There is no getter for property named ‘__frch_item_0‘ in ‘class_bbq烤鸡的博客-优快云博客
MyBatis 物理分页foreach 参数失效(list值传不进<foreach>标签为null)_jbgtwang的博客-优快云博客_foreach mybatis null
Mybatis的SQL语句拦截及修改_luxc666的博客-优快云博客_mybatis拦截sql语句
需求追加
现在某个接口需要支持两种情况,一种需要权限过滤,一种不需要
在入参对象:ProductDocumentListReq 中添加新的参数:
/**
* 是否开启 数据权限过滤 "true"开启 ,"false"关闭,不传参默认开启
*/
private String isEnableDataAuth;
同时DataAuth自定注解做修改:
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataAuth {
//是否开启数据权限,默认开启
String value() default "true";
}
新增spel表达式解析工具类:
package com.manager.productrole;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.Map;
/**
* SpEL表达式解析工具类
*
*/
@Slf4j
public class SpelExpressionResolver {
private static final ExpressionParser parser = new SpelExpressionParser();
/**
* 新增:解析SpEL表达式判断是否开启数据权限
*/
public static boolean evaluateSpElExpression(String expression, Object parameterObject) {
try {
StandardEvaluationContext context = new StandardEvaluationContext();
if (parameterObject != null) {
// 支持Map类型参数(多参数)
if (parameterObject instanceof Map) {
((Map<?, ?>) parameterObject).forEach((key, value) -> {
if (key instanceof String) {
context.setVariable((String) key, value);
}
});
} else {
// 单参数情况,使用类名首字母小写作为变量名
String variableName = getVariableName(parameterObject.getClass());
context.setVariable(variableName, parameterObject);
context.setRootObject(parameterObject);
}
}
Expression expr = parser.parseExpression(expression);
Object result = expr.getValue(context);
// 将结果转换为boolean
if (result instanceof Boolean) {
return (Boolean) result;
} else if (result instanceof String) {
return !StringUtils.equalsIgnoreCase(String.valueOf(result), "false");
}
// 如果解析结果不是boolean类型,默认开启数据权限(保持原有行为)
return true;
} catch (Exception e) {
// 解析失败时,默认开启数据权限(保持原有行为)
log.error("SpEL表达式解析失败: {} ,默认开启数据权限 ",expression,e);
return true;
}
}
/**
* 新增:获取变量名(类名首字母小写)
*/
private static String getVariableName(Class<?> clazz) {
String className = clazz.getSimpleName();
if (className.length() > 1) {
return Character.toLowerCase(className.charAt(0)) + className.substring(1);
}
return className.toLowerCase();
}
}
优化后的拦截器:
import com.manager.util.LoginUserUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.MDC;
import java.lang.reflect.Method;
import java.util.Objects;
/**
* mybatis 拦截顺序Executor -> StatementHandler->ParameterHandler->ResultSetHandler
* 要在分页插件之前完成sql语句的修改 应拦截Executor
* @author yanjj
*/
@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}),
})
public class DataAuthInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
//只拦截加注解的方法
DataAuth dataAuth = this.getDataScope(ms);
if (Objects.isNull(dataAuth)) {
return invocation.proceed();
}
// 新增:如果是SpEL表达式,则解析表达式判断是否开启数据权限
if (dataAuth.value().startsWith("#")) {
boolean enableDataAuth = SpelExpressionResolver.evaluateSpElExpression(dataAuth.value(), args[1]);
if (!enableDataAuth) {
return invocation.proceed();
}
}
String allProductAuth = RpcContext.getContext().getAttachment("allProductAuth");
String allProductAuth2 = MDC.get("allProductAuth");
if(StringUtils.isNotEmpty(allProductAuth) || StringUtils.isNotEmpty(allProductAuth2)) {
return invocation.proceed();
}
if (StringUtils.isBlank(LoginUserUtil.getProductIds())){
return invocation.proceed();
}
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if(args.length == 4){
//4 个参数时
//---------------这里就是拷贝的CachingExecutor或者BaseExecutor的代码
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
//-------------本来一般情况下是执行invocation.proceed(),继续执行拦截方法,但这里直接执行这个方法,相
//-------------当于替换了CachingExecutor或者BaseExecutor原来的实现。
String originalSql = boundSql.getSql();
originalSql = this.modifyOrgSql(originalSql);
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), originalSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
//解决mybatis分页foreach 参数失效问题:There is no getter for property named ‘__frch_ _0’ in 'class
//得知新版需要用到additionalParameters参数,也就是说不论哪个版本,value都是从metaParameters获得,
//在老版本中使用metaParameters新版中使用了additionalParameters判断是否有值。而分页插件没有注入该参数,用ref加入该参数(注释的代码),问题解决
// todo ReflectUtil方法在有物理分页的情况下foreach #{item}可以正常取值,但是在没有物理分页的情况下foreach #{item}取值为null,故修改为以下方式
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, newBoundSql);
}
/**
* 通过反射获取mapper方法是否加了数据拦截注解
*/
private DataAuth getDataScope(MappedStatement mappedStatement) throws ClassNotFoundException {
DataAuth dataAuth = null;
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
final Class<?> cls = Class.forName(className);
final Method[] methods = cls.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName) && method.isAnnotationPresent(DataAuth.class)) {
dataAuth = method.getAnnotation(DataAuth.class);
break;
}
}
return dataAuth;
}
/**
* 根据权限点拼装对应sql
* @return 拼装后的sql
*/
private String modifyOrgSql(String originalSql){
return "select * from (" + originalSql + ") temp_data_scope where temp_data_scope.productId in (".concat(LoginUserUtil.getProductIds()).concat(")");
}
}
mapper上的直接使用修改:
@DataAuth("#productListReq.isEnableDataAuth")
List<ProductListRsp> queryList(ProductListReq productListReq);
使用spel表达式要确保项目中已经添加了mvn依赖
<!-- Maven -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.xx</version>
</dependency>
<!-- 或者Gradle -->
implementation 'org.springframework:spring-expression:5.3.xx'
关于自定义注解里面 的参数:String value()而不是boolean value()
| 类型 | 优点 | 缺点 |
|---|---|---|
| String | ✅ 支持SpEL表达式 ✅ 支持动态控制 ✅ 灵活性高 ✅ 向后兼容 | ❌ 类型不够严格 |
| boolean | ✅ 类型安全 ✅ 编译期检查 | ❌ 无法支持SpEL ❌ 只能静态控制 ❌ 破坏现有功能 |
本文介绍了如何在SpringBoot应用中结合Mybatis Plus实现数据权限控制。通过拦截器处理Token验证,并将权限数据放入线程对象。详细讨论了自定义注解的创建、拦截器配置以及注解的使用方法。在实际操作中遇到了关于Mybatis分页插件的`foreach`标签报错问题,解决方案包括避免SQL注入风险的替代写法,以及参考其他博客找到的最终解决策略。

4万+

被折叠的 条评论
为什么被折叠?



