Grails4如何为管理员角色全局禁用多租户校验并保持DRY原则
问题重述
现有从Grails 2升级到Grails 4的项目,采用基于discriminator的多租户方案。原Grails 2版本依赖hibernate-filter插件,可直接在Grails过滤器中针对管理员角色全局禁用hibernate多租户过滤逻辑,不需要在业务代码各处编写管理员身份判断分支,保持了代码的*DRY(不要重复自己)*原则。
现需确认Grails 4环境下能否实现相同能力:当请求被认证为管理员角色发起时,可在整个请求的生命周期内完全禁用多租户逻辑,无需在业务代码中散落身份判断分支。
实现方案
Grails 4配套的Hibernate 5.4+本身原生支持discriminator多租户方案,同时也支持过滤器的全局开关,完全可以实现需求,不需要额外引入第三方插件,具体实现步骤如下:
- 第一步:自定义租户解析器,增加全局开关逻辑
重写默认的discriminator租户解析器,用ThreadLocal存储当前请求是否禁用多租户的状态,示例代码:
class CustomTenantResolver implements org.hibernate.context.spi.CurrentTenantIdentifierResolver { private static final ThreadLocal<Boolean> TENANT_DISABLED = ThreadLocal.withInitial { false } private static final ThreadLocal<Serializable> CURRENT_TENANT = ThreadLocal.withInitial { null } @Override String resolveCurrentTenantIdentifier() { if (TENANT_DISABLED.get()) { // 返回公共租户标识,配合过滤器逻辑跳过租户条件拼接 return "PUBLIC" } return CURRENT_TENANT.get() } @Override boolean validateExistingCurrentSessions() { return true } static void disableTenant() { TENANT_DISABLED.set(true) } static void enableTenant() { TENANT_DISABLED.set(false) } static void setCurrentTenant(Serializable tenantId) { CURRENT_TENANT.set(tenantId) } static void clear() { TENANT_DISABLED.remove() CURRENT_TENANT.remove() } }
在application.yml中注册自定义解析器:
hibernate: tenant_identifier_resolver: com.yourpackage.CustomTenantResolver multiTenancy: discriminator: true tenantResolverClass: com.yourpackage.CustomTenantResolver
- 第二步:编写Grails过滤器全局控制开关
在grails-app/conf下创建过滤器,请求进入时判断当前用户是否为管理员,是则调用禁用多租户的方法,请求结束后清除ThreadLocal状态:
class TenantControlFilter { def authenticationService // 项目自身的认证服务,用于获取当前登录用户、角色信息 int order = HIGHEST_PRECEDENCE + 100 // 保证过滤器在认证逻辑之后执行 def filters = { all(controller:'*', action:'*') { before = { if (authenticationService.isLoggedIn() && authenticationService.currentUser.hasRole('ROLE_ADMIN')) { CustomTenantResolver.disableTenant() } else { CustomTenantResolver.enableTenant() // 非管理员场景设置当前请求对应的租户ID CustomTenantResolver.setCurrentTenant(authenticationService.currentUser.tenantId) } return true } afterView = { // 请求结束必须清除ThreadLocal,避免容器线程复用导致状态串扰 CustomTenantResolver.clear() } } } }
- 第三步:调整多租户过滤器的条件拼接逻辑
自定义的discriminator租户过滤器中,判断如果当前处于禁用状态,就不拼接租户查询条件:
class TenantDiscriminatorFilter extends org.hibernate.filter.Filter { // 重写SQL拼接方法 @Override String render(String alias, Map enabledFilters, org.hibernate.engine.spi.SessionFactoryImplementor factory) { if (CustomTenantResolver.TENANT_DISABLED.get()) { // 禁用状态返回空字符串,不会拼接租户过滤条件 return "" } return "${alias}.tenant_id = :tenantId" } }
如果使用GORM自带的discriminator多租户能力,也可以直接在过滤器的before逻辑中,将管理员请求的全链路业务代码包裹在Tenant.withoutId { }块内实现相同效果,上述ThreadLocal方案兼容性更强,适配异步场景更灵活。
注意:一定要在afterView阶段彻底清除ThreadLocal的状态,避免Tomcat等容器的线程池复用线程,导致后续请求拿到错误的租户禁用状态,引发数据权限泄露问题。
该实现方式所有控制逻辑都收敛在过滤器和租户解析器层面,业务代码不需要做任何调整,完全符合DRY原则。
内容的提问来源于stack exchange,提问作者wwwclaes
相关产品推荐
相关产品推荐

