多租户
该扩展组件支持构建多租户的 Jmix 应用程序,所有租户的数据都保存在单一数据库中。一个应用程序实例即可服务于多个租户,租户是指不同的用户组,组之间互相不可见,仅共享特定的(通常是只读的)数据。
多租户应用程序中有两种类型的数据:
-
租户之间共享的通用数据。租户的用户对这些数据应该仅有只读权限。
-
租户特有的数据,其他租户无法访问。租户享有对此类数据的全部权限。
安装
按照 扩展组件 章节的说明通过 Jmix 市场进行自动安装。
手动安装,在 build.gradle 添加下列依赖:
implementation 'io.jmix.multitenancy:jmix-multitenancy-starter'
implementation 'io.jmix.multitenancy:jmix-multitenancy-ui-starter'
原理
项目中租户特有的实体必须有一个 String 类型的参数,用 @TenantId 注解标注。当租户用户加载此类实体时,框架会将租户 ID 的 WHERE 条件添加到 JPQL 查询语句中,仅读取用户租户的数据。另外,在保存新实体时,租户 ID 属性会自动设置为当前用户的租户。
| 原生 SQL 不会自动过滤,因此租户用户不应该有权限访问任何能提供对原生 SQL 或 Groovy 代码访问的功能(包括JMX 控制台、创建 报表 等)。 |
项目的 User 实体必须包含租户 ID 属性。所有租户用户的这个属性都需要设置为一个特定的值。属性值为空的用户(不属于任何租户)可以看到所有租户的数据,全局管理员 可以这样设置,方便配置所有租户和维护整个系统。
Jmix 模块中的下列实体具有 sysTenantId 属性,支持多租户:
-
EntityLogItem -
SendingMessage -
SendingAttachment -
Report -
ReportGroup -
ResourceRoleEntity -
RowLevelRoleEntity -
FilterConfiguration -
UiTablePresentation
租户管理
该扩展组件提供了 Administration(管理) → Tenants(租户) 界面,支持全局管理员创建和编辑租户。
租户注册记录包含两个属性:
-
Tenant id(租户 ID) - 租户特定实体中使用的租户标识符。创建之后无法修改。
-
Tenant name(租户名称) - 租户的描述名称。
租户用户
不同租户中的用户可能会有相同的登录名。如需在整个应用程序中提供唯一的 username 属性,租户用户需要使用租户 ID 为前缀的 username 进行注册。例如,如果 t1 和 t2 租户中都有叫 Alice 的不同用户,那么她们应该使用分别使用 t1|alice 和 t2|alice 用户名。
租户用户可以有两种登录应用程序的方法:
-
打开登录界面时,使用租户 ID 作为 URL 参数,例如,
http://localhost:8080/#login?tenantId=t1。然后用户可以输入不带租户 ID 前缀的用户名,例如,仅输入alice。可以使用
jmix.multitenancy.tenantIdUrlParamName应用程序属性为该 URL 参数指定不同的名称。 -
用户填写包括租户 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实体实现io.jmix.multitenancy.core.AcceptsTenant接口。getTenantId()方法必须返回带@TenantId注解的属性:public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant { // ... @Override public String getTenantId() { return tenant; } } -
在
user-browse.xml中的表格添加tenant列:<column id="tenant"/> -
在
user-edit.xml中添加一个选择租户的字段:<comboBox id="tenantField" property="tenant" editable="false"/> -
在
UserEdit类添加下列代码:@Autowired private ComboBox<String> tenantField; @Autowired private TenantProvider tenantProvider; @Autowired private MultitenancyUiSupport multitenancyUiSupport; @Subscribe public void onBeforeShow(BeforeShowEvent event) { String currentTenantId = tenantProvider.getCurrentUserTenantId(); if (!currentTenantId.equals(TenantProvider.NO_TENANT) && Strings.isNullOrEmpty(tenantField.getValue())) { tenantField.setEditable(false); tenantField.setValue(currentTenantId); } } @Subscribe("tenantField") public void onTenantFieldValueChange(HasValue.ValueChangeEvent<String> event) { usernameField.setValue( multitenancyUiSupport.getUsernameByTenant( usernameField.getValue(), event.getValue())); } @Subscribe public void onInit(InitEvent event) { tenantField.setOptionsList(multitenancyUiSupport.getTenantOptions()); // ... @Subscribe public void onInitEntity(InitEntityEvent<User> event) { tenantField.setEditable(true); // ... -
如需支持 上面 介绍的不同租户相同登录名的功能,在
LoginScreen类添加下列代码:@Autowired private MultitenancyUiSupport multitenancyUiSupport; @Autowired private UrlRouting urlRouting; private void login() { String username = usernameField.getValue(); // ... // add tenantId prefix if it was provided in the URL username = multitenancyUiSupport.getUsernameByUrl(username, urlRouting); try { loginScreenSupport.authenticate(
安全配置
为租户用户配置角色时,需要在实体属性策略中排除租户 ID 属性,避免用户能查看该字段。例如,如果 Customer 实体是租户特定的实体,并具有 tenant 属性带 @TenantId 注解,则对于该实体访问权限的角色中,需要显式的列出所有的属性并忽略 tenant:
@ResourceRole(name = "Users", code = "users", scope = "UI")
public interface UsersRole {
// ...
@EntityAttributePolicy(
entityClass = Customer.class, attributes = {"region", "name", "version", "id"},
action = EntityAttributePolicyAction.MODIFY)
@EntityPolicy(entityClass = Customer.class, actions = EntityPolicyAction.ALL)
void customer();