Railsのセッション管理には何が最適か

# 前提
webアプリを複数立ち上げ、それらのユーザーログインのセッション管理をどう実装するかで議論になった。
Railsで使用できるセッション管理方法にはたくさんの方法があり、それぞれのメリット・デメリットをまとめることで最適解を導く。

今回比較対象にするセッション管理方法は以下の種類。
- [CookieStore（クッキー方式）](https://railsguides.jp/security.html#セッションストレージ)
- [Redis（インメモリ方式）](https://github.com/redis-store/redis-rails)
- [ActiveRecord（DB方式）](https://github.com/rails/activerecord-session_store)


# [CookieStore（クッキー方式）](https://railsguides.jp/security.html#セッションストレージ)
## 仕組み
Session情報を全てsecret_key_baseで暗号化し、クライアントのCookieに保存する。
Cookieに保存したSession情報をリクエストの際に全て送信し、サーバではsecret_key_baseで復号し、Session情報を取得する。

## メリット
- Railsのデフォルトで用意されているセッション管理方式なので、手軽に使え、何かを意識する必要がない。
- サーバ処理の際にDBにアクセスする必要がないため、処理が高速。
- 別アプリケーションへのセッション共有の際に、同一ドメイン配下だと僅かな設定でセッションを共有できる。

## デメリット
- セッション再生攻撃をされる恐れがある。
- サーバでSession情報を保持していないので、Session情報を変更したいタイミングで変更できない。
- Cookieの容量上限である4kBがデータの保存上限値となる。

## 実践
### 準備

▼　Railsアプリケーションを構築

```bash
rails new CookieStoreTest
cd CookieStoreTest
bundle install
rails g scaffold Task title:string status:integer
```

▼　CookieからSessionの情報を取得し、復元・コンソールに表示するソースを追加

```ruby:/app/controllers/tasks_controller.rb
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 にアクセスし、コンソールにセッション情報が出力されたら成功。

▼　私の環境での出力内容

```bash
{"session_id":"8d7745352f73abf772eb2705bd78e3ef","_csrf_token":"Q2qOIpUim341/rSA7cymP8PD9N47gCSFxVF09+wYLoA=","foo":"foo"}
```

# [Redis（インメモリ方式）](https://github.com/redis-store/redis-rails)
## 仕組み
Railsはsession_idを作成し、Session情報と共にRedisサーバに送信する。
クライアントのCookieには作成したsession_idのみを保存し、リクエストの際にsession_idを送信する。
Railsはクライアントから受け取ったsession_idをRedisサーバに検索させ、ユーザの特定・Session情報の取得を行う。
Redisはインメモリで保存しているデータは定期的にスナップショットを保存し、永続化している。
設定をすることで、Redisで登録されているSessionに有効期限をつけることが出来る。

## メリット
- インメモリのため、処理が早い。
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。
- エクスパイア処理を1行の設定で出来る。

## デメリット
- メモリを使い果たすと書き込みが全てエラーになる。
- 情報を検索する必要があるため、クッキー方式に比べると処理が遅い。（メリットにある様に、基本的に早い部類ではある）
- スナップショットを保存してから、次のスナップショット保存までの期間にサーバが停止・再起動した場合は、その間のデータが失われる。
- Redisサーバを立てる必要があるため、コストが掛かる。また、可用性を上げるための施策を打たなければならない。（Redisサーバが落ちるとログイン機能などのSessionを元に構築されている機能が使えなくなってしまうため。）

## 実践
### 準備

▼　Railsアプリケーションを構築

```bash
rails new RedisTest
cd RedisTest
bundle install
rails g scaffold Task title:string status:integer
```

▼　Redisサーバ(コンテナ)構築

※私はDockerで環境構築をしているため、下記の記述を追加してRedisサーバを用意しました。各自の環境にあった方法でRedisサーバを用意して下さい。

```yml:/docker-compose.yml
・・・
  redis:
    image: redis:latest
    ports:
      - 6379:6379
    volumes:
      - ./data/redis:/data
    command: redis-server
```

▼ Gemfileにredis-railsを追記

```docker:/Gemfile
・・・
gem 'redis-rails'
```

▼　gemのインストール

```bash
bundle install
```

▼　redis-railsをsession_storeに設定

```ruby:/app/config/initializers/session_store.rb
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の中のホストとポートは自分の環境に合わせて下さい。

```ruby:/controllers/tasks_controller.rb
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 にアクセスし、コンソールにセッション情報が出力されたら成功。

▼　私の環境での出力内容

```bash
{"foo"=>"foo"}
```

▼　RedisサーバでSessionが保存されていることを確認したい場合

```bash
redis-cli
keys *
# keys * で取得したsession:~の内容をgetする
get session:~
```

Sessionが出てくれば成功です。

### 応用
▼　Sessionに有効期限を付ける

```ruby:/config/initializers/session_store.rb
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方式）](https://github.com/rails/activerecord-session_store)
## 仕組み
Railsはsession_idを作成し、Session情報と共にDBサーバに送信する。
クライアントのCookieには作成したsession_idのみを保存し、リクエストの際にsession_idを送信する。
Railsはクライアントから受け取ったsession_idをDBサーバに検索させ、ユーザの特定・Session情報の取得を行う。

## メリット
- DBに保存されているため、データが常に永続化されている。
- 基本的にsession情報に比べるとDBの容量は遥かに大きいため、容量を気にせず情報を保存できる。
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。

## デメリット
- DBに対して情報を検索する必要があるため、処理が遅い。（インメモリに比べて1000倍ほど処理時間が掛かると言われている）
- エクスパイア処理をバッチ等で作成するか、またはSession情報を溜めっぱなしにすることになる。

## 実践
### 準備

▼　Railsアプリケーションを構築

```bash
rails new ActiveRecordTest
cd ActiveRecordTest
bundle install
rails g scaffold Task title:string status:integer
```

▼　Gemfileにactiverecord-session_storeを追記

```docker:/Gemfile
・・・
gem 'activerecord-session_store'
```

▼　gemのインストール

```bash
bundle install
```

▼　activerecord-session_storeをsession_storeに設定

```ruby:/config/initializers/session_store.rb
AppName::Application.config.session_store :active_record_store, key: "_#{Rails.application.class.parent_name.downcase}_session"
```

▼　Session管理のためのテーブルを作成

```bash
rails generate active_record:session_migration
rake db:migrate
```

▼　Cookieからsession_idを取得し、それを元にSessionの情報をDBサーバから取得し、表示するソースを追加

```ruby:/controllers/tasks_controller.rb
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 にアクセスし、コンソールにセッション情報が出力されたら成功。

▼　私の環境での出力内容

```bash
{"foo"=>"foo", "_csrf_token"=>"gC++OkzlxWAXudi4vIN1vFZLXNX/G8rZxCIElnfqdJE="}
```

また、DBのsessionsテーブルにSessionが保存されていることも確認すると良いですね。


# 最適解
それぞれの方式にメリット・デメリットがあり、一長一短であった。
最適解が存在せず、プロダクトやその環境に依って採用すべき方式が異なると感じた。
その関連を以下に示す。

### CookieStore
セッション再生攻撃をされても大きなリスクを伴わない場合に採用すべき。特に別アプリケーションとの連携が容易に出来ることが大きな利点となる。

### Redis
セッション再生攻撃をされることに大きなリスクを伴う場合で、かつSession情報の変更をサーバで行いたい場合に採用すべき。コストは掛かるが、リスクを可能な限り抑え、処理速度が早いことから、ハイブリッドな管理方法と言える。

### ActiveRecord
セッション再生攻撃をされることに大きなリスクを伴う場合で、かつSession情報の変更をサーバで行いたく、コストを抑えたい場合に採用すべき。可用性は高いが、処理が遅くエクスパイア処理を作成する必要があるため、進んで採用すべきではないと感じる。