railsのAPIモードでセッションしたときのアレコレ

2021-03-01

rails, session, cookies

検証用に適当なアプリ作って ブレークポイントで止めてsessionの中身をアレコレ確認していきます

Rails 6.1.3
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux-musl]

準備

rails new

1
rails new sessions -d mysql --skip-action-mailbox --skip-active-storage --skip-action-cable -S --skip-spring --skip-system-test --skip-bootsnap --skip-bundle --skip-webpack-install --api

docker-compose.yml

環境変数は.envに書きました
Dockerfileは省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
version: "3.7"
services:
  db:
    image: mysql:5.7
    container_name: "${APP_NAME}_db"
    environment:
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: "${APP_NAME}_${RAILS_ENV:-development}"

  api:
    build:
      context: .
      args:
        - "WORKDIR=${WORKDIR:-/app}"
        - "USER=${USER:-root}"
        - "USER_ID=${USER_ID:-0}"
        - "GROUP=${GROUP:-root}"
        - "GROUP_ID=${GROUP_ID:-0}"
    user: "${USER_ID:-0}:${GROUP_ID:-0}"
    container_name: "${APP_NAME}_api"
    # 1. bundle install
    # 2. mysqlの起動待ち
    # 3. db:migrate
    # 4. rails server
    command: /bin/sh -c "bundle && bundle exec rails r bin/await_mysql.rb && bundle exec rails db:migrate && rails server -b 0.0.0.0 -p 3000"
    environment:
      RAILS_ENV: "${RAILS_ENV:-development}"
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - ".:${WORKDIR:-/app}"
    ports:
      - "${API_PORT:-3030}:3000"
    depends_on:
      - db

  redis:
     image: redis
     command: redis-server

... bin/await_mysql.rb はrailsがdb接続できるようになるまで待機するスクリプトです
中身はこんな感じ

1
2
3
4
5
6
begin
  ActiveRecord::Base.connection
  puts "database connected![#{Rails.configuration.database_configuration["development"]["database"]}]"
rescue
  retry
end

docker-compose run --rm api bundle exec rails g scaffold user name:string

1
2
3
4
5
6
  def index
    binding.pry
    @users = User.all

    render json: @users
  end

docker-compose up -d

curl http://localhost:3000/users

1
2
3
4
5
6
> session
=> {}
> session.class
=> Hash
> cookies
NameError: undefined local variable or method `cookies' for #<UsersController:0x00000000003de0>

sessionはただのハッシュだしcookiesは定義すらない

Cookie使えるようにする

ApplicationController

1
2
3
class ApplicationController < ActionController::API
  include ActionController::Cookies # <- コレ追加
end

これでcookiesは呼べるようになるけど保存しても次のリクエストでは消えてる

1
2
3
4
5
6
7
8
9
> cookies
=> #<ActionDispatch::Cookies::CookieJar:0x0000557fe78d88a0
 @committed=false,
 @cookies={},
 @delete_cookies={},
 @request=#<ActionDispatch::Request GET "http://localhost:3530/users" for 192.168.96.1>,
 @set_cookies={}>
> cookies[:foo] = 'FOO'
=> "FOO"

config/application.rb

1
2
3
4
5
  class Application < Rails::Application
    config.load_defaults 6.1
    config.api_only = true
    config.middleware.use ActionDispatch::Cookies # <- コレ追加
  end

これでレスポンスにクッキーが乗るようになる
同一ドメインなら次回アクセス時にも送信されて参照できる

Session使えるようにする

develoment環境のキャッシュを有効化

1
$ touch tmp/caching-dev.txt

session_storeを有効化

config/application.rb

1
2
3
4
5
6
  class Application < Rails::Application
    config.load_defaults 6.1
    config.api_only = true
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore # <- コレ追加
  end

これでセッションがcookieに保存されるようになる

1
2
3
4
5
6
7
8
> session
=> #<ActionDispatch::Request::Session:0x3db8 not yet loaded>
> session.keys
=> []
> session[:foo] = 'FOO'
=> "FOO"
> session.keys
=> ["session_id", "foo"]

_session_idってのがセッション情報のキー
クッキーのセッション

valueがsession情報を暗号化したもの
secret_key_baseかなんかでデコードするのだろうか?

セッション情報をサーバサイドでキャッシュ

セッション情報をクッキーに全部のせちゃうと嬉しくないことが結構ある

  • ユーザーが中身見れちゃう
    なんとなくシステムの中身が想像できちゃったり
  • クッキー盗めばなんでもやり放題
    大事な情報は全部クッキーの中にあるので
  • サーバー側でセッションを制御できない
    セッション削除のためのデプロイなんてやってらんない

なのでクッキーにはセッションを特定する情報(session_id)だけを渡して
実際のセッション情報(ユーザーのIDとか)はサーバー側で持つのが良いらしい
保存する場所は「メモリ、ファイル、KVS、RDB」など。

今回はKVS(Redis)にセッション情報を保存します

config/application.rb

1
2
3
4
5
6
7
8
class Application < Rails::Application
  config.load_defaults 6.1
  config.api_only = true

  config.middleware.use ActionDispatch::Cookies
  # config.middleware.use ActionDispatch::Session::CookieStore # <- Out
  config.middleware.use ActionDispatch::Session::CacheStore    # <- In
end

config/environments/development.rb

1
2
3
4
5
6
  if Rails.root.join('tmp', 'caching-dev.txt').exist?
    # config.cache_store = :memory_store  # <- コメントアウト
    # ↓コレ追加
    config.cache_store = :redis_cache_store, { expires_in: 1.day, url: 'redis://redis:6379/0' }
    ...
  end

Gemfile

redis-actionpackを追加

1
echo "gem 'redis-actionpack'" >> Gemfile

コンテナ再起動して動作確認

1
2
3
4
5
6
7
8
9
10
> session.id
=> nil                                # 最初はセッションもってない
> session.keys
=> []                                 # 当然何も入ってない
> session[:foo] = 'ふー'
=> "ふー"                             # なんか適当に登録
> session.keys
=> ["foo"]
> session.id
=> "b65297bf44cc6acd42dd5c2d26cf543d" # セッションID付与された!

実際にブラウザでクッキーを確認すると _session_id に保存されてる
コレがsession_idだ

サーバに戻って確認すると

1
2
> cookies[:_session_id]
=> "b65297bf44cc6acd42dd5c2d26cf543d"  # 入ってますねー

cache_storeからもsession_idで取れるのかと思いきや

1
2
> Rails.cache.read session.id
=> nil                                # おや…?

redisの中身を見てみると

1
2
127.0.0.1:6379> keys *
1) "_session_id:2::4ae09071570b9b6b83af9dc5ed0e219a5eb1bc12cb216e89ac61edc8bd75fab9"

_session_id:2::4ae09071570b9b6b83af9dc5ed0e219a5eb1bc12cb216e89ac61edc8bd75fab9 ってなんだ?

_session_id:2:: は接頭詞かな?
2はテーブル番号だろ多分。

じゃぁ 4ae09071570b9b6b83af9dc5ed0e219a5eb1bc12cb216e89ac61edc8bd75fab9 はなにかと言うと

1
2
> Digest::SHA256.hexdigest session.id.to_s
=> "4ae09071570b9b6b83af9dc5ed0e219a5eb1bc12cb216e89ac61edc8bd75fab9" # こいつか

session_idをsha256でハッシュ化したものがsession_storeのキーになってるようだ
ちなみにsession.idはRack::Session::SessionIdという型なのでにto_sしてる

1
2
> session.id.class
=> Rack::Session::SessionId

セッション操作

1
2
> session.id
=> "3824df2bfc82fb826556f5b3d851a027"
1
2
> session[:foo] = 'FOO'
=> "FOO"

そもそもSessionがHashの拡張なのかな?
find、clear、each、digなんかも使えるようです

ちゃんと(?)Hashにしたければ

1
2
> session.to_hash
=> {"user_id"=>"foo", "bar"=>"BAR"}

セッションそのものは消えません

1
2
3
4
5
6
7
8
> session[:foo] = :foo
=> :foo
> session.keys
=> ["foo"]
> session.destroy
=> true
> session.keys
=> []

clearでも消える

1
2
3
4
> session[:foo] = :foo
=> :foo
> session.clear
=> {}

普通にはできないっぽい。
redisに保存しているセッション情報自体は強制的に上書きできる

1
2
3
Rails.cache.fetch('_session_id:2::' + Digest::SHA256.hexdigest(cookies[:_session_id]), expires_in: 3.month, force: true) do
  Rails.cache.read('_session_id:2::' + Digest::SHA256.hexdigest(cookies[:_session_id]))
end

でもcookies[:_session_id] の期限は延長されない
cookies[:session_id] = { value: cookies[:session_id], expires: 3.years } としても上書きできなかった
独自のcookieにセッションID複製して、sessionから情報取れなかったらそっから復元的な小細工が必要な気がする
session自体を拡張するのもいいかも

コメント

投稿する

投稿したコメントはご自身で削除できません

不適切なコメントと判断した場合は管理側で削除することがあります