在Rails的会话管理中,什么是最佳选择?
前提
在多个web应用程序上启动并讨论如何实现它们的用户登录会话管理。
在Rails中,有很多可用的会话管理方法,通过总结各自的优缺点来寻求最佳解决方案。
以下是本次比较的会话管理方法的种类:
– CookieStore(Cookie方式)
– Redis(内存方式)
– ActiveRecord(数据库方式)
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 中获取会话信息,并进行恢复和在控制台中显示。
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(内存数据存储方式)
机制 (jī zhì)
Rails将创建session_id并将其与Session信息一起发送到Redis服务器。
客户端的Cookie仅保存已创建的session_id,并在请求时发送session_id。
Rails会将从客户端接收到的session_id发送到Redis服务器进行搜索,以确定用户并获取Session信息。
Redis会定期保存并持久化保存在内存中的数据。
通过配置,可以为Redis中注册的Session设置有效期。
优点
-
- インメモリのため、処理が早い。
-
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。
- エクスパイア処理を1行の設定で出来る。
缺点 (quē
-
- メモリを使い果たすと書き込みが全てエラーになる。
-
- 情報を検索する必要があるため、クッキー方式に比べると処理が遅い。(メリットにある様に、基本的に早い部類ではある)
-
- スナップショットを保存してから、次のスナップショット保存までの期間にサーバが停止・再起動した場合は、その間のデータが失われる。
- 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_id从Redis服务器获取Session信息并显示,将此源代码添加进去。
请根据您自己的环境设置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服务器中是否保存了会话,请说明。
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服务器上检查会话,如果会话在10秒后消失了,则表示成功。
ActiveRecord(数据库方法)
系统
Railsはsession_idを作成し、Session情報と共にDBサーバに送信する。
クライアントのCookieには作成したsession_idのみを保存し、リクエストの際にsession_idを送信する。
Railsはクライアントから受け取ったsession_idをDBサーバに検索させ、ユーザの特定・Session情報の取得を行う。
好处
-
- DBに保存されているため、データが常に永続化されている。
-
- 基本的にsession情報に比べるとDBの容量は遥かに大きいため、容量を気にせず情報を保存できる。
- クライアントにはIDしか保存しないため、情報漏洩のリスクが低い。
缺点 (quē
-
- DBに対して情報を検索する必要があるため、処理が遅い。(インメモリに比べて1000倍ほど処理時間が掛かると言われている)
- エクスパイア処理をバッチ等で作成するか、またはSession情報を溜めっぱなしにすることになる。
实践 (shí
准备
构建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_id从DB服务器获取Session的信息,并添加显示的源代码。
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="}
另外,还需要确认一下会话是否保存在数据库的sessions表中。
最佳解答
每种方法都有优缺点,各有千秋。
没有最佳解,感觉应根据产品和环境的不同来选择采用的方法。
以下是相关内容。
曲奇店
当遭受会话重放攻击并不带来巨大风险时,应该采用该方法。特别是与其他应用程序的协作更容易,这是一个重要的优势。
Redis (中文:远程字典服务)
如果存在承受会话重放攻击的高风险情况,并且希望在服务器端进行会话信息的更改,应该采用这种方法。尽管成本较高,但能够尽可能地减少风险并且具备快速处理速度,因此可以称之为混合管理方法。
ActiveRecord
活动记录
如果在存在重播攻击风险的情况下,并且希望在服务器上进行会话信息修改且希望控制成本,这时应该采用这种方法。可用性很高,但由于处理速度较慢且需要创建过期处理,我觉得不应主动采用。