You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在本地使用Apartment Gem开发SaaS应用,无需lvh.me,采用路径式租户访问

用Apartment Gem实现路径式租户隔离(无需子域名/域名)

嘿,我之前也折腾过一模一样的需求——完全不用子域名或自定义域名,靠路径前缀(比如localhost:3000/tenant1)来区分租户,下面是亲测可行的一步步实现方案:

1. 调整Apartment初始化配置

首先修改config/initializers/apartment.rb,把默认的子域名解析逻辑换成路径导向的配置:

Apartment.configure do |config|
  # 彻底禁用子域名模式
  config.use_subdomain = false
  config.excluded_subdomains = []

  # 动态获取所有租户标识(假设你用Tenant模型管理租户,subdomain字段存路径里的租户名,比如tenant1)
  config.tenant_names = -> { Tenant.pluck(:subdomain) }
end

2. 自定义租户解析中间件

Apartment默认的中间件是处理子域名的,我们需要写一个自定义中间件,从URL路径的第一个片段提取租户名:
在app/middleware/apartment_path_middleware.rb创建中间件文件:

class ApartmentPathMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)
    # 拆分路径,取第一个片段作为租户名:/tenant1/dashboard → tenant1
    tenant_name = request.path.split('/')[1]

    # 排除公共路径(比如注册、登录、根路径),这些不需要切换租户
    unless %w[signup login admin ""].include?(tenant_name)
      # 校验租户是否存在,存在则切换租户
      if Tenant.exists?(subdomain: tenant_name)
        Apartment::Tenant.switch!(tenant_name)
      else
        # 租户不存在时返回404或跳转公共页面
        return [404, {'Content-Type' => 'text/html'}, ['租户不存在']]
      end
    end

    @app.call(env)
  ensure
    # 请求结束后切回默认租户,避免后续请求受影响
    Apartment::Tenant.reset if Apartment::Tenant.current
  end
end

然后在config/application.rb里注册这个中间件:

module YourAppName
  class Application < Rails::Application
    # ... 其他配置项
    config.middleware.use ApartmentPathMiddleware
  end
end

3. 配置路由结构

把租户专属路由全部放在/:tenant的作用域下,同时保留公共路由(注册、登录、首页等):

# config/routes.rb
Rails.application.routes.draw do
  # 公共路由:不需要租户即可访问的页面
  root 'public#home'
  resources :signups, only: [:new, :create]
  resources :sessions, only: [:new, :create, :destroy]

  # 租户专属路由,带路径前缀
  scope '/:tenant' do
    get 'dashboard', to: 'dashboard#index'
    resources :projects
    resources :tasks
    # ... 其他租户相关路由
  end
end

4. 注册流程中创建租户

在租户注册控制器里,确保租户标识符合路径规则(只能是字母、数字、下划线,避免特殊字符导致路径错误):

# app/controllers/signups_controller.rb
class SignupsController < ApplicationController
  def create
    @tenant = Tenant.new(tenant_params)
    if @tenant.valid?
      # 创建数据库租户
      Apartment::Tenant.create(@tenant.subdomain)
      # 切换到新租户,初始化基础数据(比如创建租户管理员)
      Apartment::Tenant.switch!(@tenant.subdomain)
      User.create!(email: params[:email], password: params[:password], role: :admin)
      # 切回默认租户
      Apartment::Tenant.reset
      # 跳转到新租户的仪表盘
      redirect_to "/#{@tenant.subdomain}/dashboard"
    else
      render :new
    end
  end

  private
  def tenant_params
    params.require(:tenant).permit(:subdomain, :name).tap do |p|
      # 清洗租户名,确保符合路径要求
      p[:subdomain] = p[:subdomain].downcase.gsub(/[^a-z0-9_]/, '')
    end
  end
end

5. 生成带租户路径的链接

在视图或控制器里生成URL时,要带上租户参数,避免跳错路径:

<!-- 视图链接示例 -->
<%= link_to '我的仪表盘', dashboard_path(tenant: current_tenant.subdomain) %>

<!-- 或者用url_for生成 -->
<%= url_for(controller: 'dashboard', action: 'index', tenant: current_tenant.subdomain) %>

嫌每次传参数麻烦的话,可以自定义路由助手:

# app/helpers/application_helper.rb
module ApplicationHelper
  def tenant_link(path, tenant = current_tenant.subdomain)
    "/#{tenant}#{path}"
  end
end

之后在视图里就能简化成:

<%= link_to '项目列表', tenant_link(projects_path) %>

关键注意事项

  • 默认租户隔离:公共路由访问时,确保Apartment处于默认租户(比如public或main),避免误操作其他租户数据。
  • 迁移执行:运行数据库迁移时,要用rails apartment:migrate命令,确保所有租户都执行迁移,而不是普通的rails migrate。
  • 缓存隔离:如果用了缓存,一定要在缓存键里加入租户标识(比如Rails.cache.fetch("#{current_tenant.id}_project_list")),防止跨租户缓存污染。
  • 路径冲突校验:注册租户时要检查租户名是否和公共路由重复(比如不能叫signup),避免路由冲突。

内容的提问来源于stack exchange,提问作者Rajnish

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:42:10