From 9d97c0ccb413f5a4ec2fa48f5e206f3955fa9836 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 15 Dec 2021 10:06:02 +0800 Subject: [PATCH] yudao-spring-boot-starter-data-permission 和 yudao-spring-boot-starter-tenant 调整为 biz 组件 --- yudao-admin-server/pom.xml | 18 ++++++++---------- yudao-core-service/pom.xml | 8 ++++---- yudao-dependencies/pom.xml | 22 ++++++++++------------ yudao-framework/pom.xml | 7 ++++--- yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java | 45 +++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java | 507 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java | 6 ++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java | 20 ++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java | 22 ++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java | 35 +++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java | 28 ++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java | 4 ++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories | 3 +++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java | 370 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java | 34 ++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java | 30 ++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java | 20 ++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java | 25 +++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java | 25 +++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java | 21 +++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java | 39 +++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java | 14 ++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java | 14 ++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java | 42 ++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java | 19 +++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java | 39 +++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java | 17 +++++++++++++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories | 6 ++++++ yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java | 27 +++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-data-permission/pom.xml | 45 --------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java | 45 --------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java | 34 ---------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java | 35 ----------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java | 36 ------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java | 72 ------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java | 72 ------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java | 507 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java | 6 ------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java | 186 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java | 20 -------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java | 22 ---------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java | 35 ----------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java | 36 ------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java | 28 ---------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java | 62 -------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java | 4 ---- yudao-framework/yudao-spring-boot-starter-data-permission/src/main/resources/META-INF/spring.factories | 3 --- yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java | 108 ------------------------------------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java | 66 ------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java | 190 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java | 370 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java | 221 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java | 145 ------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/pom.xml | 61 ------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java | 34 ---------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java | 30 ------------------------------ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java | 43 ------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java | 20 -------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java | 25 ------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java | 25 ------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java | 44 -------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java | 21 --------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java | 39 --------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java | 14 -------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java | 58 ---------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java | 14 -------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java | 42 ------------------------------------------ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java | 47 ----------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java | 50 -------------------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java | 19 ------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java | 39 --------------------------------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java | 17 ----------------- yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories | 6 ------ yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java | 27 --------------------------- 更新日志.md | 3 +++ 95 files changed, 3052 insertions(+), 3052 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/pom.xml delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/main/resources/META-INF/spring.factories delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/pom.xml delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java diff --git a/yudao-admin-server/pom.xml b/yudao-admin-server/pom.xml index 4bd789b..847e626 100644 --- a/yudao-admin-server/pom.xml +++ b/yudao-admin-server/pom.xml @@ -35,6 +35,14 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-sms + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-data-permission + cn.iocoder.boot @@ -118,16 +126,6 @@ - cn.iocoder.boot - yudao-spring-boot-starter-tenant - - - - cn.iocoder.boot - yudao-spring-boot-starter-data-permission - - - org.apache.velocity velocity-engine-core diff --git a/yudao-core-service/pom.xml b/yudao-core-service/pom.xml index 4be7e2e..f8a5bf7 100644 --- a/yudao-core-service/pom.xml +++ b/yudao-core-service/pom.xml @@ -36,6 +36,10 @@ cn.iocoder.boot yudao-spring-boot-starter-biz-pay + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + @@ -91,10 +95,6 @@ - - cn.iocoder.boot - yudao-spring-boot-starter-tenant - com.google.guava diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 300635c..7d879b5 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -105,6 +105,16 @@ yudao-spring-boot-starter-biz-weixin ${revision} + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + ${revision} + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-data-permission + ${revision} + @@ -364,18 +374,6 @@ - cn.iocoder.boot - yudao-spring-boot-starter-tenant - ${revision} - - - - cn.iocoder.boot - yudao-spring-boot-starter-data-permission - ${revision} - - - org.projectlombok lombok ${lombok.version} diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml index d2cc800..85533c9 100644 --- a/yudao-framework/pom.xml +++ b/yudao-framework/pom.xml @@ -24,6 +24,7 @@ yudao-spring-boot-starter-excel yudao-spring-boot-starter-test + yudao-spring-boot-starter-extension yudao-spring-boot-starter-biz-operatelog yudao-spring-boot-starter-biz-dict @@ -31,9 +32,9 @@ yudao-spring-boot-starter-activiti yudao-spring-boot-starter-biz-pay yudao-spring-boot-starter-biz-weixin - yudao-spring-boot-starter-extension - yudao-spring-boot-starter-tenant - yudao-spring-boot-starter-data-permission + yudao-spring-boot-starter-biz-social + yudao-spring-boot-starter-biz-tenant + yudao-spring-boot-starter-biz-data-permission yudao-framework diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml new file mode 100644 index 0000000..7565aae --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/pom.xml @@ -0,0 +1,45 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-data-permission + jar + + ${artifactId} + 数据权限 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + true + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..609e8a3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; +import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * 数据全新啊的自动配置类 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoDataPermissionAutoConfiguration { + + @Bean + public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { + return new DataPermissionRuleFactoryImpl(rules); + } + + @Bean + public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, + List rules) { + // 创建 DataPermissionDatabaseInterceptor 拦截器 + DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules); + DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + @Bean + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { + return new DataPermissionAnnotationAdvisor(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..b66c176 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * 基于部门的数据权限 AutoConfiguration + * + * @author 芋道源码 + */ +@Configuration +@ConditionalOnClass(LoginUser.class) +@ConditionalOnBean(value = {DeptDataPermissionFrameworkService.class, DeptDataPermissionRuleCustomizer.class}) +public class YudaoDeptDataPermissionAutoConfiguration { + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(DeptDataPermissionFrameworkService service, + List customizers) { + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(service); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java new file mode 100644 index 0000000..4e8a921 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.datapermission.core.annotation; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * 可声明在类或者方法上,标识使用的数据权限规则 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataPermission { + + /** + * 当前类或方法是否开启数据权限 + * 即使不添加 @DataPermission 注解,默认是开启状态 + * 可通过设置 enable 为 false 禁用 + */ + boolean enable() default true; + + /** + * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} + */ + Class[] includeRules() default {}; + + /** + * 排除的数据权限规则数组,优先级最低 + */ + Class[] excludeRules() default {}; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java new file mode 100644 index 0000000..03d212c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; + +/** + * {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 + * + * @author 芋道源码 + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { + + private final Advice advice; + + private final Pointcut pointcut; + + public DataPermissionAnnotationAdvisor() { + this.advice = new DataPermissionAnnotationInterceptor(); + this.pointcut = this.buildPointcut(); + } + + protected Pointcut buildPointcut() { + Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); + Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); + return new ComposablePointcut(classPointcut).union(methodPointcut); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java new file mode 100644 index 0000000..48a0354 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import lombok.Getter; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link DataPermission} 注解的拦截器 + * 1. 在执行方法前,将 @DataPermission 注解入栈 + * 2. 在执行方法后,将 @DataPermission 注解出栈 + * + * @author 芋道源码 + */ +@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 +public class DataPermissionAnnotationInterceptor implements MethodInterceptor { + + /** + * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 + */ + static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); + + @Getter + private final Map dataPermissionCache = new ConcurrentHashMap<>(); + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // 入栈 + DataPermission dataPermission = this.findAnnotation(methodInvocation); + if (dataPermission != null) { + DataPermissionContextHolder.add(dataPermission); + } + try { + // 执行逻辑 + return methodInvocation.proceed(); + } finally { + // 出栈 + if (dataPermission != null) { + DataPermissionContextHolder.remove(); + } + } + } + + private DataPermission findAnnotation(MethodInvocation methodInvocation) { + // 1. 从缓存中获取 + Method method = methodInvocation.getMethod(); + Object targetObject = methodInvocation.getThis(); + Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); + MethodClassKey methodClassKey = new MethodClassKey(method, clazz); + DataPermission dataPermission = dataPermissionCache.get(methodClassKey); + if (dataPermission != null) { + return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; + } + + // 2.1 从方法中获取 + dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); + // 2.2 从类上获取 + if (dataPermission == null) { + dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); + } + // 2.3 添加到缓存中 + dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); + return dataPermission; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java new file mode 100644 index 0000000..9705d88 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import com.alibaba.ttl.TransmittableThreadLocal; + +import java.util.LinkedList; +import java.util.List; + +/** + * {@link DataPermission} 注解的 Context 上下文 + * + * @author 芋道源码 + */ +public class DataPermissionContextHolder { + + /** + * 使用 List 的原因,可能存在方法的嵌套调用 + */ + private static final ThreadLocal> DATA_PERMISSIONS = + TransmittableThreadLocal.withInitial(LinkedList::new); + + /** + * 获得当前的 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission get() { + return DATA_PERMISSIONS.get().peekLast(); + } + + /** + * 入栈 DataPermission 注解 + * + * @param dataPermission DataPermission 注解 + */ + public static void add(DataPermission dataPermission) { + DATA_PERMISSIONS.get().addLast(dataPermission); + } + + /** + * 出栈 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission remove() { + DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); + // 无元素时,清空 ThreadLocal + if (DATA_PERMISSIONS.get().isEmpty()) { + DATA_PERMISSIONS.remove(); + } + return dataPermission; + } + + /** + * 获得所有 DataPermission + * + * @return DataPermission 队列 + */ + public static List getAll() { + return DATA_PERMISSIONS.get(); + } + + /** + * 清空上下文 + * + * 目前仅仅用于单测 + */ + public static void clear() { + DATA_PERMISSIONS.remove(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java new file mode 100644 index 0000000..e181b19 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java @@ -0,0 +1,507 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import com.alibaba.ttl.TransmittableThreadLocal; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ItemsList; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 + * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法 + * + * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 + * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { + + private final DataPermissionRuleFactory ruleFactory; + + @Getter + private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); + + @Override // SELECT 场景 + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, + RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserSingle(mpBs.sql(), null)); + } finally { + addMappedStatementCache(ms); + ContextHolder.clear(); + } + } + + @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景 + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); + MappedStatement ms = mpSh.mappedStatement(); + SqlCommandType sct = ms.getSqlCommandType(); + if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserMulti(mpBs.sql(), null)); + } finally { + addMappedStatementCache(ms); + ContextHolder.clear(); + } + } + } + + @Override + protected void processSelect(Select select, int index, String sql, Object obj) { + processSelectBody(select.getSelectBody()); + List withItemsList = select.getWithItemsList(); + if (!CollectionUtils.isEmpty(withItemsList)) { + withItemsList.forEach(this::processSelectBody); + } + } + + protected void processSelectBody(SelectBody selectBody) { + if (selectBody == null) { + return; + } + if (selectBody instanceof PlainSelect) { + processPlainSelect((PlainSelect) selectBody); + } else if (selectBody instanceof WithItem) { + WithItem withItem = (WithItem) selectBody; + processSelectBody(withItem.getSubSelect().getSelectBody()); + } else { + SetOperationList operationList = (SetOperationList) selectBody; + List selectBodys = operationList.getSelects(); + if (CollectionUtils.isNotEmpty(selectBodys)) { + selectBodys.forEach(this::processSelectBody); + } + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + update.setWhere(this.builderExpression(update.getWhere(), table)); + } + + /** + * delete 语句处理 + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); + } + + /** + * 处理 PlainSelect + */ + protected void processPlainSelect(PlainSelect plainSelect) { + FromItem fromItem = plainSelect.getFromItem(); + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where); + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + plainSelect.setWhere(builderExpression(where, fromTable)); + } else { + processFromItem(fromItem); + } + //#3087 github + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(this::processSelectItem); + } + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + processJoins(joins); + } + } + + /** + * 处理where条件内的子查询 + *

+ * 支持如下: + * 1. in + * 2. = + * 3. > + * 4. < + * 5. >= + * 6. <= + * 7. <> + * 8. EXISTS + * 9. NOT EXISTS + *

+ * 前提条件: + * 1. 子查询必须放在小括号中 + * 2. 子查询一般放在比较操作符的右边 + * + * @param where where 条件 + */ + protected void processWhereSubSelect(Expression where) { + if (where == null) { + return; + } + if (where instanceof FromItem) { + processFromItem((FromItem) where); + return; + } + if (where.toString().indexOf("SELECT") > 0) { + // 有子查询 + if (where instanceof BinaryExpression) { + // 比较符号 , and , or , 等等 + BinaryExpression expression = (BinaryExpression) where; + processWhereSubSelect(expression.getLeftExpression()); + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof InExpression) { + // in + InExpression expression = (InExpression) where; + ItemsList itemsList = expression.getRightItemsList(); + if (itemsList instanceof SubSelect) { + processSelectBody(((SubSelect) itemsList).getSelectBody()); + } + } else if (where instanceof ExistsExpression) { + // exists + ExistsExpression expression = (ExistsExpression) where; + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof NotExpression) { + // not exists + NotExpression expression = (NotExpression) where; + processWhereSubSelect(expression.getExpression()); + } else if (where instanceof Parenthesis) { + Parenthesis expression = (Parenthesis) where; + processWhereSubSelect(expression.getExpression()); + } + } + } + + protected void processSelectItem(SelectItem selectItem) { + if (selectItem instanceof SelectExpressionItem) { + SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; + if (selectExpressionItem.getExpression() instanceof SubSelect) { + processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); + } else if (selectExpressionItem.getExpression() instanceof Function) { + processFunction((Function) selectExpressionItem.getExpression()); + } + } + } + + /** + * 处理函数 + *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

+ *

fixed gitee pulls/141

+ * + * @param function 函数 + */ + protected void processFunction(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters != null) { + parameters.getExpressions().forEach(expression -> { + if (expression instanceof SubSelect) { + processSelectBody(((SubSelect) expression).getSelectBody()); + } else if (expression instanceof Function) { + processFunction((Function) expression); + } + }); + } + } + + /** + * 处理子查询等 + */ + protected void processFromItem(FromItem fromItem) { + if (fromItem instanceof SubJoin) { + SubJoin subJoin = (SubJoin) fromItem; + if (subJoin.getJoinList() != null) { + processJoins(subJoin.getJoinList()); + } + if (subJoin.getLeft() != null) { + processFromItem(subJoin.getLeft()); + } + } else if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } else if (fromItem instanceof ValuesList) { + logger.debug("Perform a subquery, if you do not give us feedback"); + } else if (fromItem instanceof LateralSubSelect) { + LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; + if (lateralSubSelect.getSubSelect() != null) { + SubSelect subSelect = lateralSubSelect.getSubSelect(); + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } + } + } + + /** + * 处理 joins + * + * @param joins join 集合 + */ + private void processJoins(List joins) { + //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 + Deque tables = new LinkedList<>(); + for (Join join : joins) { + // 处理 on 表达式 + FromItem fromItem = join.getRightItem(); + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + // 获取 join 尾缀的 on 表达式列表 + Collection originOnExpressions = join.getOnExpressions(); + // 正常 join on 表达式只有一个,立刻处理 + if (originOnExpressions.size() == 1) { + processJoin(join); + continue; + } + tables.push(fromTable); + // 尾缀多个 on 表达式的时候统一处理 + if (originOnExpressions.size() > 1) { + Collection onExpressions = new LinkedList<>(); + for (Expression originOnExpression : originOnExpressions) { + Table currentTable = tables.poll(); + onExpressions.add(builderExpression(originOnExpression, currentTable)); + } + join.setOnExpressions(onExpressions); + } + } else { + // 处理右边连接的子表达式 + processFromItem(fromItem); + } + } + } + + /** + * 处理联接语句 + */ + protected void processJoin(Join join) { + if (join.getRightItem() instanceof Table) { + Table fromTable = (Table) join.getRightItem(); + Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions()); + originOnExpression = builderExpression(originOnExpression, fromTable); + join.setOnExpressions(CollUtil.newArrayList(originOnExpression)); + } + } + + /** + * 处理条件 + */ + protected Expression builderExpression(Expression currentExpression, Table table) { + // 获得 Table 对应的数据权限条件 + Expression equalsTo = buildDataPermissionExpression(table); + if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认 + return currentExpression; + } + + // 表达式为空,则直接返回 equalsTo + if (currentExpression == null) { + return equalsTo; + } + // 如果表达式为 Or,则需要 (currentExpression) AND equalsTo + if (currentExpression instanceof OrExpression) { + return new AndExpression(new Parenthesis(currentExpression), equalsTo); + } + // 如果表达式为 And,则直接返回 currentExpression AND equalsTo + return new AndExpression(currentExpression, equalsTo); + } + + /** + * 构建指定表的数据权限的 Expression 过滤条件 + * + * @param table 表 + * @return Expression 过滤条件 + */ + private Expression buildDataPermissionExpression(Table table) { + // 生成条件 + Expression allExpression = null; + for (DataPermissionRule rule : ContextHolder.getRules()) { + // 判断表名是否匹配 + if (!rule.getTableNames().contains(table.getName())) { + continue; + } + // 单条规则的条件 + String tableName = MyBatisUtils.getTableName(table); + Expression oneExpress = rule.getExpression(tableName, table.getAlias()); + // 拼接到 allExpression 中 + allExpression = allExpression == null ? oneExpress + : new AndExpression(allExpression, oneExpress); + } + + // 如果条件非空,说明已经重写了 + if (allExpression != null) { + ContextHolder.setRewrite(true); + } + return allExpression; + } + + /** + * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 + * + * @param ms MappedStatement + */ + private void addMappedStatementCache(MappedStatement ms) { + if (ContextHolder.getRewrite()) { + return; + } + // 有重写,进行添加 + mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); + } + + /** + * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 + * + * @author 芋道源码 + */ + static final class ContextHolder { + + /** + * 该 {@link MappedStatement} 对应的规则 + */ + private static final ThreadLocal> RULES = new TransmittableThreadLocal<>(); + /** + * SQL 是否进行重写 + */ + private static final ThreadLocal REWRITE = new TransmittableThreadLocal<>(); + + public static void init(List rules) { + RULES.set(rules); + REWRITE.set(false); + } + + public static void clear() { + RULES.remove(); + REWRITE.remove(); + } + + public static boolean getRewrite() { + return REWRITE.get(); + } + + public static void setRewrite(boolean rewrite) { + REWRITE.set(rewrite); + } + + public static List getRules() { + return RULES.get(); + } + + } + + /** + * {@link MappedStatement} 缓存 + * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 + * 如果无效,则可以避免 SQL 的解析,加快速度 + * + * @author 芋道源码 + */ + static final class MappedStatementCache { + + /** + * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 + * + * value:{@link MappedStatement#getId()} 编号 + */ + @Getter + private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); + + /** + * 判断是否无需重写 + * ps:虽然有点中文式英语,但是容易读懂即可 + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + * @return 是否无需重写 + */ + public boolean noRewritable(MappedStatement ms, List rules) { + // 如果规则为空,说明无需重写 + if (CollUtil.isEmpty(rules)) { + return true; + } + // 任一规则不在 noRewritableMap 中,则说明可能需要重写 + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (!CollUtil.contains(mappedStatementIds, ms.getId())) { + return false; + } + } + return true; + } + + /** + * 添加无需重写的 MappedStatement + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + */ + public void addNoRewritable(MappedStatement ms, List rules) { + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (CollUtil.isNotEmpty(mappedStatementIds)) { + mappedStatementIds.add(ms.getId()); + } else { + noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); + } + } + } + + /** + * 清空缓存 + * 目前主要提供给单元测试 + */ + public void clear() { + noRewritableMappedStatements.clear(); + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java new file mode 100644 index 0000000..20daa85 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.framework.datapermission.core.dept; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java new file mode 100644 index 0000000..4f7d18d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.framework.datapermission.core.dept.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 + * + * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 + * + * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? + * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-server 采用该方案】 + * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 + * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 + * 最终过滤条件是 WHERE dept_id = ? + * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; + * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) + * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; + * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class DeptDataPermissionRule implements DataPermissionRule { + + private static final String DEPT_COLUMN_NAME = "dept_id"; + private static final String USER_COLUMN_NAME = "user_id"; + + private final DeptDataPermissionFrameworkService deptDataPermissionService; + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map deptColumns = new HashMap<>(); + /** + * 基于用户的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map userColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 只有有登陆用户的情况下,才进行数据权限的处理 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + + // 获得数据权限 + DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser); + if (deptDataPermission == null) { + log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", + loginUser.getId(), tableName, tableAlias.getName())); + } + + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 + if (deptDataPermission.getAll()) { + return null; + } + + // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 + if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) + && Boolean.FALSE.equals(deptDataPermission.getSelf())) { + return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 + } + + // 情况三,拼接 Dept 和 User 的条件,最后组合 + Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + if (deptExpression == null && userExpression == null) { + log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", + loginUser.getId(), tableName, tableAlias.getName())); + } + if (deptExpression == null) { + return userExpression; + } + if (userExpression == null) { + return deptExpression; + } + // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ? + return new OrExpression(deptExpression, userExpression); + } + + private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { + // 如果不存在配置,则无需作为条件 + String columnName = deptColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(deptIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); + } + + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + // 如果不查看自己,则无需作为条件 + if (Boolean.FALSE.equals(self)) { + return null; + } + String columnName = userColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 拼接条件 + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } + + // ==================== 添加配置 ==================== + + public void addDeptColumn(Class entityClass) { + addDeptColumn(entityClass, DEPT_COLUMN_NAME); + } + + public void addDeptColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addDeptColumn(tableName, columnName); + } + + public void addDeptColumn(String tableName, String columnName) { + deptColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + + public void addUserColumn(Class entityClass) { + addUserColumn(entityClass, USER_COLUMN_NAME); + } + + public void addUserColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addUserColumn(tableName, columnName); + } + + public void addUserColumn(String tableName, String columnName) { + userColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 0000000..5341ee5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.datapermission.core.dept.rule; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface DeptDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(DeptDataPermissionRule rule); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java new file mode 100644 index 0000000..3ee6167 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.datapermission.core.dept.service; + +import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.security.core.LoginUser; + +/** + * 基于部门的数据权限 Framework Service 接口 + * 目前的实现类是 SysPermissionServiceImpl 类 + * + * @author 芋道源码 + */ +public interface DeptDataPermissionFrameworkService { + + /** + * 获得登陆用户的部门数据权限 + * + * @param loginUser 登陆用户 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 0000000..897fb22 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.framework.datapermission.core.dept.service.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 部门的数据权限 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptDataPermissionRespDTO { + + /** + * 是否可查看全部数据 + */ + private Boolean all; + /** + * 是否可查看自己的数据 + */ + private Boolean self; + /** + * 可查看的部门编号数组 + */ + private Set deptIds; + + public DeptDataPermissionRespDTO() { + this.all = false; + this.self = false; + this.deptIds = new HashSet<>(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java new file mode 100644 index 0000000..2bccde8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; + +import java.util.Set; + +/** + * 数据权限规则接口 + * 通过实现接口,自定义数据规则。例如说, + * + * @author 芋道源码 + */ +public interface DataPermissionRule { + + /** + * 返回需要生效的表名数组 + * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 + * + * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 + * + * @return 表名数组 + */ + Set getTableNames(); + + /** + * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 + * + * @param tableName 表名 + * @param tableAlias 别名,可能为空 + * @return 过滤条件 Expression 表达式 + */ + Expression getExpression(String tableName, Alias tableAlias); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java new file mode 100644 index 0000000..166dfea --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import java.util.List; + +/** + * {@link DataPermissionRule} 工厂接口 + * 作为 {@link DataPermissionRule} 的容器,提供管理能力 + * + * @author 芋道源码 + */ +public interface DataPermissionRuleFactory { + + /** + * 获得所有数据权限规则数组 + * + * @return 数据权限规则数组 + */ + List getDataPermissionRules(); + + /** + * 获得指定 Mapper 的数据权限规则数组 + * + * @param mappedStatementId 指定 Mapper 的编号 + * @return 数据权限规则数组 + */ + List getDataPermissionRule(String mappedStatementId); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java new file mode 100644 index 0000000..eaa6e6a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 默认的 DataPermissionRuleFactoryImpl 实现类 + * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { + + /** + * 数据权限规则数组 + */ + private final List rules; + + @Override + public List getDataPermissionRules() { + return rules; + } + + @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 + public List getDataPermissionRule(String mappedStatementId) { + // 1. 无数据权限 + if (CollUtil.isEmpty(rules)) { + return Collections.emptyList(); + } + // 2. 未配置,则默认开启 + DataPermission dataPermission = DataPermissionContextHolder.get(); + if (dataPermission == null) { + return rules; + } + // 3. 已配置,但禁用 + if (!dataPermission.enable()) { + return Collections.emptyList(); + } + + // 4. 已配置,只选择部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { + return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 5. 已配置,只排除部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { + return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 6. 已配置,全部规则 + return rules; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java new file mode 100644 index 0000000..831aa7c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 + */ +package cn.iocoder.yudao.framework.datapermission; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..1a4c029 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\ + cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java new file mode 100644 index 0000000..ba97ede --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link DataPermissionAnnotationInterceptor} 的单元测试 + * + * @author 芋道源码 + */ +public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionAnnotationInterceptor interceptor; + + @Mock + private MethodInvocation methodInvocation; + + @BeforeEach + public void setUp() { + interceptor.getDataPermissionCache().clear(); + } + + @Test // 无 @DataPermission 注解 + public void testInvoke_none() throws Throwable { + // 参数 + mockMethodInvocation(TestNone.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("none", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Method 上有 @DataPermission 注解 + public void testInvoke_method() throws Throwable { + // 参数 + mockMethodInvocation(TestMethod.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("method", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Class 上有 @DataPermission 注解 + public void testInvoke_class() throws Throwable { + // 参数 + mockMethodInvocation(TestClass.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("class", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + private void mockMethodInvocation(Class clazz) throws Throwable { + Object targetObject = clazz.newInstance(); + Method method = targetObject.getClass().getMethod("echo"); + when(methodInvocation.getThis()).thenReturn(targetObject); + when(methodInvocation.getMethod()).thenReturn(method); + when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); + } + + static class TestMethod { + + @DataPermission(enable = false) + public String echo() { + return "method"; + } + + } + + @DataPermission(enable = false) + static class TestClass { + + public String echo() { + return "class"; + } + + } + + static class TestNone { + + public String echo() { + return "none"; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java new file mode 100644 index 0000000..688b92d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.framework.datapermission.core.aop; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +/** + * {@link DataPermissionContextHolder} 的单元测试 + * + * @author 芋道源码 + */ +class DataPermissionContextHolderTest { + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGet() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.get(); + // 断言 + assertSame(result, dataPermission02); + } + + @Test + public void testPush() { + // 调用 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + // 断言 + DataPermission first = DataPermissionContextHolder.getAll().get(0); + DataPermission second = DataPermissionContextHolder.getAll().get(1); + assertSame(dataPermission01, first); + assertSame(dataPermission02, second); + } + + @Test + public void testRemove() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.remove(); + // 断言 + assertSame(result, dataPermission02); + assertEquals(1, DataPermissionContextHolder.getAll().size()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java new file mode 100644 index 0000000..36bf5c0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java @@ -0,0 +1,190 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.schema.Column; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.sql.Connection; +import java.util.*; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} + * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} + * 以及在这个过程中,ContextHolder 和 MappedStatementCache + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 清理上下文 + DataPermissionDatabaseInterceptor.ContextHolder.clear(); + // 清空缓存 + interceptor.getMappedStatementCache().clear(); + } + + @Test // 不存在规则,且不匹配 + public void testBeforeQuery_withoutRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql)); + } + } + + @Test // 存在规则,且不匹配 + public void testBeforeQuery_withMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_user where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100")); + // 断言缓存 + assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test // 存在规则,但不匹配 + public void testBeforeQuery_withoutMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_role where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_role WHERE id = 1")); + // 断言缓存 + assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test + public void testAddNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + List rules = singletonList(new DeptDataPermissionRule()); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + + // 调用 + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + // 断言 + Map, Set> noRewritableMappedStatements = + interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); + assertEquals(1, noRewritableMappedStatements.size()); + assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); + } + + @Test + public void testNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + // mock 数据 + List rules = singletonList(new DeptDataPermissionRule()); + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + + // 场景一,rules 为空 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); + // 场景二,rules 非空,可重写 + assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); + // 场景三,rule 非空,不可重写 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); + } + + private static class DeptDataPermissionRule implements DataPermissionRule { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return SetUtils.asSet("t_user"); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(100L); + return new EqualsTo(column, value); + } + + } + + private static class EmptyDataPermissionRule implements DataPermissionRule { + + @Override + public Set getTableNames() { + return Collections.emptySet(); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java new file mode 100644 index 0000000..8c0772f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java @@ -0,0 +1,370 @@ +package cn.iocoder.yudao.framework.datapermission.core.db; + +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.schema.Column; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 + * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 租户的数据权限规则 + DataPermissionRule tenantRule = new DataPermissionRule() { + + private static final String COLUMN = "tenant_id"; + + @Override + public Set getTableNames() { + return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试 + "t_user", "t_role"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(1L); + return new EqualsTo(column, value); + } + + }; + // 部门的数据权限规则 + DataPermissionRule deptRule = new DataPermissionRule() { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return asSet("t_user"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + ExpressionList values = new ExpressionList(new LongValue(10L), + new LongValue(20L)); + return new InExpression(column, values); + } + + }; + // 设置到上下文,保证 + DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); + } + + @Test + void delete() { + assertSql("delete from entity where id = ?", + "DELETE FROM entity WHERE id = ? AND tenant_id = 1"); + } + + @Test + void update() { + assertSql("update entity set name = ? where id = ?", + "UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1"); + } + + @Test + void selectSingle() { + // 单表 + assertSql("select * from entity where id = ?", + "SELECT * FROM entity WHERE id = ? AND tenant_id = 1"); + + assertSql("select * from entity where id = ? or name = ?", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); + + assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); + + /* not */ + assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", + "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1"); + } + + @Test + void selectSubSelectIn() { + /* in */ + assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在最前 + assertSql("SELECT * FROM entity e WHERE e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + // 在最后 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在中间 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectEq() { + /* = */ + assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectInnerNotEq() { + /* inner not = */ + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectExists() { + /* EXISTS */ + assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* NOT EXISTS */ + assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelect() { + /* >= */ + assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + /* <= */ + assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + /* <> */ + assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectFromSelect() { + assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); + } + + @Test + void selectBodySubSelect() { + assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", + "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); + } + + @Test + void selectLeftJoin() { + // left join + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectRightJoin() { + // right join + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectLeftJoinMultipleTrailingOn() { + // 多个 on 尾缀的 + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + } + + @Test + void selectInnerJoin() { + // inner join + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + // 垃圾 inner join todo +// assertSql("SELECT * FROM entity,entity1 " + +// "WHERE entity.id = entity1.id", +// "SELECT * FROM entity e " + +// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + +// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + + @Test + void selectWithAs() { + assertSql("with with_as_A as (select * from entity) select * from with_as_A", + "WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A"); + } + + private void assertSql(String sql, String targetSql) { + assertEquals(targetSql, interceptor.parserSingle(sql, null)); + } + + // ========== 额外的测试 ========== + + @Test + public void testSelectSingle() { + // 单表 + assertSql("select * from t_user where id = ?", + "SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)"); + + assertSql("select * from t_user where id = ? or name = ?", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + + assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + + /* not */ + assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); + } + + @Test + public void testSelectLeftJoin() { + // left join + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectRightJoin() { + // right join + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectInnerJoin() { + // inner join + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "inner join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 垃圾 inner join todo +// assertSql("SELECT * FROM entity,entity1 " + +// "WHERE entity.id = entity1.id", +// "SELECT * FROM entity e " + +// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + +// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java new file mode 100644 index 0000000..2953e58 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java @@ -0,0 +1,221 @@ +package cn.iocoder.yudao.framework.datapermission.core.dept.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; +import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DeptDataPermissionRule} 的单元测试 + * + * @author 芋道源码 + */ +class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeptDataPermissionRule rule; + + @Mock + private DeptDataPermissionFrameworkService deptDataPermissionFrameworkService; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + // 清空 rule + rule.getTableNames().clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + } + + @Test // 无 LoginUser + public void testGetExpression_noLoginUser() { + // 准备参数 + String tableName = randomString(); + Alias tableAlias = new Alias(randomString()); + // mock 方法 + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + + @Test // 无数据权限时 + public void testGetExpression_noDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); + } + } + + @Test // 全部数据权限 + public void testGetExpression_allDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + } + + @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 + public void testGetExpression_noDept_noSelf() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("null = null", expression.toString()); + } + } + + @Test // 拼接 Dept 和 User 的条件(字段都不符合) + public void testGetExpression_noDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 构建的条件为空", exception.getMessage()); + } + } + + @Test // 拼接 Dept 和 User 的条件(self 符合) + public void testGetExpression_noDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setSelf(true); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.id = 1", expression.toString()); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept 符合) + public void testGetExpression_yesDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20)", expression.toString()); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept + self 符合) + public void testGetExpression_yesDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); + when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java new file mode 100644 index 0000000..17dddc9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java @@ -0,0 +1,145 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule; + +import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; +import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DataPermissionRuleFactoryImpl} 单元测试 + * + * @author 芋道源码 + */ +class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; + + @Spy + private List rules = Arrays.asList(new DataPermissionRule01(), + new DataPermissionRule02()); + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGetDataPermissionRule_02() { + // 准备参数 + String mappedStatementId = randomString(); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @Test + public void testGetDataPermissionRule_03() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertTrue(result.isEmpty()); + } + + @Test + public void testGetDataPermissionRule_04() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule01.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_05() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule02.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_06() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @DataPermission(enable = false) + static class TestClass03 {} + + @DataPermission(includeRules = DataPermissionRule01.class) + static class TestClass04 {} + + @DataPermission(excludeRules = DataPermissionRule01.class) + static class TestClass05 {} + + @DataPermission + static class TestClass06 {} + + static class DataPermissionRule01 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + + static class DataPermissionRule02 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml new file mode 100644 index 0000000..0f22e28 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml @@ -0,0 +1,61 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-spring-boot-starter-biz-tenant + jar + + ${artifactId} + 多租户 + https://github.com/YunaiV/ruoyi-vue-pro + + + + cn.iocoder.boot + yudao-common + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mybatis + + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + cn.iocoder.boot + yudao-spring-boot-starter-job + + + + + cn.iocoder.boot + yudao-spring-boot-starter-mq + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java new file mode 100644 index 0000000..33390f6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Set; + +/** + * 多租户配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.tenant") +@Data +public class TenantProperties { + +// /** +// * 租户是否开启 +// */ +// private static final Boolean ENABLE_DEFAULT = true; +// +// /** +// * 是否开启 +// */ +// private Boolean enable = ENABLE_DEFAULT; + /** + * 需要多租户的表 + * + * 由于多租户并不作为 yudao 项目的重点功能,更多是扩展性的功能,所以采用正向配置需要多租户的表。 + * 如果需要,你可以改成 ignoreTables 来取消部分不需要的表 + */ + private Set tables; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java new file mode 100644 index 0000000..1c7d8a7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 DB 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +@EnableConfigurationProperties(TenantProperties.class) +public class YudaoTenantDatabaseAutoConfiguration { + + @Bean + public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, + MybatisPlusInterceptor interceptor) { + TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java new file mode 100644 index 0000000..89b58f8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 Job 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoTenantJobAutoConfiguration { + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof JobHandler)) { + return bean; + } + // 有 TenantJob 注解的情况下,才会进行处理 + if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) { + return bean; + } + + // 使用 TenantJobHandlerDecorator 装饰 + return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean); + } + + }; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java new file mode 100644 index 0000000..6a465b9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 MQ 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoTenantMQAutoConfiguration { + + @Bean + public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { + return new TenantRedisMessageInterceptor(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java new file mode 100644 index 0000000..b5dbd00 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 Web 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoTenantSecurityAutoConfiguration { + + @Bean + public FilterRegistrationBean tenantSecurityWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantSecurityWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); + return registrationBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java new file mode 100644 index 0000000..c2830a7 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 Web 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoTenantWebAutoConfiguration { + + @Bean + public FilterRegistrationBean tenantContextWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); + return registrationBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java new file mode 100644 index 0000000..cf5a941 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.tenant.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 多租户上下文 Holder + * + * @author 芋道源码 + */ +public class TenantContextHolder { + + private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); + + /** + * 获得租户编号。 + * + * @return 租户编号 + */ + public static Long getTenantId() { + return TENANT_ID.get(); + } + + /** + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 + * + * @return 租户编号 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null) { + throw new NullPointerException("TenantContextHolder 不存在租户编号"); + } + return tenantId; + } + + public static void setTenantId(Long tenantId) { + TENANT_ID.set(tenantId); + } + + public static void clear() { + TENANT_ID.remove(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java new file mode 100644 index 0000000..f4f0ea5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 拓展多租户的 BaseDO 基类 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class TenantBaseDO extends BaseDO { + + /** + * 多租户编号 + */ + private Long tenantId; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java new file mode 100644 index 0000000..283bd44 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.tenant.core.db; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import lombok.AllArgsConstructor; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; + +/** + * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class TenantDatabaseInterceptor implements TenantLineHandler { + + private final TenantProperties properties; + + @Override + public Expression getTenantId() { + return new StringValue(TenantContextHolder.getRequiredTenantId().toString()); + } + + @Override + public boolean ignoreTable(String tableName) { + // 如果实体类继承 TenantBaseDO 类,则是多租户表,不进行忽略 + TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName); + if (tableInfo != null && TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) { + return false; + } + // 不包含,说明要过滤 + return !CollUtil.contains(properties.getTables(), tableName); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java new file mode 100644 index 0000000..fd2ecad --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 多租户 Job 注解 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TenantJob { +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java new file mode 100644 index 0000000..25a6e01 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 多租户 JobHandler 装饰器 + * 任务执行时,会按照租户逐个执行 Job 的逻辑 + * + * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class TenantJobHandlerDecorator implements JobHandler { + + private final TenantFrameworkService tenantFrameworkService; + /** + * 被装饰的 Job + */ + private final JobHandler jobHandler; + + @Override + public final String execute(String param) throws Exception { + // 获得租户列表 + List tenantIds = tenantFrameworkService.getTenantIds(); + if (CollUtil.isEmpty(tenantIds)) { + return null; + } + + // 逐个租户,执行 Job + Map results = new ConcurrentHashMap<>(); + tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 + try { + // 设置租户 + TenantContextHolder.setTenantId(tenantId); + // 执行 Job + String result = jobHandler.execute(param); + // 添加结果 + results.put(tenantId, result); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.clear(); + } + }); + return JsonUtils.toJsonString(results); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java new file mode 100644 index 0000000..5127150 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.tenant.core.job; + +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker; + +/** + * 多租户 JobHandlerInvoker 拓展实现类 + * + * @author 芋道源码 + */ +public class TenantJobHandlerInvoker extends JobHandlerInvoker { + + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java new file mode 100644 index 0000000..15c72f9 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tenant.core.mq; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +/** + * 多租户 {@link AbstractRedisMessage} 拦截器 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 + * + * @author 芋道源码 + */ +public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { + + private static final String HEADER_TENANT_ID = "tenant-id"; + + @Override + public void sendMessageBefore(AbstractRedisMessage message) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + message.addHeader(HEADER_TENANT_ID, tenantId.toString()); + } + } + + @Override + public void consumeMessageBefore(AbstractRedisMessage message) { + String tenantIdStr = message.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantIdStr)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); + } + } + + @Override + public void consumeMessageAfter(AbstractRedisMessage message) { + // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 + TenantContextHolder.clear(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java new file mode 100644 index 0000000..c23bc71 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.tenant.core.redis; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; + +import java.time.Duration; + +/** + * 多租户拓展的 RedisKeyDefine 实现类 + * + * 由于 Redis 不同于 MySQL 有 column 字段,所以无法通过类似 WHERE tenant_id = ? 的方式过滤 + * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是: + * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d, + * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化 + * + * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,是 Redis Key 可能存在冲突的情况。 + * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题 + * + * @author 芋道源码 + */ +public class TenantRedisKeyDefine extends RedisKeyDefine { + + /** + * 多租户的 KEY 模板 + */ + private static final String KEY_TEMPLATE_SUFFIX = ":%d"; + + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout); + } + + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType); + } + + private static String buildKeyTemplate(String keyTemplate) { + return keyTemplate + KEY_TEMPLATE_SUFFIX; + } + + @Override + public String formatKey(Object... args) { + args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId()); + return super.formatKey(args); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 0000000..0801427 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.tenant.core.security; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 多租户 Security Web 过滤器 + * 校验用户访问的租户,是否是其所在的租户,避免越权问题 + * + * @author 芋道源码 + */ +@Slf4j +public class TenantSecurityWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + assert user != null; // shouldNotFilter 已经校验 + if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", + user.getTenantId(), user.getId(), user.getUserType(), + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), + "您无权访问该租户的数据")); + return; + } + // 继续过滤 + chain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return SecurityFrameworkUtils.getLoginUser() == null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java new file mode 100644 index 0000000..a36e612 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.tenant.core.service; + +import java.util.List; + +/** + * Tenant 框架 Service 接口,定义获取租户信息 + * + * @author 芋道源码 + */ +public interface TenantFrameworkService { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIds(); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 0000000..ec159e0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 多租户 Context Web 过滤器 + * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 + * + * @author 芋道源码 + */ +public class TenantContextWebFilter extends OncePerRequestFilter { + + private static final String HEADER_TENANT_ID = "tenant-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 设置 + String tenantId = request.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantId)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantId)); + } + try { + chain.doFilter(request, response); + } finally { + // 清理 + TenantContextHolder.clear(); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java new file mode 100644 index 0000000..aa22cdb --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java @@ -0,0 +1,17 @@ +/** + * 多租户,支持如下层面: + * 1. DB:基于 MyBatis Plus 多租户的功能实现。 + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: + * 1)Spring Async: + * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} + * 2)Spring Security: + * TransmittableThreadLocalSecurityContextHolderStrategy + * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 + * + */ +package cn.iocoder.yudao.framework.tenant; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..159dfcf --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java new file mode 100644 index 0000000..d456e01 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.tenant.core.redis; + +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TenantRedisKeyDefineTest { + + @Test + public void testFormatKey() { + Long tenantId = 30L; + TenantContextHolder.setTenantId(tenantId); + // 准备参数 + TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH, + Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED); + Long userId = 10L; + Integer userType = 1; + + // 调用 + String key = define.formatKey(userId, userType); + // 断言 + assertEquals("user:10:1:30", key); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/pom.xml b/yudao-framework/yudao-spring-boot-starter-data-permission/pom.xml deleted file mode 100644 index 15b0615..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/pom.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - yudao-framework - cn.iocoder.boot - ${revision} - - 4.0.0 - yudao-spring-boot-starter-data-permission - jar - - ${artifactId} - 数据权限 - https://github.com/YunaiV/ruoyi-vue-pro - - - - cn.iocoder.boot - yudao-common - - - - - cn.iocoder.boot - yudao-spring-boot-starter-security - true - - - - - cn.iocoder.boot - yudao-spring-boot-starter-mybatis - - - - - cn.iocoder.boot - yudao-spring-boot-starter-test - test - - - - diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java deleted file mode 100644 index 609e8a3..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDataPermissionAutoConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.config; - -import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; -import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -/** - * 数据全新啊的自动配置类 - * - * @author 芋道源码 - */ -@Configuration -public class YudaoDataPermissionAutoConfiguration { - - @Bean - public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { - return new DataPermissionRuleFactoryImpl(rules); - } - - @Bean - public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, - List rules) { - // 创建 DataPermissionDatabaseInterceptor 拦截器 - DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules); - DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); - // 添加到 interceptor 中 - // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 - MyBatisUtils.addInterceptor(interceptor, inner, 0); - return inner; - } - - @Bean - public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { - return new DataPermissionAnnotationAdvisor(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java deleted file mode 100644 index b66c176..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.config; - -import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -/** - * 基于部门的数据权限 AutoConfiguration - * - * @author 芋道源码 - */ -@Configuration -@ConditionalOnClass(LoginUser.class) -@ConditionalOnBean(value = {DeptDataPermissionFrameworkService.class, DeptDataPermissionRuleCustomizer.class}) -public class YudaoDeptDataPermissionAutoConfiguration { - - @Bean - public DeptDataPermissionRule deptDataPermissionRule(DeptDataPermissionFrameworkService service, - List customizers) { - // 创建 DeptDataPermissionRule 对象 - DeptDataPermissionRule rule = new DeptDataPermissionRule(service); - // 补全表配置 - customizers.forEach(customizer -> customizer.customize(rule)); - return rule; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java deleted file mode 100644 index 4e8a921..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/annotation/DataPermission.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.annotation; - -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; - -import java.lang.annotation.*; - -/** - * 数据权限注解 - * 可声明在类或者方法上,标识使用的数据权限规则 - * - * @author 芋道源码 - */ -@Target({ElementType.TYPE, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface DataPermission { - - /** - * 当前类或方法是否开启数据权限 - * 即使不添加 @DataPermission 注解,默认是开启状态 - * 可通过设置 enable 为 false 禁用 - */ - boolean enable() default true; - - /** - * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} - */ - Class[] includeRules() default {}; - - /** - * 排除的数据权限规则数组,优先级最低 - */ - Class[] excludeRules() default {}; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java deleted file mode 100644 index 03d212c..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.aop; - -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import org.aopalliance.aop.Advice; -import org.springframework.aop.Pointcut; -import org.springframework.aop.support.AbstractPointcutAdvisor; -import org.springframework.aop.support.ComposablePointcut; -import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; - -/** - * {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 - * - * @author 芋道源码 - */ -@Getter -@EqualsAndHashCode(callSuper = true) -public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { - - private final Advice advice; - - private final Pointcut pointcut; - - public DataPermissionAnnotationAdvisor() { - this.advice = new DataPermissionAnnotationInterceptor(); - this.pointcut = this.buildPointcut(); - } - - protected Pointcut buildPointcut() { - Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); - Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); - return new ComposablePointcut(classPointcut).union(methodPointcut); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java deleted file mode 100644 index 48a0354..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java +++ /dev/null @@ -1,72 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.aop; - -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import lombok.Getter; -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; -import org.springframework.core.MethodClassKey; -import org.springframework.core.annotation.AnnotationUtils; - -import java.lang.reflect.Method; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * {@link DataPermission} 注解的拦截器 - * 1. 在执行方法前,将 @DataPermission 注解入栈 - * 2. 在执行方法后,将 @DataPermission 注解出栈 - * - * @author 芋道源码 - */ -@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 -public class DataPermissionAnnotationInterceptor implements MethodInterceptor { - - /** - * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 - */ - static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); - - @Getter - private final Map dataPermissionCache = new ConcurrentHashMap<>(); - - @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { - // 入栈 - DataPermission dataPermission = this.findAnnotation(methodInvocation); - if (dataPermission != null) { - DataPermissionContextHolder.add(dataPermission); - } - try { - // 执行逻辑 - return methodInvocation.proceed(); - } finally { - // 出栈 - if (dataPermission != null) { - DataPermissionContextHolder.remove(); - } - } - } - - private DataPermission findAnnotation(MethodInvocation methodInvocation) { - // 1. 从缓存中获取 - Method method = methodInvocation.getMethod(); - Object targetObject = methodInvocation.getThis(); - Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); - MethodClassKey methodClassKey = new MethodClassKey(method, clazz); - DataPermission dataPermission = dataPermissionCache.get(methodClassKey); - if (dataPermission != null) { - return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; - } - - // 2.1 从方法中获取 - dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); - // 2.2 从类上获取 - if (dataPermission == null) { - dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); - } - // 2.3 添加到缓存中 - dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); - return dataPermission; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java deleted file mode 100644 index 9705d88..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolder.java +++ /dev/null @@ -1,72 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.aop; - -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import com.alibaba.ttl.TransmittableThreadLocal; - -import java.util.LinkedList; -import java.util.List; - -/** - * {@link DataPermission} 注解的 Context 上下文 - * - * @author 芋道源码 - */ -public class DataPermissionContextHolder { - - /** - * 使用 List 的原因,可能存在方法的嵌套调用 - */ - private static final ThreadLocal> DATA_PERMISSIONS = - TransmittableThreadLocal.withInitial(LinkedList::new); - - /** - * 获得当前的 DataPermission 注解 - * - * @return DataPermission 注解 - */ - public static DataPermission get() { - return DATA_PERMISSIONS.get().peekLast(); - } - - /** - * 入栈 DataPermission 注解 - * - * @param dataPermission DataPermission 注解 - */ - public static void add(DataPermission dataPermission) { - DATA_PERMISSIONS.get().addLast(dataPermission); - } - - /** - * 出栈 DataPermission 注解 - * - * @return DataPermission 注解 - */ - public static DataPermission remove() { - DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); - // 无元素时,清空 ThreadLocal - if (DATA_PERMISSIONS.get().isEmpty()) { - DATA_PERMISSIONS.remove(); - } - return dataPermission; - } - - /** - * 获得所有 DataPermission - * - * @return DataPermission 队列 - */ - public static List getAll() { - return DATA_PERMISSIONS.get(); - } - - /** - * 清空上下文 - * - * 目前仅仅用于单测 - */ - public static void clear() { - DATA_PERMISSIONS.remove(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java deleted file mode 100644 index e181b19..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java +++ /dev/null @@ -1,507 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.db; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.util.collection.SetUtils; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import com.alibaba.ttl.TransmittableThreadLocal; -import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; -import com.baomidou.mybatisplus.core.toolkit.PluginUtils; -import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; -import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import net.sf.jsqlparser.expression.*; -import net.sf.jsqlparser.expression.operators.conditional.AndExpression; -import net.sf.jsqlparser.expression.operators.conditional.OrExpression; -import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.expression.operators.relational.InExpression; -import net.sf.jsqlparser.expression.operators.relational.ItemsList; -import net.sf.jsqlparser.schema.Table; -import net.sf.jsqlparser.statement.delete.Delete; -import net.sf.jsqlparser.statement.select.*; -import net.sf.jsqlparser.statement.update.Update; -import org.apache.ibatis.executor.Executor; -import org.apache.ibatis.executor.statement.StatementHandler; -import org.apache.ibatis.mapping.BoundSql; -import org.apache.ibatis.mapping.MappedStatement; -import org.apache.ibatis.mapping.SqlCommandType; -import org.apache.ibatis.session.ResultHandler; -import org.apache.ibatis.session.RowBounds; - -import java.sql.Connection; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 - * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法 - * - * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 - * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { - - private final DataPermissionRuleFactory ruleFactory; - - @Getter - private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); - - @Override // SELECT 场景 - public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, - RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { - // 获得 Mapper 对应的数据权限的规则 - List rules = ruleFactory.getDataPermissionRule(ms.getId()); - if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 - return; - } - - PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); - try { - // 初始化上下文 - ContextHolder.init(rules); - // 处理 SQL - mpBs.sql(parserSingle(mpBs.sql(), null)); - } finally { - addMappedStatementCache(ms); - ContextHolder.clear(); - } - } - - @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景 - public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { - PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); - MappedStatement ms = mpSh.mappedStatement(); - SqlCommandType sct = ms.getSqlCommandType(); - if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { - // 获得 Mapper 对应的数据权限的规则 - List rules = ruleFactory.getDataPermissionRule(ms.getId()); - if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 - return; - } - - PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); - try { - // 初始化上下文 - ContextHolder.init(rules); - // 处理 SQL - mpBs.sql(parserMulti(mpBs.sql(), null)); - } finally { - addMappedStatementCache(ms); - ContextHolder.clear(); - } - } - } - - @Override - protected void processSelect(Select select, int index, String sql, Object obj) { - processSelectBody(select.getSelectBody()); - List withItemsList = select.getWithItemsList(); - if (!CollectionUtils.isEmpty(withItemsList)) { - withItemsList.forEach(this::processSelectBody); - } - } - - protected void processSelectBody(SelectBody selectBody) { - if (selectBody == null) { - return; - } - if (selectBody instanceof PlainSelect) { - processPlainSelect((PlainSelect) selectBody); - } else if (selectBody instanceof WithItem) { - WithItem withItem = (WithItem) selectBody; - processSelectBody(withItem.getSubSelect().getSelectBody()); - } else { - SetOperationList operationList = (SetOperationList) selectBody; - List selectBodys = operationList.getSelects(); - if (CollectionUtils.isNotEmpty(selectBodys)) { - selectBodys.forEach(this::processSelectBody); - } - } - } - - /** - * update 语句处理 - */ - @Override - protected void processUpdate(Update update, int index, String sql, Object obj) { - final Table table = update.getTable(); - update.setWhere(this.builderExpression(update.getWhere(), table)); - } - - /** - * delete 语句处理 - */ - @Override - protected void processDelete(Delete delete, int index, String sql, Object obj) { - delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); - } - - /** - * 处理 PlainSelect - */ - protected void processPlainSelect(PlainSelect plainSelect) { - FromItem fromItem = plainSelect.getFromItem(); - Expression where = plainSelect.getWhere(); - processWhereSubSelect(where); - if (fromItem instanceof Table) { - Table fromTable = (Table) fromItem; - plainSelect.setWhere(builderExpression(where, fromTable)); - } else { - processFromItem(fromItem); - } - //#3087 github - List selectItems = plainSelect.getSelectItems(); - if (CollectionUtils.isNotEmpty(selectItems)) { - selectItems.forEach(this::processSelectItem); - } - List joins = plainSelect.getJoins(); - if (CollectionUtils.isNotEmpty(joins)) { - processJoins(joins); - } - } - - /** - * 处理where条件内的子查询 - *

- * 支持如下: - * 1. in - * 2. = - * 3. > - * 4. < - * 5. >= - * 6. <= - * 7. <> - * 8. EXISTS - * 9. NOT EXISTS - *

- * 前提条件: - * 1. 子查询必须放在小括号中 - * 2. 子查询一般放在比较操作符的右边 - * - * @param where where 条件 - */ - protected void processWhereSubSelect(Expression where) { - if (where == null) { - return; - } - if (where instanceof FromItem) { - processFromItem((FromItem) where); - return; - } - if (where.toString().indexOf("SELECT") > 0) { - // 有子查询 - if (where instanceof BinaryExpression) { - // 比较符号 , and , or , 等等 - BinaryExpression expression = (BinaryExpression) where; - processWhereSubSelect(expression.getLeftExpression()); - processWhereSubSelect(expression.getRightExpression()); - } else if (where instanceof InExpression) { - // in - InExpression expression = (InExpression) where; - ItemsList itemsList = expression.getRightItemsList(); - if (itemsList instanceof SubSelect) { - processSelectBody(((SubSelect) itemsList).getSelectBody()); - } - } else if (where instanceof ExistsExpression) { - // exists - ExistsExpression expression = (ExistsExpression) where; - processWhereSubSelect(expression.getRightExpression()); - } else if (where instanceof NotExpression) { - // not exists - NotExpression expression = (NotExpression) where; - processWhereSubSelect(expression.getExpression()); - } else if (where instanceof Parenthesis) { - Parenthesis expression = (Parenthesis) where; - processWhereSubSelect(expression.getExpression()); - } - } - } - - protected void processSelectItem(SelectItem selectItem) { - if (selectItem instanceof SelectExpressionItem) { - SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; - if (selectExpressionItem.getExpression() instanceof SubSelect) { - processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); - } else if (selectExpressionItem.getExpression() instanceof Function) { - processFunction((Function) selectExpressionItem.getExpression()); - } - } - } - - /** - * 处理函数 - *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

- *

fixed gitee pulls/141

- * - * @param function 函数 - */ - protected void processFunction(Function function) { - ExpressionList parameters = function.getParameters(); - if (parameters != null) { - parameters.getExpressions().forEach(expression -> { - if (expression instanceof SubSelect) { - processSelectBody(((SubSelect) expression).getSelectBody()); - } else if (expression instanceof Function) { - processFunction((Function) expression); - } - }); - } - } - - /** - * 处理子查询等 - */ - protected void processFromItem(FromItem fromItem) { - if (fromItem instanceof SubJoin) { - SubJoin subJoin = (SubJoin) fromItem; - if (subJoin.getJoinList() != null) { - processJoins(subJoin.getJoinList()); - } - if (subJoin.getLeft() != null) { - processFromItem(subJoin.getLeft()); - } - } else if (fromItem instanceof SubSelect) { - SubSelect subSelect = (SubSelect) fromItem; - if (subSelect.getSelectBody() != null) { - processSelectBody(subSelect.getSelectBody()); - } - } else if (fromItem instanceof ValuesList) { - logger.debug("Perform a subquery, if you do not give us feedback"); - } else if (fromItem instanceof LateralSubSelect) { - LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; - if (lateralSubSelect.getSubSelect() != null) { - SubSelect subSelect = lateralSubSelect.getSubSelect(); - if (subSelect.getSelectBody() != null) { - processSelectBody(subSelect.getSelectBody()); - } - } - } - } - - /** - * 处理 joins - * - * @param joins join 集合 - */ - private void processJoins(List joins) { - //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 - Deque
tables = new LinkedList<>(); - for (Join join : joins) { - // 处理 on 表达式 - FromItem fromItem = join.getRightItem(); - if (fromItem instanceof Table) { - Table fromTable = (Table) fromItem; - // 获取 join 尾缀的 on 表达式列表 - Collection originOnExpressions = join.getOnExpressions(); - // 正常 join on 表达式只有一个,立刻处理 - if (originOnExpressions.size() == 1) { - processJoin(join); - continue; - } - tables.push(fromTable); - // 尾缀多个 on 表达式的时候统一处理 - if (originOnExpressions.size() > 1) { - Collection onExpressions = new LinkedList<>(); - for (Expression originOnExpression : originOnExpressions) { - Table currentTable = tables.poll(); - onExpressions.add(builderExpression(originOnExpression, currentTable)); - } - join.setOnExpressions(onExpressions); - } - } else { - // 处理右边连接的子表达式 - processFromItem(fromItem); - } - } - } - - /** - * 处理联接语句 - */ - protected void processJoin(Join join) { - if (join.getRightItem() instanceof Table) { - Table fromTable = (Table) join.getRightItem(); - Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions()); - originOnExpression = builderExpression(originOnExpression, fromTable); - join.setOnExpressions(CollUtil.newArrayList(originOnExpression)); - } - } - - /** - * 处理条件 - */ - protected Expression builderExpression(Expression currentExpression, Table table) { - // 获得 Table 对应的数据权限条件 - Expression equalsTo = buildDataPermissionExpression(table); - if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认 - return currentExpression; - } - - // 表达式为空,则直接返回 equalsTo - if (currentExpression == null) { - return equalsTo; - } - // 如果表达式为 Or,则需要 (currentExpression) AND equalsTo - if (currentExpression instanceof OrExpression) { - return new AndExpression(new Parenthesis(currentExpression), equalsTo); - } - // 如果表达式为 And,则直接返回 currentExpression AND equalsTo - return new AndExpression(currentExpression, equalsTo); - } - - /** - * 构建指定表的数据权限的 Expression 过滤条件 - * - * @param table 表 - * @return Expression 过滤条件 - */ - private Expression buildDataPermissionExpression(Table table) { - // 生成条件 - Expression allExpression = null; - for (DataPermissionRule rule : ContextHolder.getRules()) { - // 判断表名是否匹配 - if (!rule.getTableNames().contains(table.getName())) { - continue; - } - // 单条规则的条件 - String tableName = MyBatisUtils.getTableName(table); - Expression oneExpress = rule.getExpression(tableName, table.getAlias()); - // 拼接到 allExpression 中 - allExpression = allExpression == null ? oneExpress - : new AndExpression(allExpression, oneExpress); - } - - // 如果条件非空,说明已经重写了 - if (allExpression != null) { - ContextHolder.setRewrite(true); - } - return allExpression; - } - - /** - * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 - * - * @param ms MappedStatement - */ - private void addMappedStatementCache(MappedStatement ms) { - if (ContextHolder.getRewrite()) { - return; - } - // 有重写,进行添加 - mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); - } - - /** - * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 - * - * @author 芋道源码 - */ - static final class ContextHolder { - - /** - * 该 {@link MappedStatement} 对应的规则 - */ - private static final ThreadLocal> RULES = new TransmittableThreadLocal<>(); - /** - * SQL 是否进行重写 - */ - private static final ThreadLocal REWRITE = new TransmittableThreadLocal<>(); - - public static void init(List rules) { - RULES.set(rules); - REWRITE.set(false); - } - - public static void clear() { - RULES.remove(); - REWRITE.remove(); - } - - public static boolean getRewrite() { - return REWRITE.get(); - } - - public static void setRewrite(boolean rewrite) { - REWRITE.set(rewrite); - } - - public static List getRules() { - return RULES.get(); - } - - } - - /** - * {@link MappedStatement} 缓存 - * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 - * 如果无效,则可以避免 SQL 的解析,加快速度 - * - * @author 芋道源码 - */ - static final class MappedStatementCache { - - /** - * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 - * - * value:{@link MappedStatement#getId()} 编号 - */ - @Getter - private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); - - /** - * 判断是否无需重写 - * ps:虽然有点中文式英语,但是容易读懂即可 - * - * @param ms MappedStatement - * @param rules 数据权限规则数组 - * @return 是否无需重写 - */ - public boolean noRewritable(MappedStatement ms, List rules) { - // 如果规则为空,说明无需重写 - if (CollUtil.isEmpty(rules)) { - return true; - } - // 任一规则不在 noRewritableMap 中,则说明可能需要重写 - for (DataPermissionRule rule : rules) { - Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); - if (!CollUtil.contains(mappedStatementIds, ms.getId())) { - return false; - } - } - return true; - } - - /** - * 添加无需重写的 MappedStatement - * - * @param ms MappedStatement - * @param rules 数据权限规则数组 - */ - public void addNoRewritable(MappedStatement ms, List rules) { - for (DataPermissionRule rule : rules) { - Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); - if (CollUtil.isNotEmpty(mappedStatementIds)) { - mappedStatementIds.add(ms.getId()); - } else { - noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); - } - } - } - - /** - * 清空缓存 - * 目前主要提供给单元测试 - */ - public void clear() { - noRewritableMappedStatements.clear(); - } - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java deleted file mode 100644 index 20daa85..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * 基于部门的数据权限规则 - * - * @author 芋道源码 - */ -package cn.iocoder.yudao.framework.datapermission.core.dept; diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java deleted file mode 100644 index 4f7d18d..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRule.java +++ /dev/null @@ -1,186 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.LongValue; -import net.sf.jsqlparser.expression.operators.conditional.OrExpression; -import net.sf.jsqlparser.expression.operators.relational.EqualsTo; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.expression.operators.relational.InExpression; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 - * - * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 - * - * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? - * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-server 采用该方案】 - * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 - * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 - * 最终过滤条件是 WHERE dept_id = ? - * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; - * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) - * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; - * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) - * - * @author 芋道源码 - */ -@AllArgsConstructor -@Slf4j -public class DeptDataPermissionRule implements DataPermissionRule { - - private static final String DEPT_COLUMN_NAME = "dept_id"; - private static final String USER_COLUMN_NAME = "user_id"; - - private final DeptDataPermissionFrameworkService deptDataPermissionService; - - /** - * 基于部门的表字段配置 - * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 - * - * key:表名 - * value:字段名 - */ - private final Map deptColumns = new HashMap<>(); - /** - * 基于用户的表字段配置 - * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 - * - * key:表名 - * value:字段名 - */ - private final Map userColumns = new HashMap<>(); - /** - * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 - */ - private final Set TABLE_NAMES = new HashSet<>(); - - @Override - public Set getTableNames() { - return TABLE_NAMES; - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - // 只有有登陆用户的情况下,才进行数据权限的处理 - LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); - if (loginUser == null) { - return null; - } - - // 获得数据权限 - DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser); - if (deptDataPermission == null) { - log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); - throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", - loginUser.getId(), tableName, tableAlias.getName())); - } - - // 情况一,如果是 ALL 可查看全部,则无需拼接条件 - if (deptDataPermission.getAll()) { - return null; - } - - // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 - if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) - && Boolean.FALSE.equals(deptDataPermission.getSelf())) { - return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 - } - - // 情况三,拼接 Dept 和 User 的条件,最后组合 - Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); - Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); - if (deptExpression == null && userExpression == null) { - log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", - JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); - throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", - loginUser.getId(), tableName, tableAlias.getName())); - } - if (deptExpression == null) { - return userExpression; - } - if (userExpression == null) { - return deptExpression; - } - // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ? - return new OrExpression(deptExpression, userExpression); - } - - private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { - // 如果不存在配置,则无需作为条件 - String columnName = deptColumns.get(tableName); - if (StrUtil.isEmpty(columnName)) { - return null; - } - // 如果为空,则无条件 - if (CollUtil.isEmpty(deptIds)) { - return null; - } - // 拼接条件 - return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), - new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); - } - - private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { - // 如果不查看自己,则无需作为条件 - if (Boolean.FALSE.equals(self)) { - return null; - } - String columnName = userColumns.get(tableName); - if (StrUtil.isEmpty(columnName)) { - return null; - } - // 拼接条件 - return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); - } - - // ==================== 添加配置 ==================== - - public void addDeptColumn(Class entityClass) { - addDeptColumn(entityClass, DEPT_COLUMN_NAME); - } - - public void addDeptColumn(Class entityClass, String columnName) { - String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); - addDeptColumn(tableName, columnName); - } - - public void addDeptColumn(String tableName, String columnName) { - deptColumns.put(tableName, columnName); - TABLE_NAMES.add(tableName); - } - - public void addUserColumn(Class entityClass) { - addUserColumn(entityClass, USER_COLUMN_NAME); - } - - public void addUserColumn(Class entityClass, String columnName) { - String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); - addUserColumn(tableName, columnName); - } - - public void addUserColumn(String tableName, String columnName) { - userColumns.put(tableName, columnName); - TABLE_NAMES.add(tableName); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java deleted file mode 100644 index 5341ee5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleCustomizer.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -/** - * {@link DeptDataPermissionRule} 的自定义配置接口 - * - * @author 芋道源码 - */ -@FunctionalInterface -public interface DeptDataPermissionRuleCustomizer { - - /** - * 自定义该权限规则 - * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 - * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 - * - * @param rule 权限规则 - */ - void customize(DeptDataPermissionRule rule); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java deleted file mode 100644 index 3ee6167..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/DeptDataPermissionFrameworkService.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.service; - -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; - -/** - * 基于部门的数据权限 Framework Service 接口 - * 目前的实现类是 SysPermissionServiceImpl 类 - * - * @author 芋道源码 - */ -public interface DeptDataPermissionFrameworkService { - - /** - * 获得登陆用户的部门数据权限 - * - * @param loginUser 登陆用户 - * @return 部门数据权限 - */ - DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java deleted file mode 100644 index 897fb22..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/dept/service/dto/DeptDataPermissionRespDTO.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.service.dto; - -import lombok.Data; - -import java.util.HashSet; -import java.util.Set; - -/** - * 部门的数据权限 Response DTO - * - * @author 芋道源码 - */ -@Data -public class DeptDataPermissionRespDTO { - - /** - * 是否可查看全部数据 - */ - private Boolean all; - /** - * 是否可查看自己的数据 - */ - private Boolean self; - /** - * 可查看的部门编号数组 - */ - private Set deptIds; - - public DeptDataPermissionRespDTO() { - this.all = false; - this.self = false; - this.deptIds = new HashSet<>(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java deleted file mode 100644 index 2bccde8..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRule.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.rule; - -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; - -import java.util.Set; - -/** - * 数据权限规则接口 - * 通过实现接口,自定义数据规则。例如说, - * - * @author 芋道源码 - */ -public interface DataPermissionRule { - - /** - * 返回需要生效的表名数组 - * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 - * - * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 - * - * @return 表名数组 - */ - Set getTableNames(); - - /** - * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 - * - * @param tableName 表名 - * @param tableAlias 别名,可能为空 - * @return 过滤条件 Expression 表达式 - */ - Expression getExpression(String tableName, Alias tableAlias); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java deleted file mode 100644 index 166dfea..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.rule; - -import java.util.List; - -/** - * {@link DataPermissionRule} 工厂接口 - * 作为 {@link DataPermissionRule} 的容器,提供管理能力 - * - * @author 芋道源码 - */ -public interface DataPermissionRuleFactory { - - /** - * 获得所有数据权限规则数组 - * - * @return 数据权限规则数组 - */ - List getDataPermissionRules(); - - /** - * 获得指定 Mapper 的数据权限规则数组 - * - * @param mappedStatementId 指定 Mapper 的编号 - * @return 数据权限规则数组 - */ - List getDataPermissionRule(String mappedStatementId); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java deleted file mode 100644 index eaa6e6a..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; -import lombok.RequiredArgsConstructor; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 默认的 DataPermissionRuleFactoryImpl 实现类 - * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 - * - * @author 芋道源码 - */ -@RequiredArgsConstructor -public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { - - /** - * 数据权限规则数组 - */ - private final List rules; - - @Override - public List getDataPermissionRules() { - return rules; - } - - @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 - public List getDataPermissionRule(String mappedStatementId) { - // 1. 无数据权限 - if (CollUtil.isEmpty(rules)) { - return Collections.emptyList(); - } - // 2. 未配置,则默认开启 - DataPermission dataPermission = DataPermissionContextHolder.get(); - if (dataPermission == null) { - return rules; - } - // 3. 已配置,但禁用 - if (!dataPermission.enable()) { - return Collections.emptyList(); - } - - // 4. 已配置,只选择部分规则 - if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { - return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) - .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 - } - // 5. 已配置,只排除部分规则 - if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { - return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) - .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 - } - // 6. 已配置,全部规则 - return rules; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java deleted file mode 100644 index 831aa7c..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 - */ -package cn.iocoder.yudao.framework.datapermission; diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 1a4c029..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,3 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\ - cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java deleted file mode 100644 index ba97ede..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.aop; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import org.aopalliance.intercept.MethodInvocation; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.lang.reflect.Method; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -/** - * {@link DataPermissionAnnotationInterceptor} 的单元测试 - * - * @author 芋道源码 - */ -public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { - - @InjectMocks - private DataPermissionAnnotationInterceptor interceptor; - - @Mock - private MethodInvocation methodInvocation; - - @BeforeEach - public void setUp() { - interceptor.getDataPermissionCache().clear(); - } - - @Test // 无 @DataPermission 注解 - public void testInvoke_none() throws Throwable { - // 参数 - mockMethodInvocation(TestNone.class); - - // 调用 - Object result = interceptor.invoke(methodInvocation); - // 断言 - assertEquals("none", result); - assertEquals(1, interceptor.getDataPermissionCache().size()); - assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); - } - - @Test // 在 Method 上有 @DataPermission 注解 - public void testInvoke_method() throws Throwable { - // 参数 - mockMethodInvocation(TestMethod.class); - - // 调用 - Object result = interceptor.invoke(methodInvocation); - // 断言 - assertEquals("method", result); - assertEquals(1, interceptor.getDataPermissionCache().size()); - assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); - } - - @Test // 在 Class 上有 @DataPermission 注解 - public void testInvoke_class() throws Throwable { - // 参数 - mockMethodInvocation(TestClass.class); - - // 调用 - Object result = interceptor.invoke(methodInvocation); - // 断言 - assertEquals("class", result); - assertEquals(1, interceptor.getDataPermissionCache().size()); - assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); - } - - private void mockMethodInvocation(Class clazz) throws Throwable { - Object targetObject = clazz.newInstance(); - Method method = targetObject.getClass().getMethod("echo"); - when(methodInvocation.getThis()).thenReturn(targetObject); - when(methodInvocation.getMethod()).thenReturn(method); - when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); - } - - static class TestMethod { - - @DataPermission(enable = false) - public String echo() { - return "method"; - } - - } - - @DataPermission(enable = false) - static class TestClass { - - public String echo() { - return "class"; - } - - } - - static class TestNone { - - public String echo() { - return "none"; - } - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java deleted file mode 100644 index 688b92d..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/aop/DataPermissionContextHolderTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.aop; - -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.mockito.Mockito.mock; - -/** - * {@link DataPermissionContextHolder} 的单元测试 - * - * @author 芋道源码 - */ -class DataPermissionContextHolderTest { - - @BeforeEach - public void setUp() { - DataPermissionContextHolder.clear(); - } - - @Test - public void testGet() { - // mock 方法 - DataPermission dataPermission01 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission01); - DataPermission dataPermission02 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission02); - - // 调用 - DataPermission result = DataPermissionContextHolder.get(); - // 断言 - assertSame(result, dataPermission02); - } - - @Test - public void testPush() { - // 调用 - DataPermission dataPermission01 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission01); - DataPermission dataPermission02 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission02); - // 断言 - DataPermission first = DataPermissionContextHolder.getAll().get(0); - DataPermission second = DataPermissionContextHolder.getAll().get(1); - assertSame(dataPermission01, first); - assertSame(dataPermission02, second); - } - - @Test - public void testRemove() { - // mock 方法 - DataPermission dataPermission01 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission01); - DataPermission dataPermission02 = mock(DataPermission.class); - DataPermissionContextHolder.add(dataPermission02); - - // 调用 - DataPermission result = DataPermissionContextHolder.remove(); - // 断言 - assertSame(result, dataPermission02); - assertEquals(1, DataPermissionContextHolder.getAll().size()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java deleted file mode 100644 index 36bf5c0..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.db; - -import cn.iocoder.yudao.framework.common.util.collection.SetUtils; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import com.baomidou.mybatisplus.core.toolkit.PluginUtils; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.LongValue; -import net.sf.jsqlparser.expression.operators.relational.EqualsTo; -import net.sf.jsqlparser.schema.Column; -import org.apache.ibatis.executor.Executor; -import org.apache.ibatis.executor.statement.StatementHandler; -import org.apache.ibatis.mapping.BoundSql; -import org.apache.ibatis.mapping.MappedStatement; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; - -import java.sql.Connection; -import java.util.*; - -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * {@link DataPermissionDatabaseInterceptor} 的单元测试 - * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} - * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} - * 以及在这个过程中,ContextHolder 和 MappedStatementCache - * - * @author 芋道源码 - */ -public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { - - @InjectMocks - private DataPermissionDatabaseInterceptor interceptor; - - @Mock - private DataPermissionRuleFactory ruleFactory; - - @BeforeEach - public void setUp() { - // 清理上下文 - DataPermissionDatabaseInterceptor.ContextHolder.clear(); - // 清空缓存 - interceptor.getMappedStatementCache().clear(); - } - - @Test // 不存在规则,且不匹配 - public void testBeforeQuery_withoutRule() { - try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { - // 准备参数 - MappedStatement mappedStatement = mock(MappedStatement.class); - BoundSql boundSql = mock(BoundSql.class); - - // 调用 - interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); - // 断言 - pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql)); - } - } - - @Test // 存在规则,且不匹配 - public void testBeforeQuery_withMatchRule() { - try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { - // 准备参数 - MappedStatement mappedStatement = mock(MappedStatement.class); - BoundSql boundSql = mock(BoundSql.class); - // mock 方法(数据权限) - when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) - .thenReturn(singletonList(new DeptDataPermissionRule())); - // mock 方法(MPBoundSql) - PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); - pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); - // mock 方法(SQL) - String sql = "select * from t_user where id = 1"; - when(mpBs.sql()).thenReturn(sql); - // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 - - // 调用 - interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); - // 断言 - verify(mpBs, times(1)).sql( - eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100")); - // 断言缓存 - assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); - } - } - - @Test // 存在规则,但不匹配 - public void testBeforeQuery_withoutMatchRule() { - try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { - // 准备参数 - MappedStatement mappedStatement = mock(MappedStatement.class); - BoundSql boundSql = mock(BoundSql.class); - // mock 方法(数据权限) - when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) - .thenReturn(singletonList(new DeptDataPermissionRule())); - // mock 方法(MPBoundSql) - PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); - pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); - // mock 方法(SQL) - String sql = "select * from t_role where id = 1"; - when(mpBs.sql()).thenReturn(sql); - // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 - - // 调用 - interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); - // 断言 - verify(mpBs, times(1)).sql( - eq("SELECT * FROM t_role WHERE id = 1")); - // 断言缓存 - assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); - } - } - - @Test - public void testAddNoRewritable() { - // 准备参数 - MappedStatement ms = mock(MappedStatement.class); - List rules = singletonList(new DeptDataPermissionRule()); - // mock 方法 - when(ms.getId()).thenReturn("selectById"); - - // 调用 - interceptor.getMappedStatementCache().addNoRewritable(ms, rules); - // 断言 - Map, Set> noRewritableMappedStatements = - interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); - assertEquals(1, noRewritableMappedStatements.size()); - assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); - } - - @Test - public void testNoRewritable() { - // 准备参数 - MappedStatement ms = mock(MappedStatement.class); - // mock 方法 - when(ms.getId()).thenReturn("selectById"); - // mock 数据 - List rules = singletonList(new DeptDataPermissionRule()); - interceptor.getMappedStatementCache().addNoRewritable(ms, rules); - - // 场景一,rules 为空 - assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); - // 场景二,rules 非空,可重写 - assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); - // 场景三,rule 非空,不可重写 - assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); - } - - private static class DeptDataPermissionRule implements DataPermissionRule { - - private static final String COLUMN = "dept_id"; - - @Override - public Set getTableNames() { - return SetUtils.asSet("t_user"); - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); - LongValue value = new LongValue(100L); - return new EqualsTo(column, value); - } - - } - - private static class EmptyDataPermissionRule implements DataPermissionRule { - - @Override - public Set getTableNames() { - return Collections.emptySet(); - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - return null; - } - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java deleted file mode 100644 index 8c0772f..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java +++ /dev/null @@ -1,370 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.db; - -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; -import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.LongValue; -import net.sf.jsqlparser.expression.operators.relational.EqualsTo; -import net.sf.jsqlparser.expression.operators.relational.ExpressionList; -import net.sf.jsqlparser.expression.operators.relational.InExpression; -import net.sf.jsqlparser.schema.Column; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.util.Arrays; -import java.util.Set; - -import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * {@link DataPermissionDatabaseInterceptor} 的单元测试 - * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 - * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ - * - * @author 芋道源码 - */ -public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { - - @InjectMocks - private DataPermissionDatabaseInterceptor interceptor; - - @Mock - private DataPermissionRuleFactory ruleFactory; - - @BeforeEach - public void setUp() { - // 租户的数据权限规则 - DataPermissionRule tenantRule = new DataPermissionRule() { - - private static final String COLUMN = "tenant_id"; - - @Override - public Set getTableNames() { - return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试 - "t_user", "t_role"); // 满足自己的单元测试 - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); - LongValue value = new LongValue(1L); - return new EqualsTo(column, value); - } - - }; - // 部门的数据权限规则 - DataPermissionRule deptRule = new DataPermissionRule() { - - private static final String COLUMN = "dept_id"; - - @Override - public Set getTableNames() { - return asSet("t_user"); // 满足自己的单元测试 - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); - ExpressionList values = new ExpressionList(new LongValue(10L), - new LongValue(20L)); - return new InExpression(column, values); - } - - }; - // 设置到上下文,保证 - DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); - } - - @Test - void delete() { - assertSql("delete from entity where id = ?", - "DELETE FROM entity WHERE id = ? AND tenant_id = 1"); - } - - @Test - void update() { - assertSql("update entity set name = ? where id = ?", - "UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1"); - } - - @Test - void selectSingle() { - // 单表 - assertSql("select * from entity where id = ?", - "SELECT * FROM entity WHERE id = ? AND tenant_id = 1"); - - assertSql("select * from entity where id = ? or name = ?", - "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); - - assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", - "SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); - - /* not */ - assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", - "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1"); - } - - @Test - void selectSubSelectIn() { - /* in */ - assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - // 在最前 - assertSql("SELECT * FROM entity e WHERE e.id IN " + - "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", - "SELECT * FROM entity e WHERE e.id IN " + - "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); - // 在最后 - assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + - "(select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + - "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - // 在中间 - assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + - "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", - "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + - "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); - } - - @Test - void selectSubSelectEq() { - /* = */ - assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - } - - @Test - void selectSubSelectInnerNotEq() { - /* inner not = */ - assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", - "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); - - assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", - "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); - } - - @Test - void selectSubSelectExists() { - /* EXISTS */ - assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - - - /* NOT EXISTS */ - assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - } - - @Test - void selectSubSelect() { - /* >= */ - assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - - /* <= */ - assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - - /* <> */ - assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", - "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); - } - - @Test - void selectFromSelect() { - assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", - "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); - } - - @Test - void selectBodySubSelect() { - assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", - "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); - } - - @Test - void selectLeftJoin() { - // left join - assertSql("SELECT * FROM entity e " + - "left join entity1 e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - - assertSql("SELECT * FROM entity e " + - "left join entity1 e1 on e1.id = e.id " + - "WHERE (e.id = ? OR e.name = ?)", - "SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - } - - @Test - void selectRightJoin() { - // right join - assertSql("SELECT * FROM entity e " + - "right join entity1 e1 on e1.id = e.id", - "SELECT * FROM entity e " + - "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE e.tenant_id = 1"); - - assertSql("SELECT * FROM entity e " + - "right join entity1 e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM entity e " + - "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - } - - @Test - void selectLeftJoinMultipleTrailingOn() { - // 多个 on 尾缀的 - assertSql("SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 " + - "LEFT JOIN entity2 e2 ON e2.id = e1.id " + - "ON e1.id = e.id " + - "WHERE (e.id = ? OR e.NAME = ?)", - "SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 " + - "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + - "ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); - - assertSql("SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 " + - "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + - "ON e1.id = e.id " + - "WHERE (e.id = ? OR e.NAME = ?)", - "SELECT * FROM entity e " + - "LEFT JOIN entity1 e1 " + - "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + - "ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); - } - - @Test - void selectInnerJoin() { - // inner join - assertSql("SELECT * FROM entity e " + - "inner join entity1 e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM entity e " + - "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - - assertSql("SELECT * FROM entity e " + - "inner join entity1 e1 on e1.id = e.id " + - "WHERE (e.id = ? OR e.name = ?)", - "SELECT * FROM entity e " + - "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - - // 垃圾 inner join todo -// assertSql("SELECT * FROM entity,entity1 " + -// "WHERE entity.id = entity1.id", -// "SELECT * FROM entity e " + -// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + -// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - } - - @Test - void selectWithAs() { - assertSql("with with_as_A as (select * from entity) select * from with_as_A", - "WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A"); - } - - private void assertSql(String sql, String targetSql) { - assertEquals(targetSql, interceptor.parserSingle(sql, null)); - } - - // ========== 额外的测试 ========== - - @Test - public void testSelectSingle() { - // 单表 - assertSql("select * from t_user where id = ?", - "SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)"); - - assertSql("select * from t_user where id = ? or name = ?", - "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); - - assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", - "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); - - /* not */ - assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", - "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); - } - - @Test - public void testSelectLeftJoin() { - // left join - assertSql("SELECT * FROM t_user e " + - "left join t_role e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM t_user e " + - "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - - // 条件 e.id = ? OR e.name = ? 带括号 - assertSql("SELECT * FROM t_user e " + - "left join t_role e1 on e1.id = e.id " + - "WHERE (e.id = ? OR e.name = ?)", - "SELECT * FROM t_user e " + - "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - } - - @Test - public void testSelectRightJoin() { - // right join - assertSql("SELECT * FROM t_user e " + - "right join t_role e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM t_user e " + - "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - - // 条件 e.id = ? OR e.name = ? 带括号 - assertSql("SELECT * FROM t_user e " + - "right join t_role e1 on e1.id = e.id " + - "WHERE (e.id = ? OR e.name = ?)", - "SELECT * FROM t_user e " + - "RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - } - - @Test - public void testSelectInnerJoin() { - // inner join - assertSql("SELECT * FROM t_user e " + - "inner join entity1 e1 on e1.id = e.id " + - "WHERE e.id = ? OR e.name = ?", - "SELECT * FROM t_user e " + - "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - - // 条件 e.id = ? OR e.name = ? 带括号 - assertSql("SELECT * FROM t_user e " + - "inner join t_role e1 on e1.id = e.id " + - "WHERE (e.id = ? OR e.name = ?)", - "SELECT * FROM t_user e " + - "INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + - "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); - - // 垃圾 inner join todo -// assertSql("SELECT * FROM entity,entity1 " + -// "WHERE entity.id = entity1.id", -// "SELECT * FROM entity e " + -// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + -// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java deleted file mode 100644 index 2953e58..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/dept/rule/DeptDataPermissionRuleTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.dept.rule; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.yudao.framework.common.util.collection.SetUtils; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; -import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; - -import java.util.Map; - -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.when; - -/** - * {@link DeptDataPermissionRule} 的单元测试 - * - * @author 芋道源码 - */ -class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { - - @InjectMocks - private DeptDataPermissionRule rule; - - @Mock - private DeptDataPermissionFrameworkService deptDataPermissionFrameworkService; - - @BeforeEach - @SuppressWarnings("unchecked") - public void setUp() { - // 清空 rule - rule.getTableNames().clear(); - ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); - ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); - } - - @Test // 无 LoginUser - public void testGetExpression_noLoginUser() { - // 准备参数 - String tableName = randomString(); - Alias tableAlias = new Alias(randomString()); - // mock 方法 - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertNull(expression); - } - - @Test // 无数据权限时 - public void testGetExpression_noDeptDataPermission() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法 - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - - // 调用 - NullPointerException exception = assertThrows(NullPointerException.class, - () -> rule.getExpression(tableName, tableAlias)); - // 断言 - assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); - } - } - - @Test // 全部数据权限 - public void testGetExpression_allDeptDataPermission() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertNull(expression); - } - } - - @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 - public void testGetExpression_noDept_noSelf() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("null = null", expression.toString()); - } - } - - @Test // 拼接 Dept 和 User 的条件(字段都不符合) - public void testGetExpression_noDeptColumn_noSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - - // 调用 - NullPointerException exception = assertThrows(NullPointerException.class, - () -> rule.getExpression(tableName, tableAlias)); - // 断言 - assertEquals("LoginUser(1) Table(t_user/u) 构建的条件为空", exception.getMessage()); - } - } - - @Test // 拼接 Dept 和 User 的条件(self 符合) - public void testGetExpression_noDeptColumn_yesSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - // 添加 user 字段配置 - rule.addUserColumn("t_user", "id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.id = 1", expression.toString()); - } - } - - @Test // 拼接 Dept 和 User 的条件(dept 符合) - public void testGetExpression_yesDeptColumn_noSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - // 添加 dept 字段配置 - rule.addDeptColumn("t_user", "dept_id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.dept_id IN (10, 20)", expression.toString()); - } - } - - @Test // 拼接 Dept 和 User 的条件(dept + self 符合) - public void testGetExpression_yesDeptColumn_yesSelfColumn() { - try (MockedStatic securityFrameworkUtilsMock - = mockStatic(SecurityFrameworkUtils.class)) { - // 准备参数 - String tableName = "t_user"; - Alias tableAlias = new Alias("u"); - // mock 方法(LoginUser) - LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); - securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); - // mock 方法(DeptDataPermissionRespDTO) - DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() - .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); - when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); - // 添加 user 字段配置 - rule.addUserColumn("t_user", "id"); - // 添加 dept 字段配置 - rule.addDeptColumn("t_user", "dept_id"); - - // 调用 - Expression expression = rule.getExpression(tableName, tableAlias); - // 断言 - assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); - } - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java deleted file mode 100644 index 17dddc9..0000000 --- a/yudao-framework/yudao-spring-boot-starter-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package cn.iocoder.yudao.framework.datapermission.core.rule; - -import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Spy; -import org.springframework.core.annotation.AnnotationUtils; - -import java.util.Arrays; -import java.util.List; -import java.util.Set; - -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; -import static org.junit.jupiter.api.Assertions.*; - -/** - * {@link DataPermissionRuleFactoryImpl} 单元测试 - * - * @author 芋道源码 - */ -class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { - - @InjectMocks - private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; - - @Spy - private List rules = Arrays.asList(new DataPermissionRule01(), - new DataPermissionRule02()); - - @BeforeEach - public void setUp() { - DataPermissionContextHolder.clear(); - } - - @Test - public void testGetDataPermissionRule_02() { - // 准备参数 - String mappedStatementId = randomString(); - - // 调用 - List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); - // 断言 - assertSame(rules, result); - } - - @Test - public void testGetDataPermissionRule_03() { - // 准备参数 - String mappedStatementId = randomString(); - // mock 方法 - DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); - - // 调用 - List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); - // 断言 - assertTrue(result.isEmpty()); - } - - @Test - public void testGetDataPermissionRule_04() { - // 准备参数 - String mappedStatementId = randomString(); - // mock 方法 - DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); - - // 调用 - List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); - // 断言 - assertEquals(1, result.size()); - assertEquals(DataPermissionRule01.class, result.get(0).getClass()); - } - - @Test - public void testGetDataPermissionRule_05() { - // 准备参数 - String mappedStatementId = randomString(); - // mock 方法 - DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); - - // 调用 - List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); - // 断言 - assertEquals(1, result.size()); - assertEquals(DataPermissionRule02.class, result.get(0).getClass()); - } - - @Test - public void testGetDataPermissionRule_06() { - // 准备参数 - String mappedStatementId = randomString(); - // mock 方法 - DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); - - // 调用 - List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); - // 断言 - assertSame(rules, result); - } - - @DataPermission(enable = false) - static class TestClass03 {} - - @DataPermission(includeRules = DataPermissionRule01.class) - static class TestClass04 {} - - @DataPermission(excludeRules = DataPermissionRule01.class) - static class TestClass05 {} - - @DataPermission - static class TestClass06 {} - - static class DataPermissionRule01 implements DataPermissionRule { - - @Override - public Set getTableNames() { - return null; - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - return null; - } - - } - - static class DataPermissionRule02 implements DataPermissionRule { - - @Override - public Set getTableNames() { - return null; - } - - @Override - public Expression getExpression(String tableName, Alias tableAlias) { - return null; - } - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml b/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml deleted file mode 100644 index f7010cf..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - yudao-framework - cn.iocoder.boot - ${revision} - - 4.0.0 - yudao-spring-boot-starter-tenant - jar - - ${artifactId} - 多租户 - https://github.com/YunaiV/ruoyi-vue-pro - - - - cn.iocoder.boot - yudao-common - - - - - cn.iocoder.boot - yudao-spring-boot-starter-security - - - - - cn.iocoder.boot - yudao-spring-boot-starter-mybatis - - - - cn.iocoder.boot - yudao-spring-boot-starter-redis - - - - - cn.iocoder.boot - yudao-spring-boot-starter-job - - - - - cn.iocoder.boot - yudao-spring-boot-starter-mq - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java deleted file mode 100644 index 33390f6..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -import java.util.Set; - -/** - * 多租户配置 - * - * @author 芋道源码 - */ -@ConfigurationProperties(prefix = "yudao.tenant") -@Data -public class TenantProperties { - -// /** -// * 租户是否开启 -// */ -// private static final Boolean ENABLE_DEFAULT = true; -// -// /** -// * 是否开启 -// */ -// private Boolean enable = ENABLE_DEFAULT; - /** - * 需要多租户的表 - * - * 由于多租户并不作为 yudao 项目的重点功能,更多是扩展性的功能,所以采用正向配置需要多租户的表。 - * 如果需要,你可以改成 ignoreTables 来取消部分不需要的表 - */ - private Set tables; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java deleted file mode 100644 index 1c7d8a7..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantDatabaseAutoConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; -import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; -import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; -import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 多租户针对 DB 的自动配置 - * - * @author 芋道源码 - */ -@Configuration -@EnableConfigurationProperties(TenantProperties.class) -public class YudaoTenantDatabaseAutoConfiguration { - - @Bean - public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, - MybatisPlusInterceptor interceptor) { - TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); - // 添加到 interceptor 中 - // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 - MyBatisUtils.addInterceptor(interceptor, inner, 0); - return inner; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java deleted file mode 100644 index 89b58f8..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import cn.hutool.core.annotation.AnnotationUtil; -import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; -import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; -import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator; -import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 多租户针对 Job 的自动配置 - * - * @author 芋道源码 - */ -@Configuration -public class YudaoTenantJobAutoConfiguration { - - @Bean - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") - public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) { - return new BeanPostProcessor() { - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (!(bean instanceof JobHandler)) { - return bean; - } - // 有 TenantJob 注解的情况下,才会进行处理 - if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) { - return bean; - } - - // 使用 TenantJobHandlerDecorator 装饰 - return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean); - } - - }; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java deleted file mode 100644 index 6a465b9..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantMQAutoConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 多租户针对 MQ 的自动配置 - * - * @author 芋道源码 - */ -@Configuration -public class YudaoTenantMQAutoConfiguration { - - @Bean - public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { - return new TenantRedisMessageInterceptor(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java deleted file mode 100644 index b5dbd00..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; -import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 多租户针对 Web 的自动配置 - * - * @author 芋道源码 - */ -@Configuration -public class YudaoTenantSecurityAutoConfiguration { - - @Bean - public FilterRegistrationBean tenantSecurityWebFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TenantSecurityWebFilter()); - registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); - return registrationBean; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java deleted file mode 100644 index c2830a7..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.config; - -import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; -import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 多租户针对 Web 的自动配置 - * - * @author 芋道源码 - */ -@Configuration -public class YudaoTenantWebAutoConfiguration { - - @Bean - public FilterRegistrationBean tenantContextWebFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TenantContextWebFilter()); - registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); - return registrationBean; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java deleted file mode 100644 index cf5a941..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java +++ /dev/null @@ -1,44 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.context; - -import com.alibaba.ttl.TransmittableThreadLocal; - -/** - * 多租户上下文 Holder - * - * @author 芋道源码 - */ -public class TenantContextHolder { - - private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); - - /** - * 获得租户编号。 - * - * @return 租户编号 - */ - public static Long getTenantId() { - return TENANT_ID.get(); - } - - /** - * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 - * - * @return 租户编号 - */ - public static Long getRequiredTenantId() { - Long tenantId = getTenantId(); - if (tenantId == null) { - throw new NullPointerException("TenantContextHolder 不存在租户编号"); - } - return tenantId; - } - - public static void setTenantId(Long tenantId) { - TENANT_ID.set(tenantId); - } - - public static void clear() { - TENANT_ID.remove(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java deleted file mode 100644 index f4f0ea5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantBaseDO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.db; - -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 拓展多租户的 BaseDO 基类 - * - * @author 芋道源码 - */ -@Data -@EqualsAndHashCode(callSuper = true) -public abstract class TenantBaseDO extends BaseDO { - - /** - * 多租户编号 - */ - private Long tenantId; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java deleted file mode 100644 index 283bd44..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.db; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.tenant.config.TenantProperties; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import com.baomidou.mybatisplus.core.metadata.TableInfo; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; -import lombok.AllArgsConstructor; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.StringValue; - -/** - * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 - * - * @author 芋道源码 - */ -@AllArgsConstructor -public class TenantDatabaseInterceptor implements TenantLineHandler { - - private final TenantProperties properties; - - @Override - public Expression getTenantId() { - return new StringValue(TenantContextHolder.getRequiredTenantId().toString()); - } - - @Override - public boolean ignoreTable(String tableName) { - // 如果实体类继承 TenantBaseDO 类,则是多租户表,不进行忽略 - TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName); - if (tableInfo != null && TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) { - return false; - } - // 不包含,说明要过滤 - return !CollUtil.contains(properties.getTables(), tableName); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java deleted file mode 100644 index fd2ecad..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.job; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * 多租户 Job 注解 - */ -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface TenantJob { -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java deleted file mode 100644 index 25a6e01..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.job; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; -import lombok.AllArgsConstructor; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 多租户 JobHandler 装饰器 - * 任务执行时,会按照租户逐个执行 Job 的逻辑 - * - * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 - * - * @author 芋道源码 - */ -@AllArgsConstructor -public class TenantJobHandlerDecorator implements JobHandler { - - private final TenantFrameworkService tenantFrameworkService; - /** - * 被装饰的 Job - */ - private final JobHandler jobHandler; - - @Override - public final String execute(String param) throws Exception { - // 获得租户列表 - List tenantIds = tenantFrameworkService.getTenantIds(); - if (CollUtil.isEmpty(tenantIds)) { - return null; - } - - // 逐个租户,执行 Job - Map results = new ConcurrentHashMap<>(); - tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 - try { - // 设置租户 - TenantContextHolder.setTenantId(tenantId); - // 执行 Job - String result = jobHandler.execute(param); - // 添加结果 - results.put(tenantId, result); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - TenantContextHolder.clear(); - } - }); - return JsonUtils.toJsonString(results); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java deleted file mode 100644 index 5127150..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.job; - -import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker; - -/** - * 多租户 JobHandlerInvoker 拓展实现类 - * - * @author 芋道源码 - */ -public class TenantJobHandlerInvoker extends JobHandlerInvoker { - - - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java deleted file mode 100644 index 15c72f9..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.mq; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; -import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; - -/** - * 多租户 {@link AbstractRedisMessage} 拦截器 - * - * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 - * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 - * - * @author 芋道源码 - */ -public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { - - private static final String HEADER_TENANT_ID = "tenant-id"; - - @Override - public void sendMessageBefore(AbstractRedisMessage message) { - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId != null) { - message.addHeader(HEADER_TENANT_ID, tenantId.toString()); - } - } - - @Override - public void consumeMessageBefore(AbstractRedisMessage message) { - String tenantIdStr = message.getHeader(HEADER_TENANT_ID); - if (StrUtil.isNotEmpty(tenantIdStr)) { - TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); - } - } - - @Override - public void consumeMessageAfter(AbstractRedisMessage message) { - // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 - TenantContextHolder.clear(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java deleted file mode 100644 index c23bc71..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java +++ /dev/null @@ -1,47 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.redis; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; - -import java.time.Duration; - -/** - * 多租户拓展的 RedisKeyDefine 实现类 - * - * 由于 Redis 不同于 MySQL 有 column 字段,所以无法通过类似 WHERE tenant_id = ? 的方式过滤 - * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是: - * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d, - * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化 - * - * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,是 Redis Key 可能存在冲突的情况。 - * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题 - * - * @author 芋道源码 - */ -public class TenantRedisKeyDefine extends RedisKeyDefine { - - /** - * 多租户的 KEY 模板 - */ - private static final String KEY_TEMPLATE_SUFFIX = ":%d"; - - public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, Duration timeout) { - super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout); - } - - public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class valueType, TimeoutTypeEnum timeoutType) { - super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType); - } - - private static String buildKeyTemplate(String keyTemplate) { - return keyTemplate + KEY_TEMPLATE_SUFFIX; - } - - @Override - public String formatKey(Object... args) { - args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId()); - return super.formatKey(args); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java deleted file mode 100644 index 0801427..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.security; - -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; -import cn.iocoder.yudao.framework.security.core.LoginUser; -import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Objects; - -/** - * 多租户 Security Web 过滤器 - * 校验用户访问的租户,是否是其所在的租户,避免越权问题 - * - * @author 芋道源码 - */ -@Slf4j -public class TenantSecurityWebFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - LoginUser user = SecurityFrameworkUtils.getLoginUser(); - assert user != null; // shouldNotFilter 已经校验 - if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { - log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", - user.getTenantId(), user.getId(), user.getUserType(), - TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); - ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), - "您无权访问该租户的数据")); - return; - } - // 继续过滤 - chain.doFilter(request, response); - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - return SecurityFrameworkUtils.getLoginUser() == null; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java deleted file mode 100644 index a36e612..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.service; - -import java.util.List; - -/** - * Tenant 框架 Service 接口,定义获取租户信息 - * - * @author 芋道源码 - */ -public interface TenantFrameworkService { - - /** - * 获得所有租户 - * - * @return 租户编号数组 - */ - List getTenantIds(); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java deleted file mode 100644 index ec159e0..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.web; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * 多租户 Context Web 过滤器 - * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 - * - * @author 芋道源码 - */ -public class TenantContextWebFilter extends OncePerRequestFilter { - - private static final String HEADER_TENANT_ID = "tenant-id"; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - // 设置 - String tenantId = request.getHeader(HEADER_TENANT_ID); - if (StrUtil.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(Long.valueOf(tenantId)); - } - try { - chain.doFilter(request, response); - } finally { - // 清理 - TenantContextHolder.clear(); - } - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java deleted file mode 100644 index aa22cdb..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 多租户,支持如下层面: - * 1. DB:基于 MyBatis Plus 多租户的功能实现。 - * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 - * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 - * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 - * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 - * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 - * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: - * 1)Spring Async: - * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} - * 2)Spring Security: - * TransmittableThreadLocalSecurityContextHolderStrategy - * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 - * - */ -package cn.iocoder.yudao.framework.tenant; diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 159dfcf..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,6 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java deleted file mode 100644 index d456e01..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.redis; - -import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class TenantRedisKeyDefineTest { - - @Test - public void testFormatKey() { - Long tenantId = 30L; - TenantContextHolder.setTenantId(tenantId); - // 准备参数 - TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH, - Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED); - Long userId = 10L; - Integer userType = 1; - - // 调用 - String key = define.formatKey(userId, userType); - // 断言 - assertEquals("user:10:1:30", key); - } - -} diff --git a/更新日志.md b/更新日志.md index 829154b..03426ff 100644 --- a/更新日志.md +++ b/更新日志.md @@ -29,6 +29,9 @@ * 【新增】多租户,支持 Web、Security、Job、MQ、Async、DB、Redis 组件 * 【新增】数据权限,内置基于部门过滤的规则 * 【新增】用户前台的昵称、头像的修改 +* 【新增】用户前台的微信公众号、微信小程序的社交登陆的 API 接口 + > 完整功能,需要等基于 Uniapp 实现的用户前台一起~ 努力 coding 中,胖友可以 star 持续关注一波! + * 【优化】管理后台的登陆成功后,LoginUser 使用统一方法补全信息 ### 🐞 Bug Fixes -- libgit2 0.21.4