多租户
该扩展组件支持构建多租户的 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();