検証用に適当なアプリ作って ブレークポイントで止めてsessionの中身をアレコレ確認していきます
Rails 6.1.3
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-linux-musl]
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
環境変数は.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は定義すらない
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"
1
2
3
4
5
class Application < Rails::Application
config.load_defaults 6.1
config.api_only = true
config.middleware.use ActionDispatch::Cookies # <- コレ追加
end
これでレスポンスにクッキーが乗るようになる
同一ドメインなら次回アクセス時にも送信されて参照できる
develoment環境のキャッシュを有効化
1
$ touch tmp/caching-dev.txt
session_storeを有効化
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)にセッション情報を保存します
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
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
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
に保存されてる
サーバに戻って確認すると
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自体を拡張するのもいいかも
コメント