什么是数据权限
数据权限是指系统根据用户的角色、职位或其他属性,控制用户能够访问的数据范围。与传统的功能权限(菜单、按钮权限)不同,数据权限关注的是数据行级别的访问控制。
常见的数据权限控制方式包括:
部门数据权限:只能访问本部门数据
个人数据权限:只能访问自己的数据
自定义数据范围:通过特定规则限制数据访问
MyBatis-Plus实现数据权限的方案
实现完整例子
下面是一个完整的基于MyBatis-Plus和Spring Security的数据权限实现示例:
1. 数据权限配置类
@Configuration
public class MybatisConfig {
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer(){
return configuration -> configuration.setObjectWrapperFactory(new MapWrapperFactory());
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1.添加数据权限插件
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataPressionConfig()));
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 分页插件
paginationInnerInterceptor.setOptimizeJoin(false);
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setDbType(DbType.POSTGRE_SQL);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
2. 数据权限拦截器
@Slf4j
@Component
public class DataPressionConfig implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
try {
if(null==mappedStatementId){
return null;
}
Class<?> mapperClazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(".")));
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(".") + 1);
// 获取自身类中的所有方法,不包括继承。与访问权限无关
Method[] methods = mapperClazz.getDeclaredMethods();
for (Method method : methods) {
DataScope dataScopeAnnotationMethod = method.getAnnotation(DataScope.class);
if(null==dataScopeAnnotationMethod){
continue;
}
//spring aoc里拿方法参数
Parameter[] parameters= method.getParameters();
if (parameters.length > 0) {
log.info("方法参数:" + dataScopeAnnotationMethod.oneselfScopeName() );
}
if (ObjectUtils.isEmpty(dataScopeAnnotationMethod) || !dataScopeAnnotationMethod.enabled()) {
continue;
}
if (method.getName().equals(methodName) || (method.getName() + "_COUNT").equals(methodName) || (method.getName() + "_count").equals(methodName)) {
return buildDataScopeByAnnotation(dataScopeAnnotationMethod,mappedStatementId);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* DataScope注解方式,拼装数据权限
*
* @param dataScope
* @return
*/
private Expression buildDataScopeByAnnotation(DataScope dataScope,String mapperId) {
Map<String, Object> params = DataPermissionContext.getParams();
if (params == null || params.isEmpty()) {
return null;
}
Object areaCodes = params.get(mapperId);
List<String> dataScopeDeptIds= (List<String>) areaCodes;
// 获取注解信息
String tableAlias = dataScope.tableAlias();
String areaCodes= dataScope.areaCodes();
Expression expression = buildDataScopeExpression(tableAlias, areaCodes, dataScopeDeptIds);
return expression == null ? null : new Parenthesis(expression);
}
/**
* 拼装数据权限
*
* @param tableAlias 表别名
* @param oneselfScopeName 本人限制范围的字段名称
* @param dataScopeDeptIds 数据权限部门ID集合,去重
* @return
*/
private Expression buildDataScopeExpression(String tableAlias, String areaCodes, List<String> dataScopeDeptIds) {
/**
* 构造部门里行政区划 area_code 的in表达式。
*/
try {
String sql=tableAlias + "." + areaCodes+" in (";
for(String areaCode:dataScopeDeptIds){
sql+="'"+areaCode+"',";
}
sql=sql.substring(0,sql.length()-1)+")";
Expression selectExpression = CCJSqlParserUtil.parseCondExpression(sql, true);
return selectExpression;
} catch (JSQLParserException e) {
throw new RuntimeException(e);
}
}
}
3. 存储spring aop切面拿到的数据
public class DataPermissionContext {
private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();
public static void setParams(Map<String, Object> params) {
CONTEXT.set(params);
}
public static Map<String, Object> getParams() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
4. 定义数据权限注解
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
/**
* 是否生效,默认true-生效
*/
boolean enabled() default true;
/**
* 表别名
*/
String tableAlias() default "";
/**
* 本人限制范围的字段名称
*/
String areaCodes() default "area_code";
}
5. 实现AOP切面
@Slf4j
@Aspect
@Component
public class DataAspet {
@Pointcut("@annotation(DataScope)")
public void logPoinCut() {
}
@Before("logPoinCut()")
public void saveSysLog(JoinPoint joinPoint) {
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
DataScope scope = method.getAnnotation(DataScope.class);
Map<String, Object> paramValues = new HashMap<>();
Object[] args = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
if(null!=parameters&¶meters.length>0) {
//添加到最后一个参数
log.info("参数名称:{}",signature.getName()+":"+signature.getDeclaringTypeName());
paramValues.put(signature.getDeclaringTypeName()+"."+signature.getName(), args[parameters.length-1]);
DataPermissionContext.setParams(paramValues);
}else{
DataPermissionContext.setParams(null);
}
}
}
6. mapper里注解使用数据权限
tableAlias里你的sql里面要插入数据权限字段的表别名。
比如:
select count(*) as total,d.type as type from bus_device d group by d.type
spring aop 会读取mapper方法最后一个参数,然后切入Sql变成
select count(*) as total,d.type as type from bus_device d where
d.area_code in(#{areaCodes} ) group by d.type
@DataScope(tableAlias = "d")
List<DeviceTypeCountDto> listByApplicationCategory(@Param( @Param("type") String type,List<String> areaCodes);
注意事项
性能考虑:数据权限过滤会增加SQL复杂度,可能影响查询性能,特别是对于大数据量表。可以考虑添加适当的索引优化。
SQL注入风险:在拼接SQL时要特别注意防止SQL注入,建议使用预编译参数。
缓存问题:如果使用了缓存,需要注意数据权限可能导致缓存命中率下降或数据泄露问题。
多租户场景:在多租户系统中,数据权限通常需要与租户隔离一起考虑。
复杂查询:对于复杂的多表关联查询,数据权限条件可能需要更精细的控制。
通过以上方案,我们可以灵活地在MyBatis-Plus中实现各种数据权限控制需求,根据项目实际情况选择最适合的实现方式。