单公司应用改造为多公司架构:实现创建公司生成独立库及动态连库
单公司应用改造多公司架构(独立数据库方案)
核心需求
- 创建公司时,自动为该公司生成独立数据库及专属数据库用户
- 用户完成认证并携带
company_id时,系统自动切换至对应公司的数据库
现有代码的问题与优化方向
你当前的代码仅实现了数据库创建,但存在几个关键问题:
- SQL语句直接拼接,存在SQL注入风险
- 未创建专属数据库用户,所有公司共用超级权限账号,不符合安全规范
- 调用
ActiveRecord::Base.establish_connection会全局修改数据库连接,影响其他请求 - 新创建的数据库没有初始化表结构(未执行迁移)
完整实现方案
1. 安全创建数据库与专属用户
修改create方法,使用参数化查询避免注入,同时创建专属数据库用户并赋予权限:
def create @datos_empresa = DatosEmpresa.new(datos_empresa_params) respond_to do |format| if @datos_empresa.save # 用超级账号连接数据库服务器 admin_client = Mysql2::Client.new( host: 'localhost', username: ENV['DB_ADMIN_USER'], password: ENV['DB_ADMIN_PASS'] ) # 生成规范的数据库名和用户名(避免特殊字符) db_name = "company_#{@datos_empresa.id}_#{@datos_empresa.nombre.parameterize}" db_user = "user_#{@datos_empresa.id}" db_pass = SecureRandom.hex(16) # 生成随机密码 begin # 创建数据库 admin_client.query("CREATE DATABASE `#{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") # 创建专属用户并赋予仅当前数据库的权限 admin_client.query("CREATE USER '#{db_user}'@'localhost' IDENTIFIED BY '#{db_pass}'") admin_client.query("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost'") admin_client.query("FLUSH PRIVILEGES") # 保存数据库连接信息到公司记录 @datos_empresa.update!( db_name: db_name, db_username: db_user, db_password: db_pass ) # 初始化新数据库的表结构(执行迁移) ActiveRecord::Base.establish_connection( adapter: 'mysql2', host: 'localhost', database: db_name, username: db_user, password: db_pass ) # 推荐调用Rails迁移任务初始化表结构 system("RAILS_ENV=#{Rails.env} DATABASE_URL=mysql2://#{db_user}:#{db_pass}@localhost/#{db_name} rails db:migrate") format.html { redirect_to @datos_empresa, notice: '公司创建成功,专属数据库已初始化。' } format.json { render :show, status: :created, location: @datos_empresa } rescue Mysql2::Error => e # 回滚操作:删除已创建的数据库和用户 admin_client.query("DROP DATABASE IF EXISTS `#{db_name}`") admin_client.query("DROP USER IF EXISTS '#{db_user}'@'localhost'") @datos_empresa.destroy format.html { render :new, alert: "创建失败:#{e.message}" } format.json { render json: { error: e.message }, status: :unprocessable_entity } ensure admin_client.close end else format.html { render :new } format.json { render json: @datos_empresa.errors, status: :unprocessable_entity } end end end
2. 认证后动态切换数据库
通过控制器前置钩子,在用户认证后根据company_id切换数据库,避免全局连接污染:
# 在ApplicationController中添加 before_action :switch_company_database, if: :current_user after_action :restore_default_database, if: :current_user private def switch_company_database company = DatosEmpresa.find(current_user.company_id) # 切换当前请求的数据库连接 ActiveRecord::Base.establish_connection( adapter: 'mysql2', host: 'localhost', database: company.db_name, username: company.db_username, password: company.db_password ) end def restore_default_database # 请求结束后恢复默认数据库连接 ActiveRecord::Base.establish_connection(Rails.env.to_sym) end
关键注意事项
- SQL注入防护:始终使用反引号包裹数据库名/用户名,避免直接拼接用户输入
- 权限最小化:为每个公司创建仅能访问自身数据库的用户,禁止使用超级账号
- 连接池管理:多公司场景下需调整Rails连接池大小,避免连接耗尽
- 数据隔离:缓存、会话等资源需按公司隔离,防止跨公司数据泄露
- 备份策略:为每个公司数据库单独配置备份,避免单库备份影响所有公司
- 迁移同步:后续新增表结构时,需批量同步到所有公司的数据库
内容的提问来源于stack exchange,提问作者FelipeBP
相关产品推荐
相关产品推荐

