若依多租户集成浅析(基于数据源隔离)

news2024/12/23 15:32:02

背景

这边有个做 saas 化应用的需求,要求做到数据源级别隔离,选了 RuoyiCRM: 基于若依Vue平台搭建的多租户独立数据库CRM系统, 项目不断迭代中。欢迎提BUG交流~ (gitee.com) 这个项目做分析

up-fc18a6102b182f0eb981e1114be155dbca4

先放一下码云上作者画的图,后面我把整个多租户实现的逻辑也梳理一遍

数据库结构分析

作者给的案例需要初始化三个数据库

image-20221206162000858 image-20221206161840004

master 多一张表 master_tenant,存放租户库的数据库连接信息

image-20221206162123774

简单给大家看下初始化完毕以后,这张表的信息

image-20221206162203549

初始化的过程后面会讲到

debug

注册

我们先注册账号

image-20221206170120601
@PostMapping("/register")
    public AjaxResult registerTenant(@RequestBody TenantRegisterBody tenantRegisterBody){
        loginService.validateCaptcha(tenantRegisterBody.getTenantName(), tenantRegisterBody.getCode(), tenantRegisterBody.getUuid());

        if (TenantConstants.NOT_UNIQUE.equals(masterTenantService.checkTenantNameUnique(tenantRegisterBody.tenantName)))
        {
            return AjaxResult.error("注册'" + tenantRegisterBody.getTenantName() + "'失败,账号已存在");
        }
        TenantDatabaseDTO tenantDatabase=null;
        try {
            tenantDatabase=tenantRegisterService.initDatabase(tenantRegisterBody);
        } catch (SQLException ex) {
            ex.printStackTrace();
            return AjaxResult.error("注册'" + tenantRegisterBody.getTenantName() + "'失败,创建租户时发生错误");
        }catch (Exception ex){
            ex.printStackTrace();
            return AjaxResult.error("注册'" + tenantRegisterBody.getTenantName() + "'失败,请与我们联系");
        }
        int i = masterTenantService.insertMasterTenant(tenantDatabase);
        return toAjax(i);
    }

看下初始化数据库这个函数

public TenantDatabaseDTO initDatabase(TenantRegisterBody form) throws Exception {
        Connection conn = getConnection();
        Statement stmt = null;
        TenantDatabaseDTO tenantDatabaseDTO = null;
        //创建数据库ID
        String tenantDatabaseID = ShortUUID.nextID();
        //组合数据库名
        String tenantDatabase = prefix + tenantDatabaseID;
        try {
            conn.setAutoCommit(false);
            stmt = conn.createStatement();

            // 创建库
            String createDatabaseSQL = "CREATE DATABASE IF NOT EXISTS `" + tenantDatabase + "` DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;";
            stmt.execute(createDatabaseSQL);

            //创建数据库用户名及密码
            String tenantDatabaseUsername = tenantDatabase;
            String tenantDatabasePassword = ShortUUID.nextID();

            //创建用户
            String createUser = "create user '"+tenantDatabaseUsername+"'@'localhost' identified by '"+tenantDatabasePassword+"';";
            stmt.execute(createUser);

            //用户授权
            String grantSQL = "GRANT select, insert, update, delete ON "+tenantDatabase+".* TO '" + tenantDatabaseUsername + "'@'localhost';";
            stmt.execute(grantSQL);

            // 切换到
            conn.setCatalog(tenantDatabase);

            // 获取当前数据库名称
            log.info("当前数据库:{}", conn.getCatalog()); // 若未选择数据库,则 getCatalog 返回空
            conn.getCatalog();

            //创建返回对象
            tenantDatabaseDTO = new TenantDatabaseDTO();
            tenantDatabaseDTO.setTenantDatabase(tenantDatabase);
            tenantDatabaseDTO.setTenantName(form.tenantName);
            tenantDatabaseDTO.setDbUser(tenantDatabaseUsername);
            tenantDatabaseDTO.setDbPass(tenantDatabasePassword);
            tenantDatabaseDTO.setAdminName(form.adminName);
            tenantDatabaseDTO.setAdminPass(form.adminPass);
            String tenantUrl = getUrl() + tenantDatabaseDTO.tenantDatabase + "?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true";
            tenantDatabaseDTO.setUrl(tenantUrl);

            //执行初始化脚本
            executeInitScript(conn, tenantDatabaseDTO);

        } catch (Exception ex){
            ex.printStackTrace();


            //删除数据库
            stmt.execute("DROP DATABASE IF EXISTS "+ tenantDatabase);
            log.error("删除数据库:{}",tenantDatabase );

            throw new ServiceException("执行数据库操作时发生错误");
        }finally{
            if (stmt != null) {
                stmt.close();
            }
            conn.close();
        }
        return tenantDatabaseDTO;
    }

我把这块拼出来的几条 sql 粘贴出来

CREATE DATABASE IF NOT EXISTS `ryt_lljZ8o8T` DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;
create user 'ryt_lljZ8o8T'@'localhost' identified by 'mNz9Xu4u';
GRANT select, insert, update, delete ON ryt_lljZ8o8T.* TO 'ryt_lljZ8o8T'@'localhost';

再看下这块拼接出来的 对象

image-20221206164641334

看下面一个函数

private void executeInitScript(Connection conn, TenantDatabaseDTO tenantDatabaseDTO) throws SQLException {
        try {
            ScriptRunner runner = new ScriptRunner(conn);
            runner.setErrorLogWriter(null);
            runner.setLogWriter(null);

            Resources.setCharset(StandardCharsets.UTF_8);//设置字符集,不然中文乱码插入错误
            Reader reader = Resources.getResourceAsReader("init-sql-script/rycrm-tenant-sample.sql");
            runner.runScript(reader);

            SqlRunner sqlRunner = new SqlRunner(conn);
            String insertSql;

            //插入部门
            insertSql = "INSERT INTO sys_dept VALUES (100, 0, '0', '" + tenantDatabaseDTO.tenantName + "', 0, '" + tenantDatabaseDTO.tenantName + "', '00000000000', 'admin@admin.com', '0', '0', 'admin', '" + DateUtils.getTime() + "', 'admin', '" + DateUtils.getTime() + "');";
            sqlRunner.run(insertSql);

            //生成密码
            String encryptPassword = SecurityUtils.encryptPassword(tenantDatabaseDTO.adminPass);
            //插入系统超级管理员
            insertSql = "INSERT INTO sys_user (`user_id`, `dept_id`, `user_name`, `nick_name`, `user_type`, `email`, `phonenumber`, `sex`, `avatar`, `password`, `status`, `del_flag`, `login_ip`, `login_date`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) " +
                    "VALUES (1, 100, '" + tenantDatabaseDTO.adminName + "', '管理员', '00', 'admin@admin.com', '00000000000', '1', '', '" + encryptPassword + "', '0', '0', '127.0.0.1', '" + DateUtils.getTime() + "', '" + tenantDatabaseDTO.adminName + "', '" + DateUtils.getTime() + "', '', '" + DateUtils.getTime() + "', '管理员');";
            sqlRunner.run(insertSql);

            conn.commit();

        } catch (SQLException e) {
            e.printStackTrace();
            conn.rollback();
            throw new ServiceException("初始化用户数据脚本时出错");
        }catch (Exception ex){
            ex.printStackTrace();
            throw new ServiceException("执行初始用户数据时出错");
        }

    }

看下sql

INSERT INTO sys_dept VALUES (100, 0, '0', 'tenant1', 0, 'tenant1', '00000000000', 'admin@admin.com', '0', '0', 'admin', '2022-12-06 16:50:23', 'admin', '2022-12-06 16:50:23');
INSERT INTO sys_user (`user_id`, `dept_id`, `user_name`, `nick_name`, `user_type`, `email`, `phonenumber`, `sex`, `avatar`, `password`, `status`, `del_flag`, `login_ip`, `login_date`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1, 100, 'zds', '管理员', '00', 'admin@admin.com', '00000000000', '1', '', '$2a$10$TYPn2q6Ccp8uf5qiKg33Q.Un5GCGkIcrVPSYh3SgBtItbG5NFW9je', '0', '0', '127.0.0.1', '2022-12-06 16:50:55', 'zds', '2022-12-06 16:50:55', '', '2022-12-06 16:50:55', '管理员');

最后把初始化出来的租户schema信息写入 master_tenant

登录

填写 登录信息

image-20221206170249377
/**
     * 登录方法,前端通过Header方式传递tenant信息
     * 
     * @param loginBody 登录信息
     * @return 结果
     */
    @PostMapping("/login")
    public AjaxResult login(HttpServletRequest request, @RequestBody LoginBody loginBody)
    {
        String tenant= request.getHeader("tenant");

        if(StringUtils.isEmpty(tenant)){
            return AjaxResult.error("租户ID不能为空");
        }

        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(tenant, loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

这块会动态切换数据源,去这个租户对应的数据源连接里面来登录

这块的实现在 TenantInterceptor

@Component
@Slf4j
public class TenantInterceptor implements HandlerInterceptor {

    @Autowired
    private IMasterTenantService masterTenantService;

    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;

    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String url = request.getServletPath();
        String tenant= request.getHeader("tenant");
        log.info("&&&&&&&&&&&&&&&& 租户拦截 &&&&&&&&&&&&&&&&");
        if (StringUtils.isNotBlank(tenant)) {
            if (!dynamicRoutingDataSource.existDataSource(tenant)) {
                //搜索默认数据库,去注册租户的数据源,下次进来直接session匹配数据源
                MasterTenant masterTenant = masterTenantService.selectMasterTenant(tenant);
                if (masterTenant == null) {
                    throw new RuntimeException("无此租户:"+tenant );
                }else if(TenantStatus.DISABLE.getCode().equals(masterTenant.getStatus())){
                    throw new RuntimeException("租户["+tenant+"]已停用" );
                }else if(masterTenant.getExpirationDate()!=null){
                    if(masterTenant.getExpirationDate().before(DateUtils.getNowDate())){
                        throw new RuntimeException("租户["+tenant+"]已过期");
                    }
                }
                Map<String, Object> map = new HashMap<>();
                map.put("driverClassName", driverClassName);
                map.put("url", masterTenant.getUrl());
                map.put("username", masterTenant.getUsername());
                map.put("password", masterTenant.getPassword());
                dynamicRoutingDataSource.addDataSource(tenant, map);

                log.info("&&&&&&&&&&& 已设置租户:{} 连接信息: {}", tenant, masterTenant);
            }else{
                log.info("&&&&&&&&&&& 当前租户:{}", tenant);
            }
        }else{
            throw new RuntimeException("缺少租户信息");
        }
        // 为了单次请求,多次连接数据库的情况,这里设置localThread,AbstractRoutingDataSource的方法去获取设置数据源
        DynamicDataSourceContextHolder.setDataSourceKey(tenant);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 请求结束删除localThread
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }
}

会去取出我们刚才注册的时候写到 master 数据源当中的master_tenant表里面的信息

小结

image-20221206171235400

这块其实离商用还有不少距离,比如没有一个超级管理员来进到一个主界面管理所有租户数据,但好像如果采用这种数据源粒度的隔离都会有这个问题,后面再分析下只根据表隔离的实现思路

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/66290.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

股票量化怎样分析股票数据精准选股?

在日常的股票量化交易过程中&#xff0c;通常有不少的交易者会借助股票数据接口来分析股票数据&#xff0c;并且经过一番股票量化分析之后&#xff0c;做到精准选股也是很有可能的事情。那么&#xff0c;普通投资者进行股票量化怎样分析股票数据选好股呢&#xff1f; 首先来了…

springboot:集成Kaptcha实现图片验证码

文章目录springboot&#xff1a;集成Kaptcha实现图片验证码一、导入依赖系统配置文件二、生成验证码1、Kaptcha的配置2、自定义验证码文本生成器3、具体实现三、校验验证码1、controller接口2、自定义前端过滤器3、自定义验证码处理过滤器4、自定义BodyReaderFilter解决读取bod…

Redis——Jedis的使用

前言 接上文&#xff0c;上一篇文章分享了在Linux下安装redis&#xff0c;以及redis的一些命令的使用。本文要分享的内容是java使用代码连接操作redis。 一、连接redis 这里我们要用到Jedis&#xff0c;那么什么是Jedis 简单来说&#xff0c;Jedis就是Redis官方推荐的Java连接…

【元胞自动机】模拟电波在整个心脏中的传导和传播的时空动力学研究(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

(八)SpringCloud+Security+Oauth2--token增强个性化和格式化输出

一 token的个性化输出 我们知道token默认的输出格式是: {"access_token": "21bd6b0b-0c24-40d1-8928-93274aa1180f","token_type": "bearer","refresh_token": "2c38965b-d4ce-4151-b88d-e39f278ce1bb","e…

[思考进阶]02 如何进行认知升级?

除了要提升自己的技术能力&#xff0c;思维的学习和成长也非常非常重要&#xff0c;特推出此[思考进阶]系列&#xff0c;进行刻意练习&#xff0c;从而提升自己的认知。 最近在看东野的《无名之町》&#xff0c;这本书写于2021年&#xff0c;日本正值疫情&#xff0c;书中也有大…

这个项目获2022世界物联网博览会三新成果奖!

近日&#xff0c;2022世界物联网无锡峰会在无锡太湖国际博览中心召开。天翼物联科技有限公司副总经理赵建军代表中国电信出席会议。 大会颁发了“物联网新技术新产品新应用金奖成果奖”&#xff08;简称“三新成果奖”&#xff09;&#xff0c;中国电信天翼物联“基于5G物联孪…

gRPC:以 C++为例

文章目录1、gRPC 环境搭建1.1、安装 cmake1.2、安装 gcc/gdb1.3、安装 gRPC1.4、protobuf 安装1.5、测试环境2.1、grpc 同步2.1、定义服务2.2、gRPC 服务端2.3、gRPC 客户端2.4、消息流3、gRPC stream3.1、服务端&#xff1a;RPC 实现3.2、客户端&#xff1a;RPC 调用3.3、流的…

刷爆力扣之子数组最大平均数 I

刷爆力扣之子数组最大平均数 I HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&…

Centos 8.2 本地部署 Jenkins

文章目录1. 简介2. 准备条件3. 安装依赖工具4. 配置 jenkins 源5. 安装 java 176. 安装 Jenkins7. 登陆8. 安装插件8.1 kubernets 插件8.2 git 插件8.3 docker 插件9. 创建 pipeline job9.1 加载本地 Jenkinsfile 构建9.2 git 构建10. 问题1. 简介 Jenkins 是一个 CI/CD 工具。…

Transformer是如何进军点云学习领域的?

点击进入—>3D视觉工坊学习交流群0.笔者个人体会&#xff1a;这个工作来自于牛津大学、香港大学、香港中文大学和Intel Labs&#xff0c;发表于ICCV2021。我们知道&#xff0c;Transformer在近两年来于各个领域内大放异彩。其最开始是自然语言处理领域的一个强有力的工具。后…

Unity 动画系统(Animation,Animator,Timeline)

文章目录1. Animation1.1 创建Animation1.2 Animation 属性2. Animator2.1 Animator 组件2.2 Animation 状态2.3 状态控制参数2.4 代码中控制状态3. 代码控制动画的播放/暂停/继续播放1. Animation 1.1 创建Animation 选中需要添加动画的物体&#xff0c;打开Animation面板 …

乡村科技杂志乡村科技杂志社乡村科技编辑部2022年第20期目录

三农资讯 科技特派员助力柘城县大豆玉米带状复合种植见成效 宋先锋;贾志远; 1《乡村科技》投稿&#xff1a;cnqikantg126.com 河南省科技特派员赴遂平县指导多花黑麦草防治 蒋洪杰;欧阳曦; 2 河南省肉牛产业科技特派员服务团到光山县开展技术培训服务 翟媛媛;朱燚波…

la3_系统调用(上)

1. 实验内容 理解操作系统接口&#xff1b;系统调用的实现&#xff1a; 应用程序 调用库函数 &#xff08;API&#xff09;API 将 系统调用号 放入 EAX 中&#xff0c; 然后通过中断调用 使系统进入内核态&#xff1b;内核中的中断处理函数 根据系统调用号&#xff0c; 调用对…

通过postgres_fdw实现跨库访问

瀚高数据库 目录 文档用途 详细信息 介绍Postgresql跨库访问中postgres_fdw的使用方法 详细信息 PostgreSQL 外部数据包装器&#xff0c;即 PostgreSQL Foreign Data Wrappers&#xff0c;是现实数据库使用场景中一个非常实用的功能&#xff0c;PostgreSQL 的 FDW 类似于 Ora…

2022年12月编程语言排行榜,数据来了!

2022年迎来了最后一个月&#xff0c;我们可以看到&#xff0c;在这一年中编程语言起起伏伏&#xff0c;有的语言始终炙手可热&#xff0c;而有的语言却逐渐“没落”… 日前&#xff0c;全球知名TIOBE编程语言社区发布了12月编程语言排行榜&#xff0c;有哪些新变化&#xff1f…

木聚糖-聚乙二醇-透明质酸,Hyaluronicacid-PEG-Xylan,透明质酸-PEG-木聚糖

木聚糖-聚乙二醇-透明质酸,Hyaluronicacid-PEG-Xylan,透明质酸-PEG-木聚糖 中文名称&#xff1a;木聚糖-透明质酸 英文名称&#xff1a;Xylan-Hyaluronicacid 别称&#xff1a;透明质酸修饰木聚糖&#xff0c;HA-木聚糖 存储条件&#xff1a;-20C&#xff0c;避光&#xff…

农产品商城毕业设计,农产品销售系统毕业设计,农产品电商毕业设计论文方案需求分析作品参考

项目背景和意义 目的&#xff1a;本课题主要目标是设计并能够实现一个基于web网页的多用户商城系统&#xff0c;整个网站项目使用了B/S架构&#xff0c;基于python的Django框架下开发&#xff1b;用户通过登录网站&#xff0c;查询商品&#xff0c;购买商品&#xff0c;下单&am…

奋勇拼搏绿茵场,永不言败足球魂——2022卡塔尔世界杯纪念

“我从来都不惧怕压力,老实说,我享受这种压力。”——C罗 第一部分&#xff1a;&#x1f1f6;&#x1f1e6;卡塔尔世界杯 2022年卡塔尔世界杯&#xff08;英语&#xff1a;FIFA World Cup Qatar 2022&#xff09;是第二十二届世界杯足球赛&#xff0c;是历史上首次在卡塔尔和中…

Apple官方优化Stable Diffusion绘画教程

Apple官方优化Stable Diffusion绘画教程 苹果为M1芯片优化Stable Diffusion模型&#xff0c;其中Mac Studio (M1 Ultra, 64-core GPU)生成512*512的图像时间为9秒。想要1秒出图&#xff0c;可以在线体验3090显卡AI绘画。 AI绘图在线体验 二次元绘图 在线体验地址:Stable Di…