将Rails大项目拆分成多个独立Application

1. Background

随着业务发展,功能越来越多,开发和测试越来越慢。需要进行功能切分和解耦。

尝试将部分功能转移至Mountable Engine,但感觉不理想。

  1. migration麻烦。功能迭代很快,每次都要执行 rake your_engine:install:migrations; rake db:migraterails plugin new my_engine --mountable 生成的目录不包含app/models,估计也是不提倡在Mountable Engine摆models。

  2. 功能依然耦合。各Engine和Main APP实际还是可以互相调用,易写出耦合的东西。

  3. 任何部署仍要deploy整个Main APP

开始尝试单点登录(SSO),谷歌了一下,主要有共享cookie, CAS, OAuth这三种方式,目前先用最简单的共享cookie方法。

2. Create authority server

2.1 设置一致的 sessionstore 和 secrettoken。

1
2
config/initializers/session_store.rb
config/initializers/secret_token.rb

2.2 提供Restful API.

config/routes.rb

1
2
3
4
5
6
7
8
namespace :api do
  resources :users, :only=>[:index, :show] do
    resources :roles, :only=>[:index], :controller=>'user_roles'
  end
  resources :roles, :only=>[:index, :show] do
    resources :users, :only=>[:index], :controller=>'role_users'
  end
end

2.3 登陆后设置session[:currentuserid],方便子应用调用。

app/controllers/application_controller.rb

1
2
3
4
5
6
7
8
9
10
11
def after_sign_in_path_for(resource)
  session[:current_user_id] = current_user.id
  session[:current_user_name] = current_user.name
  super
end

def after_sign_out_path_for(resource_or_scope)
  session[:current_user_id] = nil
  session[:current_user_name] = nil
  super
end

3. Sub APP

3.1 Gemfile

1
gem 'eagle_base', :git => 'https://github.com/richfisher/eagle_base.git', :branch=>'master'

3.2 config/initializers/eagle_base.rb

1
2
AUTHORITY_SERVER_URL = 'http://host/sub_uri'
AUTHORITY_PATH = '/sub_uri'

3.3 设置一致的 sessionstore 和 secrettoken。

1
2
config/initializers/session_store.rb
config/initializers/secret_token.rb

设置完成后,即可实现:

  1. 单点登陆和登出。
  2. 在controllers和elpers均可调用current_user。

4. Base Engine

https://github.com/richfisher/eagle_base

进行登陆验证并提供UserResource和RoleResource

4.1 Models

调用Authority Server的API,并对结果进行封装。

UserResource
1
2
3
4
5
6
UserResource.all(condition)
UserResource.find(condition)

UserResource#roles
UserResource#id
UserResource#name
RoleResource
1
2
3
4
5
6
7
RoleResource.all(condition)
RoleResource.find(condition)

RoleResource#users
RoleResource#id
RoleResource#name
RoleResource#permissions

4.2 Controllers

current_user

config/initializers/current_user.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ActionController::Base.class_eval do
  before_filter :authenticate_user!
  helper_method :current_user, :current_user_id, :current_user_name

  def authenticate_user!
    redirect_to login_path and return if session[:current_user_id].blank?
  end

  def current_user
    UserResource.find(current_user_id)
  end

  def current_user_id
    session[:current_user_id]
  end

  def current_user_name
    session[:current_user_name]
  end

  def login_path
    "#{AUTHORITY_PATH}/users/sign_in"
  end
end
处理 CanCan::AccessDenied

config/initializers/cancanaccessdenied.rb

1
2
3
4
5
6
7
8
9
ActionController::Base.class_eval do
  rescue_from CanCan::AccessDenied do |exception|
    if current_user.blank?
      redirect_to login_path , :alert=>t('please_login_first') and return
    else
      redirect_to '/', :alert => exception.message
    end
  end
end