Snowy多租户插件使用教程:企业级应用隔离方案

Snowy多租户插件使用教程:企业级应用隔离方案

【免费下载链接】Snowy 💖国内首个国密前后分离快速开发平台💖《免费商用》,基于开源技术栈精心打造,融合Vue3+AntDesignVue4+Vite5+SpringBoot3+Mp+HuTool+Sa-Token。平台内置国密加解密功能,保障前后端数据传输安全;全面支持国产化环境,适配多种机型、中间件及数据库。特别推荐:插件提供工作流、多租户、多数据源、即时通讯等高级插件,灵活接入,让您的项目开发如虎添翼。 【免费下载链接】Snowy 项目地址: https://gitcode.com/xiaonuobase/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 性能优化建议

  1. 连接池配置:为不同隔离模式优化数据库连接池

    spring:
      datasource:
        hikari:
          maximum-pool-size: 20
          minimum-idle: 5
          connection-timeout: 30000
    
  2. 缓存策略:租户级二级缓存配置

    @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();
    }
    
  3. 定时任务隔离:使用租户标识隔离定时任务

    @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 租户数据初始化失败

问题现象:创建租户时数据库表未正确初始化
排查步骤

  1. 检查数据库用户是否有CREATE SCHEMA权限
  2. 查看日志确认初始化SQL脚本是否正确执行
  3. 验证数据库连接参数是否正确配置

解决方案

-- 授予数据库权限
GRANT ALL PRIVILEGES ON *.* TO 'snowy'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;

6.2 跨租户数据访问问题

问题现象:租户A可以查询到租户B的数据
排查步骤

  1. 检查TenantContext是否正确设置租户ID
  2. 验证MyBatis拦截器是否正常工作
  3. 确认@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 未来规划

  1. 多租户监控面板:可视化展示各租户资源使用情况
  2. 智能扩缩容:基于租户负载自动调整资源分配
  3. 租户模板市场:提供行业化租户初始化模板
  4. 跨租户数据共享:支持租户间安全数据交换机制

7.3 学习资源

  • 官方文档:https://xiaonuobase.gitcode.host/docs/tenant
  • 示例项目:https://gitcode.com/xiaonuobase/snowy-tenant-demo
  • 视频教程:Snowy平台多租户实战(B站搜索"小诺方舟多租户")

点赞+收藏+关注,获取更多企业级实战教程!下期预告:《Snowy工作流插件与多租户集成方案》

通过本文档学习,您已掌握Snowy多租户插件的核心功能和实施方法。无论是构建SaaS应用还是企业内部多部门系统,Snowy多租户解决方案都能为您提供安全、高效、可扩展的技术支撑,助力业务快速发展。

【免费下载链接】Snowy 💖国内首个国密前后分离快速开发平台💖《免费商用》,基于开源技术栈精心打造,融合Vue3+AntDesignVue4+Vite5+SpringBoot3+Mp+HuTool+Sa-Token。平台内置国密加解密功能,保障前后端数据传输安全;全面支持国产化环境,适配多种机型、中间件及数据库。特别推荐:插件提供工作流、多租户、多数据源、即时通讯等高级插件,灵活接入,让您的项目开发如虎添翼。 【免费下载链接】Snowy 项目地址: https://gitcode.com/xiaonuobase/Snowy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值