多租户
该扩展组件支持构建多租户的 Jmix 应用程序,多个租户的数据都保存在单一数据库中。一个应用程序实例即可满足多个租户的需求,租户是指彼此隔离且只能访问特定(通常是只读)数据的用户组。
在一个多租户的 Jmix 应用程序中,有两种类型的数据:
-
通用数据:
-
应用程序中所有租户之间共享的数据。
-
租户对这些通用数据具有只读访问权限,这些数据可全局访问,但不能由单个租户修改。
-
-
租户特有的数据:
-
每个租户的特定数据,对其他租户不可见或无法访问。
-
租户可以完全访问自己的租户特定数据,他们可以根据需要与这些数据进行交互或修改,而不会影响属于其他租户的数据。
-
通过在 Jmix 应用程序中实现多租户,开发人员可以有效地管理和向多个客户或用户组提供服务,确保数据隔离、安全性和基于每个租户特定需求的个性化访问控制。这种方式优化了数据的结构和访问,同时维护了不同用户组的数据隐私和完整性。
REST API 扩展组件不能支持多租户。目前还无法按租户分隔实体实例。 |
安装
按照 扩展组件 章节的说明通过 Jmix 市场进行自动安装。
手动安装,在 build.gradle
添加下列依赖:
implementation 'io.jmix.multitenancy:jmix-multitenancy-starter'
implementation 'io.jmix.multitenancy:jmix-multitenancy-flowui-starter'
原理
在项目中,特定于租户的实体应包含带 @TenantId
注解的 String 类型属性。当租户用户加载这些实体时,框架会将 tenant-id 的 WHERE
条件添加到 JPQL 查询语句中,仅读取用户租户的数据。此外,在保存新实体时,tenant-id 属性会自动分配给当前用户的租户。
在项目中,User
实体必须包含 tenant-id 属性。所有租户用户的这个属性都需要设置为一个特定的值。属性值为空的用户(不属于任何租户)可以看到所有租户的数据,全局管理员 可以这样设置,方便配置所有租户和维护整个系统。
Jmix 模块中的下列实体具有 sysTenantId
属性,支持多租户:
-
EntityLogItem
-
SendingMessage
-
SendingAttachment
-
Report
-
ReportGroup
-
ResourceRoleEntity
-
RowLevelRoleEntity
-
FilterConfiguration
租户管理
该扩展组件提供了 Multitenancy(多租户) → Tenants(租户) 界面,支持全局管理员创建和编辑租户。
租户注册记录包含两个属性:
-
Tenant id(租户 ID) - 租户特定实体中使用的租户标识符。创建之后无法修改。
-
Tenant name(租户名称) - 租户的描述名称。
租户用户
在多租户应用程序中,不同租户中的用户可以具有相同的登录名。如需确保 username
属性在整个应用程序中是唯一的,租户用户应使用包含 tenant-id 前缀的用户名进行注册。例如,如果在 t1
和 t2
两个租户中有两个名为 Alice 的不同用户,则其用户名应分别为 t1|alice
和 t2|alice
。
租户用户可以使用完整的用户名(包括 tenant-id,例如 t1|alice
)来登录应用程序。
租户用户可通过两种方式登录应用程序:
-
第一种方法是在访问登录屏幕时使用包含 tenant-id 的 URL 参数,例如
http://localhost:8080/login?tenantId=t1
。此时,用户只需输入不带 tenant-id 前缀的登录名,例如alice
。可以使用
jmix.multitenancy.tenantIdUrlParamName
应用程序属性来设置 URL 参数的自定义名称。 -
用户可以提供完整的用户名,包括 tenant-id,例如
t1|alice
。
除上述方法之外,还可以实现自定义的唯一用户名结构。 |
用户配置
本节中,我们介绍在项目中配置用户管理和认证以支持多租户的流程。
-
在
User
实体中添加一个属性,并使用@TenantId
注解:@TenantId @Column(name = "TENANT") private String tenant; public String getTenant() { return tenant; } public void setTenant(String tenant) { this.tenant = tenant; }
-
在
user-list-view.xml
中的数据网格添加tenant
列:<column property="tenant"/>
-
在
user-detail-view.xml
中添加一个选择租户的字段:<comboBox id="tenantField" property="tenant" readOnly="true"/>
-
在
UserDetailView
类添加下列代码:@ViewComponent private JmixComboBox<String> tenantField; @Autowired private TenantProvider tenantProvider; @Autowired private MultitenancyUiSupport multitenancyUiSupport; @Subscribe public void onInit(final InitEvent event) { timeZoneField.setItems(List.of(TimeZone.getAvailableIDs())); tenantField.setItems(multitenancyUiSupport.getTenantOptions()); } @Subscribe public void onInitEntity(final InitEntityEvent<User> event) { tenantField.setReadOnly(false); usernameField.setReadOnly(false); passwordField.setVisible(true); confirmPasswordField.setVisible(true); } @Subscribe public void onBeforeShow(final BeforeShowEvent event) { String currentTenantId = tenantProvider.getCurrentUserTenantId(); if (!currentTenantId.equals(TenantProvider.NO_TENANT) && Strings.isNullOrEmpty(tenantField.getValue())) { tenantField.setReadOnly(true); tenantField.setValue(currentTenantId); } } @Subscribe("tenantField") public void onTenantFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<JmixComboBox<String>, String> event) { usernameField.setValue( multitenancyUiSupport.getUsernameByTenant(usernameField.getValue(), event.getValue()) ); }
-
如需 按照之前解释的,支持在各个租户之间使用相同的登录名,请在
LoginView
类中添加以下代码:@Autowired private MultitenancyUiSupport multitenancyUiSupport; private Location currentLocation; (1) @Override public void beforeEnter(BeforeEnterEvent event) { currentLocation = event.getLocation(); super.beforeEnter(event); } @Subscribe("login") public void onLogin(final LoginEvent event) { String username = multitenancyUiSupport.getUsernameByLocation(event.getUsername(), currentLocation); try { loginViewSupport.authenticate( AuthDetails.of(username, event.getPassword()) .withLocale(login.getSelectedLocale()) .withRememberMe(login.isRememberMe()) ); } catch (BadCredentialsException | DisabledException | LockedException | AccessDeniedException e) { log.info("Login failed", e); event.getSource().setError(true); } }
安全配置
为租户用户设置角色时,请勿在实体属性策略中包含 tenant-id 属性,因为该属性需要对用户隐藏。例如,如果 Customer
属于租户特定实体,并且包含用 @TenantId
注解的 tenant
属性,则授予此实体访问权限的角色需要显式地指定包含的属性,且不要包含 tenant
属性:
@ResourceRole(name = "UsersRole", code = UsersRole.CODE, scope = "UI")
public interface UsersRole {
String CODE = "users-role";
@EntityAttributePolicy(entityClass = Customer.class,
attributes = {"id", "name", "region", "version"},
action = EntityAttributePolicyAction.MODIFY)
@EntityPolicy(entityClass = Customer.class, actions = EntityPolicyAction.ALL)
void customer();
}