前提
webアプリを複数立ち上げ、それらのユーザーログインのセッション管理をどう実装するかで議論になった。
Railsで使用できるセッション管理方法にはたくさんの方法があり、それぞれのメリット・デメリットをまとめることで最適解を導く。
今回比較対象にするセッション管理方法は以下の種類。
CookieStore(クッキー方式)
仕組み
Session情報を全てsecret_key_baseで暗号化し、クライアントのCookieに保存する。
Cookieに保存したSession情報をリクエストの際に全て送信し、サーバではsecret_key_baseで復号し、Session情報を取得する。
メリット
- Railsのデフォルトで用意されているセッション管理方式なので、手軽に使え、何かを意識する必要がない。
- サーバ処理の際にDBにアクセスする必要がないため、処理が高速。
- 別アプリケーションへのセッション共有の際に、同一ドメイン配下だと僅かな設定でセッションを共有できる。
デメリット
- セッション再生攻撃をされる恐れがある。
- サーバでSession情報を保持していないので、Session情報を変更したいタイミングで変更できない。
- Cookieの容量上限である4kBがデータの保存上限値となる。
実践
準備
▼ Railsアプリケーションを構築
rails new CookieStoreTest
cd CookieStoreTest
bundle install
rails g scaffold Task title:string status:integer
▼ CookieからSessionの情報を取得し、復元・コンソールに表示するソースを追加
require 'cgi'
require 'active_support'
require 'yaml'
class TasksController < ApplicationController
before_action :set_task, only: [:show, :edit, :update, :destroy]
# GET /tasks
# GET /tasks.json
def index
cookie = cookies["_app_name_session"]
key = YAML.load_file("config/secrets.yml")["development"]["secret_key_base"]
cookie = CGI::unescape(cookie)
# Default values for Rails 4 apps
key_iter_num = 1000
salt = "encrypted cookie"
signed_salt = "signed encrypted cookie"
key_generator = ActiveSupport::KeyGenerator.new(key, iterations: key_iter_num)
secret = key_generator.generate_key(salt)[0, 32]
sign_secret = key_generator.generate_key(signed_salt)
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
puts encryptor.decrypt_and_verify(cookie)
session['foo'] = 'foo'
@tasks = Task.all
end
・・・
確認
http://localhost:3000/tasks にアクセスし、コンソールにセッション情報が出力されたら成功。
▼ 私の環境での出力内容
{"session_id":"8d7745352f73abf772eb2705bd78e3ef","_csrf_token":"Q2qOIpUim341/rSA7cymP8PD9N47gCSFxVF09+wYLoA=","foo":"foo"}
Redis(インメモリ方式)
仕組み
Railsはsession_idを作成し、Session情報と共にRedisサーバに送信する。
クライアントのCookieには作成したsession_idのみを保存し、リクエストの際にsession_idを送信する。
Railsはクライアントから受け取ったsession_idをRedisサーバに検索させ、ユーザの特定・Session情報の取得を行う。
Redisはインメモリで保存しているデータは定期的にスナップショットを保存し、永続化している。
設定をすることで、Redisで登録されているSessionに有効期限をつけることが出来る。
メリット
- インメモリのため、処理が早い。
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。
- エクスパイア処理を1行の設定で出来る。
デメリット
- メモリを使い果たすと書き込みが全てエラーになる。
- 情報を検索する必要があるため、クッキー方式に比べると処理が遅い。(メリットにある様に、基本的に早い部類ではある)
- スナップショットを保存してから、次のスナップショット保存までの期間にサーバが停止・再起動した場合は、その間のデータが失われる。
- Redisサーバを立てる必要があるため、コストが掛かる。また、可用性を上げるための施策を打たなければならない。(Redisサーバが落ちるとログイン機能などのSessionを元に構築されている機能が使えなくなってしまうため。)
実践
準備
▼ Railsアプリケーションを構築
rails new RedisTest
cd RedisTest
bundle install
rails g scaffold Task title:string status:integer
▼ Redisサーバ(コンテナ)構築
※私はDockerで環境構築をしているため、下記の記述を追加してRedisサーバを用意しました。各自の環境にあった方法でRedisサーバを用意して下さい。
・・・
redis:
image: redis:latest
ports:
- 6379:6379
volumes:
- ./data/redis:/data
command: redis-server
▼ Gemfileにredis-railsを追記
・・・
gem 'redis-rails'
▼ gemのインストール
bundle install
▼ redis-railsをsession_storeに設定
AppName::Application.config.session_store :redis_store, {
servers: [
{
host: "redis",
port: 6379,
db: 0,
namespace: "session"
},
],
key: "_#{Rails.application.class.parent_name.downcase}_session"
}
▼ Cookieからsession_idを取得し、それを元にSessionの情報をRedisサーバから取得し、表示するソースを追加
※Redis.newの中のホストとポートは自分の環境に合わせて下さい。
class TasksController < ApplicationController
before_action :set_task, only: [:show, :edit, :update, :destroy]
def index
redis_namespace = "session"
cookie_key = cookies[Rails.application.config.session_options[:key]]
session_key = "#{redis_namespace}:#{cookie_key}"
redis = Redis.new(:host => "redis", :port => 6379)
puts Marshal.load(redis.get(session_key))
session['foo'] = 'foo'
@tasks = Task.all
end
・・・
確認
http://localhost:3000/tasks にアクセスし、コンソールにセッション情報が出力されたら成功。
▼ 私の環境での出力内容
{"foo"=>"foo"}
▼ RedisサーバでSessionが保存されていることを確認したい場合
redis-cli
keys *
# keys * で取得したsession:~の内容をgetする
get session:~
Sessionが出てくれば成功です。
応用
▼ Sessionに有効期限を付ける
AppName::Application.config.session_store :redis_store, {
servers: [
{
host: "redis",
port: 6379,
db: 0,
namespace: "session"
},
],
#この1行を追加↓
expire_after: 10.seconds,
key: "_#{Rails.application.class.parent_name.downcase}_session"
}
再度サーバを立ち上げ直し、http://localhost:3000/tasks へアクセス。
RedisサーバでSessionを確認し、10秒後に消えていたら成功。
ActiveRecord(DB方式)
仕組み
Railsはsession_idを作成し、Session情報と共にDBサーバに送信する。
クライアントのCookieには作成したsession_idのみを保存し、リクエストの際にsession_idを送信する。
Railsはクライアントから受け取ったsession_idをDBサーバに検索させ、ユーザの特定・Session情報の取得を行う。
メリット
- DBに保存されているため、データが常に永続化されている。
- 基本的にsession情報に比べるとDBの容量は遥かに大きいため、容量を気にせず情報を保存できる。
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。
デメリット
- DBに対して情報を検索する必要があるため、処理が遅い。(インメモリに比べて1000倍ほど処理時間が掛かると言われている)
- エクスパイア処理をバッチ等で作成するか、またはSession情報を溜めっぱなしにすることになる。
実践
準備
▼ Railsアプリケーションを構築
rails new ActiveRecordTest
cd ActiveRecordTest
bundle install
rails g scaffold Task title:string status:integer
▼ Gemfileにactiverecord-session_storeを追記
・・・
gem 'activerecord-session_store'
▼ gemのインストール
bundle install
▼ activerecord-session_storeをsession_storeに設定
AppName::Application.config.session_store :active_record_store, key: "_#{Rails.application.class.parent_name.downcase}_session"
▼ Session管理のためのテーブルを作成
rails generate active_record:session_migration
rake db:migrate
▼ Cookieからsession_idを取得し、それを元にSessionの情報をDBサーバから取得し、表示するソースを追加
class TasksController < ApplicationController
before_action :set_task, only: [:show, :edit, :update, :destroy]
def index
session_data = Session.find_by(session_id: cookies[Rails.application.config.session_options[:key]])[:data]
data_dec64 = Base64.decode64(session_data)
puts Marshal.load(data_dec64)
session["foo"] = "foo"
@tasks = Task.all
end
・・・
確認
http://localhost:3000/tasks にアクセスし、コンソールにセッション情報が出力されたら成功。
▼ 私の環境での出力内容
{"foo"=>"foo", "_csrf_token"=>"gC++OkzlxWAXudi4vIN1vFZLXNX/G8rZxCIElnfqdJE="}
また、DBのsessionsテーブルにSessionが保存されていることも確認すると良いですね。
最適解
それぞれの方式にメリット・デメリットがあり、一長一短であった。
最適解が存在せず、プロダクトやその環境に依って採用すべき方式が異なると感じた。
その関連を以下に示す。
CookieStore
セッション再生攻撃をされても大きなリスクを伴わない場合に採用すべき。特に別アプリケーションとの連携が容易に出来ることが大きな利点となる。
Redis
セッション再生攻撃をされることに大きなリスクを伴う場合で、かつSession情報の変更をサーバで行いたい場合に採用すべき。コストは掛かるが、リスクを可能な限り抑え、処理速度が早いことから、ハイブリッドな管理方法と言える。
ActiveRecord
セッション再生攻撃をされることに大きなリスクを伴う場合で、かつSession情報の変更をサーバで行いたく、コストを抑えたい場合に採用すべき。可用性は高いが、処理が遅くエクスパイア処理を作成する必要があるため、進んで採用すべきではないと感じる。