Snowy多租户插件使用教程:企业级应用隔离方案
一、多租户架构概述
1.1 企业级数据隔离痛点
企业在数字化转型过程中,面临多部门、多项目或多客户数据隔离的核心挑战:
- 数据安全风险:不同业务单元数据混杂存储导致越权访问
- 资源浪费严重:为每个租户部署独立系统造成服务器资源利用率低下
- 运维复杂度高:多系统版本并行维护带来的人力成本剧增
- 扩展性瓶颈:传统架构难以支撑租户数量快速增长
1.2 Snowy多租户解决方案
Snowy(小诺方舟)作为国内首个国密前后分离快速开发平台,其多租户插件基于Vue3+AntDesignVue4+SpringBoot3技术栈构建,提供三种隔离模式满足不同场景需求:
| 隔离模式 | 实现原理 | 适用场景 | 数据安全级别 | 部署复杂度 |
|---|---|---|---|---|
| 共享数据库共享表 | 通过tenant_id字段区分数据 | 中小团队SaaS应用 | 中 | 低 |
| 共享数据库独立Schema | 每个租户独立数据库Schema | 部门级数据隔离 | 高 | 中 |
| 独立数据库 | 为租户分配独立数据库实例 | 金融/企业级应用等高安全需求 | 最高 | 高 |
二、环境准备与插件安装
2.1 前置条件
- JDK 17+
- MySQL 8.0+ 或 PostgreSQL 14+
- Maven 3.8+
- Node.js 18+
- Snowy 3.X 基础平台
2.2 插件获取与安装
# 1. 克隆Snowy仓库
git clone https://gitcode.com/xiaonuobase/Snowy.git
cd Snowy
# 2. 启用多租户插件
sed -i 's/<!-- multi-tenant-plugin -->//g' pom.xml
# 3. 编译安装
mvn clean package -DskipTests
# 4. 前端依赖安装
cd snowy-admin-web
npm install
2.3 数据库配置
在application.yml中添加多租户配置:
snowy:
tenant:
enable: true
type: COLUMN # 可选 COLUMN/SCHEMA/DATABASE
column: tenant_id # 租户ID字段名
ignore-tables: sys_user, sys_role # 全局共享表
schema-prefix: tenant_ # Schema模式下的前缀
三、核心功能实现
3.1 租户管理模块
3.1.1 租户创建流程
@Service
public class TenantServiceImpl implements TenantService {
@Autowired
private TenantMapper tenantMapper;
@Autowired
private DataSource dataSource;
@Transactional(rollbackFor = Exception.class)
@Override
public TenantVO createTenant(TenantCreateParam param) {
// 1. 参数校验
validateTenantParam(param);
// 2. 生成租户ID
String tenantId = IdUtil.fastSimpleUUID();
// 3. 创建租户记录
Tenant tenant = new Tenant();
tenant.setId(tenantId);
tenant.setName(param.getName());
tenant.setSchema(param.getSchema());
tenant.setDatabase(param.getDatabase());
tenant.setStatus(TenantStatusEnum.NORMAL);
tenant.setCreateTime(new Date());
tenantMapper.insert(tenant);
// 4. 根据隔离模式初始化资源
if ("SCHEMA".equals(tenantType)) {
createSchema(tenantId);
} else if ("DATABASE".equals(tenantType)) {
createDatabase(tenantId);
}
// 5. 初始化租户管理员
initTenantAdmin(tenantId, param.getAdminAccount());
return convert(tenant);
}
// Schema创建示例
private void createSchema(String tenantId) {
String schemaName = "tenant_" + tenantId;
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);
// 执行初始化SQL脚本
executeSqlScript(conn, schemaName, "classpath:sql/tenant_init.sql");
} catch (SQLException e) {
throw new BusinessException("租户Schema创建失败: " + e.getMessage());
}
}
}
3.1.2 前端租户管理界面
<template>
<a-card :title="t('tenant.management')">
<a-row :gutter="16">
<a-col :span="6">
<a-button
type="primary"
@click="openCreateModal"
:icon="PlusOutlined"
>
{{ t('common.create') }}
</a-button>
</a-col>
<a-col :span="18">
<tenant-search @search="handleSearch" />
</a-col>
</a-row>
<a-table
:columns="columns"
:data-source="tenantList"
:pagination="pagination"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="record.status === 'NORMAL' ? 'green' : 'red'">
{{ t('tenant.status.' + record.status) }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'action'">
<a-space>
<a-button @click="handleEdit(record)">编辑</a-button>
<a-button @click="handleConfig(record)">配置</a-button>
<a-popconfirm
:title="t('common.confirmDelete')"
@confirm="handleDelete(record.id)"
>
<a-button type="primary" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<tenant-create-modal
v-model:visible="createModalVisible"
@success="handleCreateSuccess"
/>
</a-card>
</template>
<script setup>
import { ref } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { useI18n } from 'vue-i18n';
import TenantSearch from './components/TenantSearch.vue';
import TenantCreateModal from './components/TenantCreateModal.vue';
const { t } = useI18n();
const tenantList = ref([]);
const pagination = ref({
current: 1,
pageSize: 10,
total: 0
});
const createModalVisible = ref(false);
// 表格列定义
const columns = [
{
title: '租户名称',
dataIndex: 'name',
key: 'name',
width: 180
},
{
title: '租户ID',
dataIndex: 'id',
key: 'id',
width: 120
},
{
title: '隔离模式',
dataIndex: 'isolationType',
key: 'isolationType',
width: 120,
customRender: (text) => t('tenant.isolationType.' + text)
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 180
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 280
}
];
// 方法定义
const handleSearch = (params) => {
// 搜索逻辑实现
};
const handleTableChange = (pagination) => {
// 分页逻辑实现
};
const openCreateModal = () => {
createModalVisible.value = true;
};
const handleCreateSuccess = () => {
createModalVisible.value = false;
// 刷新表格数据
};
</script>
3.2 数据隔离实现
3.2.1 租户上下文管理
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
/**
* 设置当前租户ID
*/
public static void setTenantId(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
/**
* 获取当前租户ID
*/
public static String getTenantId() {
return CURRENT_TENANT.get();
}
/**
* 清除租户上下文
*/
public static void clear() {
CURRENT_TENANT.remove();
}
}
3.2.2 MyBatis拦截器实现数据过滤
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TenantSqlInterceptor implements Interceptor {
@Autowired
private TenantProperties tenantProperties;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取当前租户ID
String tenantId = TenantContext.getTenantId();
// 2. 如果没有租户ID或忽略表,直接放行
if (StringUtils.isEmpty(tenantId) || isIgnoreTable(invocation)) {
return invocation.proceed();
}
// 3. 获取原始SQL
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String sql = boundSql.getSql();
// 4. 根据隔离类型处理SQL
String handledSql = handleSql(sql, tenantId);
// 5. 设置处理后的SQL
metaObject.setValue("delegate.boundSql.sql", handledSql);
return invocation.proceed();
}
// 根据不同隔离类型处理SQL
private String handleSql(String sql, String tenantId) {
if ("COLUMN".equals(tenantProperties.getType())) {
return addTenantColumn(sql, tenantId);
} else if ("SCHEMA".equals(tenantProperties.getType())) {
return replaceSchema(sql, tenantId);
}
return sql;
}
// 添加租户字段过滤
private String addTenantColumn(String sql, String tenantId) {
// SQL解析逻辑,为查询添加tenant_id条件
// 实现略...
}
// 替换Schema
private String replaceSchema(String sql, String tenantId) {
String schemaName = tenantProperties.getSchemaPrefix() + tenantId;
// 替换表名前缀为租户Schema
// 实现略...
}
}
3.3 国密加密增强
Snowy平台内置国密(SM2/SM3/SM4)加解密功能,可对租户敏感数据进行加密存储:
@Service
public class SensitiveDataService {
@Autowired
private Sm4Util sm4Util;
/**
* 加密敏感数据
*/
public String encrypt(String data, String tenantId) {
// 使用租户专属密钥加密
String key = generateTenantKey(tenantId);
return sm4Util.encryptHex(data, key);
}
/**
* 解密敏感数据
*/
public String decrypt(String encryptedData, String tenantId) {
String key = generateTenantKey(tenantId);
return sm4Util.decryptHex(encryptedData, key);
}
// 生成租户专属密钥
private String generateTenantKey(String tenantId) {
// 基于租户ID和系统根密钥派生
// 实现略...
}
}
四、租户配置与权限管理
4.1 租户参数配置
通过DevConfigService实现租户级参数配置:
@Service
public class TenantConfigService {
@Autowired
private DevConfigMapper configMapper;
/**
* 获取租户配置
*/
public String getConfigValue(String key) {
String tenantId = TenantContext.getTenantId();
DevConfig config = configMapper.selectOne(
Wrappers.<DevConfig>lambdaQuery()
.eq(DevConfig::getKey, key)
.eq(DevConfig::getTenantId, tenantId)
);
return config != null ? config.getValue() : null;
}
/**
* 保存租户配置
*/
@Transactional
public void saveConfig(String key, String value) {
String tenantId = TenantContext.getTenantId();
DevConfig config = new DevConfig();
config.setKey(key);
config.setValue(value);
config.setTenantId(tenantId);
config.setCategory("TENANT_SETTING");
DevConfig existing = configMapper.selectOne(
Wrappers.<DevConfig>lambdaQuery()
.eq(DevConfig::getKey, key)
.eq(DevConfig::getTenantId, tenantId)
);
if (existing != null) {
config.setId(existing.getId());
configMapper.updateById(config);
} else {
configMapper.insert(config);
}
}
}
4.2 租户权限控制
结合Sa-Token实现租户内RBAC权限控制:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SaTokenConfig saTokenConfig() {
SaTokenConfig config = new SaTokenConfig();
config.setTokenName("snowy_token");
config.setTimeout(3600 * 12); // 12小时超时
config.setIsConcurrent(true); // 支持并发登录
config.setIsShare(true); // 同一账号多设备登录共享token
return config;
}
/**
* 租户权限验证规则
*/
@Bean
public StpInterface stpInterface() {
return new StpInterface() {
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
String tenantId = TenantContext.getTenantId();
// 返回租户内用户权限列表
return permissionService.getUserPermissions(loginId.toString(), tenantId);
}
@Override
public List<String> getRoleList(Object loginId, String loginType) {
String tenantId = TenantContext.getTenantId();
// 返回租户内用户角色列表
return roleService.getUserRoles(loginId.toString(), tenantId);
}
};
}
}
五、高级特性与最佳实践
5.1 租户数据迁移工具
提供命令行工具支持租户数据在不同隔离模式间迁移:
# 查看迁移帮助
java -jar snowy-plugin-tenant.jar --help
# 单租户迁移示例
java -jar snowy-plugin-tenant.jar \
--source-type COLUMN \
--target-type SCHEMA \
--tenant-id TENANT2023001 \
--db-url jdbc:mysql://localhost:3306/snowy \
--db-username root \
--db-password 123456
5.2 多租户监控与告警
集成Prometheus监控租户资源使用情况:
# Prometheus配置示例
scrape_configs:
- job_name: 'snowy-tenant'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
# 租户指标示例
tenant_jvm_memory_used_bytes{tenant_id="TENANT001", area="heap"} 124589632.0
tenant_db_connection_count{tenant_id="TENANT001"} 15.0
tenant_api_requests_total{tenant_id="TENANT001", status="success"} 3452.0
5.3 性能优化建议
-
连接池配置:为不同隔离模式优化数据库连接池
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 -
缓存策略:租户级二级缓存配置
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { // 租户缓存键前缀 String tenantId = TenantContext.getTenantId(); String cachePrefix = StringUtils.isEmpty(tenantId) ? "snowy:" : "snowy:" + tenantId + ":"; RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(2)) .prefixCacheNameWith(cachePrefix) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } -
定时任务隔离:使用租户标识隔离定时任务
@Scheduled(cron = "0 0 1 * * ?") public void tenantBackupJob() { // 获取所有租户 List<Tenant> tenants = tenantService.getAllTenants(); for (Tenant tenant : tenants) { try { // 设置租户上下文 TenantContext.setTenantId(tenant.getId()); // 执行租户数据备份 backupService.backupTenantData(tenant.getId()); } catch (Exception e) { log.error("租户[{}]备份失败", tenant.getId(), e); } finally { // 清除上下文 TenantContext.clear(); } } }
六、常见问题与解决方案
6.1 租户数据初始化失败
问题现象:创建租户时数据库表未正确初始化
排查步骤:
- 检查数据库用户是否有CREATE SCHEMA权限
- 查看日志确认初始化SQL脚本是否正确执行
- 验证数据库连接参数是否正确配置
解决方案:
-- 授予数据库权限
GRANT ALL PRIVILEGES ON *.* TO 'snowy'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
6.2 跨租户数据访问问题
问题现象:租户A可以查询到租户B的数据
排查步骤:
- 检查TenantContext是否正确设置租户ID
- 验证MyBatis拦截器是否正常工作
- 确认@Table注解中schema配置是否正确
解决方案:
// 添加租户过滤器测试接口
@GetMapping("/test/tenant/filter")
public String testTenantFilter() {
String tenantId = TenantContext.getTenantId();
if (StringUtils.isEmpty(tenantId)) {
return "租户ID未设置";
}
// 测试查询是否自动添加租户条件
List<User> users = userMapper.selectList(null);
return "查询到" + users.size() + "条数据,租户ID:" + tenantId;
}
七、总结与展望
7.1 功能回顾
Snowy多租户插件通过灵活的隔离策略、完善的租户生命周期管理和严密的权限控制,为企业级应用提供了安全高效的数据隔离解决方案。核心优势包括:
- 国密安全:全程国密加解密保障数据传输和存储安全
- 灵活扩展:三种隔离模式满足不同安全需求
- 无缝集成:与Snowy平台权限、配置等模块深度整合
- 国产化适配:全面支持达梦、人大金仓等国产数据库
7.2 未来规划
- 多租户监控面板:可视化展示各租户资源使用情况
- 智能扩缩容:基于租户负载自动调整资源分配
- 租户模板市场:提供行业化租户初始化模板
- 跨租户数据共享:支持租户间安全数据交换机制
7.3 学习资源
- 官方文档:https://xiaonuobase.gitcode.host/docs/tenant
- 示例项目:https://gitcode.com/xiaonuobase/snowy-tenant-demo
- 视频教程:Snowy平台多租户实战(B站搜索"小诺方舟多租户")
点赞+收藏+关注,获取更多企业级实战教程!下期预告:《Snowy工作流插件与多租户集成方案》
通过本文档学习,您已掌握Snowy多租户插件的核心功能和实施方法。无论是构建SaaS应用还是企业内部多部门系统,Snowy多租户解决方案都能为您提供安全、高效、可扩展的技术支撑,助力业务快速发展。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



