什么是數據權限
數據權限是指系統根據用戶的角色、職位或其他屬性,控制用戶能夠訪問的數據范圍。與傳統的功能權限(菜單、按鈕權限)不同,數據權限關注的是數據行級別的訪問控制。
常見的數據權限控制方式包括:
-
部門數據權限:只能訪問本部門數據
-
個人數據權限:只能訪問自己的數據
-
自定義數據范圍:通過特定規則限制數據訪問
MyBatis-Plus實現數據權限的方案
實現完整例子
下面是一個完整的基于MyBatis-Plus和Spring Security的數據權限實現示例:
?1. 數據權限配置類
@Configuration
public class MybatisConfig {@Beanpublic ConfigurationCustomizer mybatisConfigurationCustomizer(){return configuration -> configuration.setObjectWrapperFactory(new MapWrapperFactory());}@Beanpublic 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 {@Overridepublic 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中實現各種數據權限控制需求,根據項目實際情況選擇最適合的實現方式。