基于mybatis拦截器实现数据权限
mybatis拦截器实现数据权限
需求场景:
业务要求对列表查询功能添加数据权限控制。
比如:给用户A针对某些订单模块设置一些查询条件,比如地址 = 山东,运输方式 = 空运,商品大类 = 家电。当用户访问订单模块时,只能访问满足这些条件的数据。
需求分析:
要实现动态条件拼接, 需要在执行查询前获取要执行的SQL,然后解析SQL,将现有的条件组装成SQL语句拼接到原有SQL中再执行新的SQL。
解决方案:
1.control层方法上添加自定义注解。目的是用来获取当前用户配置的权限数据。
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;
/**
*
*
* 项目名称:ff-flatcar
* 类名称:Qcym
* 类描述: 定制一个接口,用来获取当前用户对应的数据权限数据
* 创建人:wuwenjin
* 创建时间:2022年5月16日 上午9:51:27
* 修改人:wuwenjin
* 修改时间:2022年5月16日 上午9:51:27
* 修改备注:
* @version
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Qcym {
String value() default "数据权限注解";
}
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 类名称:QcymAspect
* 类描述: 接口Qcym的实现类
* 创建人:wuwenjin
* 创建时间:2022年5月16日 上午9:55:05
* 修改人:wuwenjin
* 修改时间:2022年5月16日 上午9:55:05
* 修改备注:
* @version 1.0
*
*/
@Aspect
@Component
public class QcymAspect {
//获取用户数据权限service
@Resource
private DataAuthorityTestDataService dataAuthorityTestDataService;
/**
* cutMethod(设置切点)
* @return void
* @author wuwenjin
* @date 2022年5月16日 上午9:57:34
* @Exception 异常对象
* @version 1.0
*/
@Pointcut("@annotation(com.dataauth.freight.utils.aspect.Qcym)")
public void cutMethod() {
}
// @Before("cutMethod()&& args(qcym)")
@Before("cutMethod()")
public void before(JoinPoint joinPoint) {
// 获取当前用户ID
String userId = String.valueOf(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("userId"));
// 获取当前请求的方法路径
String servletPath = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
.getServletPath();
// 模拟获取数据权限配置信息 --- 传入当前请求。
// 此处时用来获取用户权限数据,根据具体情况自行修改
List<JnTestDataauthorityData> list = dataAuthorityTestDataService.queryWithPath(servletPath);
if (!CommonUtils.isEmpty(list)) {
// 格式转换数据处理 。业务需要。
List<JnTestDataauthorityVo> dataVoList = transData(list);
// 将获取到的数据处理成自己需要的格式后,存到threadloacl中,后面mybatis拦截器中获取数据
QueryInterceptorRegistry.setQueryInterceptor(dataVoList);
}
System.out.println("------------>@Before 前置通知之结束");
}
/**
* transData(数据转换)
* @param list
* @return List<JnTestDataauthorityVo>
* @author wuwenjin
* @date 2022年5月17日 上午10:42:06
* @Exception 异常对象
* @version 1.0
*/
private List<JnTestDataauthorityVo> transData(List<JnTestDataauthorityData> list) {
List<JnTestDataauthorityVo> result = new ArrayList<JnTestDataauthorityVo>();
// 先将传入的数据根据表名+字段分组
Map<String, List<JnTestDataauthorityData>> basicMap = list.stream()
.collect(Collectors.groupingBy(e -> e.getTableName() + e.getCode()));
JnTestDataauthorityVo datavo = null;
for (Map.Entry<String, List<JnTestDataauthorityData>> entry : basicMap.entrySet()) {
// String mapKey = entry.getKey();
List<JnTestDataauthorityData> mapValueList = entry.getValue();
datavo = new JnTestDataauthorityVo();
// 获取当前分组中纬度值
List<String> valueList = mapValueList.stream().map(s -> s.getCvalue()).distinct()
.collect(Collectors.toList());
// 对象赋值
datavo.setCode(mapValueList.get(0).getCode());
datavo.setMapperMethod(mapValueList.get(0).getMapperMethod());
datavo.setRequestPath(mapValueList.get(0).getRequestPath());
datavo.setTableName(mapValueList.get(0).getTableName());
datavo.setCvalueList(valueList);
datavo.setExtFlag(mapValueList.get(0).getExtFlag());// 是否扩展表字段 1是 0否
datavo.setMainid(mapValueList.get(0).getMainid());
datavo.setMainTable(mapValueList.get(0).getMainTable());
if (valueList.size() == 1) {
datavo.setSingleFlag("0");
} else {
datavo.setSingleFlag("1");
}
result.add(datavo);
}
return result;
}
/**
* 定制一个环绕通知
* @param joinPoint
* @throws Throwable
*/
@Around("cutMethod()")
public Object advice(ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();// 执行proceed方法的作用是让目标方法执行
}
@After("cutMethod()")
public void after() {
System.out.println("------------>@After 已经记录下操作日志@After 方法执行后");
}
}
说明:自定义注解的用途主要是用来获取当前用户的数据权限数据, 然后存到threadlocal中。为什么要存到threadlocal中呢? 因为后面再mytaits拦截器没法直接注入业务service来获取权限数据。 所以此处用了threadlocal来中转了一下。 其实此处也可以使用其它缓存来实现,此处就不再拓展。下面时threadlocal相关代码。
import java.util.List;
import com.freshport.freight.flatcar.entity.vo.JnTestDataauthorityVo;
public class QueryInterceptorRegistry {
// ThreadLocal 存储的数据格式根据自己业务定义即可
private static ThreadLocal<List<JnTestDataauthorityVo>> queryInterceptor = new ThreadLocal<List<JnTestDataauthorityVo>>();
public static List<JnTestDataauthorityVo> getQueryInterceptor() {
return queryInterceptor.get();
}
public static void setQueryInterceptor(List<JnTestDataauthorityVo> queryInterceptor) {
QueryInterceptorRegistry.queryInterceptor.set(queryInterceptor);
}
public static void clear() {
queryInterceptor.remove();
}
}
到这一步,前期的获取数据已经准备完成, 接下来就是重点了,更改SQL。
// 自定义一个mybatis拦截器,用来获取执行SQL
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
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.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import com.freshport.freight.common.util.CommonUtils;
import com.freshport.freight.flatcar.entity.vo.JnTestDataauthorityVo;
/**
* 自定义 MyBatis 拦截器
*/
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }) })
public class MySqlInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(MySqlInterceptor.class);
/**
* intercept 方法用来对拦截的sql进行具体的操作
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取用户自定义权限数据。
List<JnTestDataauthorityVo> authDataList = QueryInterceptorRegistry.getQueryInterceptor();
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
// sql语句类型 select、delete、insert、update
String sqlCommandType = ms.getSqlCommandType().toString();
// 仅拦截 select 查询 或者 传入的参数为空
if (!sqlCommandType.equals(SqlCommandType.SELECT.toString()) || CommonUtils.isEmpty(authDataList)) {
return invocation.proceed();
}
// id为执行的mapper方法的全路径名,如com.mapper.UserMapper
String id = ms.getId();
BoundSql boundSql = ms.getBoundSql(parameterObject);
String origSql = boundSql.getSql();
System.out.println("======>原始SQL: " + origSql);
String newSql = "";
// 这个方法主要是用来解析原来的SQL, 然后再将现有的数据权限拼装成SQL语句添加上,返回一个新SQL。
newSql = handleSql(origSql, authDataList, sqlFlag);
System.out.println("======>改写的SQL: " + newSql);
// 重新new一个查询语句对象
BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), newSql, boundSql.getParameterMappings(),
boundSql.getParameterObject());
// 把新的查询放到statement里
MappedStatement newMs = newMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
// MappedStatement维护了⼀条<select|update|delete|insert>节点的封装
Object[] queryArgs = invocation.getArgs();
queryArgs[0] = newMs;
return invocation.proceed();
}
/**
* 定义一个内部辅助类,作用是包装 SQL
*/
class BoundSqlSqlSource implements SqlSource {
private BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
builder.keyProperty(ms.getKeyProperties()[0]);
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
@Override
public Object plugin(Object target) {
logger.info("plugin方法:{}", target);
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 获取属性
// String value1 = properties.getProperty("prop1");
logger.info("properties方法:{}", properties.toString());
}
}
关于mybatis自定义拦截器说明:
1.需要在MybatisConfiguration.java中配置一下你的自定义拦截器。如果你的项目中使用的pagehelper分页,那么需要将你的自定义拦截器放到pagehelper前面, 这样才可以获取到pagehelper生成的SQL,否则无法获取SQL语句。
2.上面mybatis拦截器中, handlesql()方法我没有放出来。 是因为此处我解析SQL的方法是自己根据SQL格式截取处理的,没有通用性。 有具体的解析SQL的sqlprease,druid等工具,但是由于我们的SQL过于复杂导致解析报错没法使用才用的截取方式。大家可以试试解析工具,简单的SQL是可以解析出来。
截至到这, 通过mybatis拦截器方式添加数据权限的方式就可以实现了。 但是仍然还有优化的空间,比如说微服务中跨系统调用时数据传值问题,缓存如何使用问题这些没有详细的来说。上面主要是一个解决思路,具体细节大家再搜一下差不多就能解决。

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐
所有评论(0)