如何在本地使用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
相关产品推荐
相关产品推荐

