Rails 6中,会话键都是字符串,这与所有指南都不一致
我一直在与一个已经维护和升级多年的旧Rails项目中的会话不稳定问题作斗争。
多年来越来越严重的问题是会话不可靠。我使用CookieStore,只存放少量值,例如user_id、language、expiry。
所有Rails指南都使用符号键来访问会话中的信息,例如这里:https://guides.rubyonrails.org/v6.1/action_controller_overview.html#session。当我用这种方式访问我的会话时,值永远找不到。只有用字符串键访问时才会找到:
session[:user_id] # is nil when read, even when written to in exactly this form
session['user_id'] # can be written to and read from, albeit somewhat unreliably
这与我能找到的所有指南相矛盾。
如果我像这样从刚写入的会话中读取:
session[:user] = {} #{id: user.id.to_s, expires: expire_time}
session[:user][:id] = user.id.to_s
session[:user][:expires] = expire_time
puts "=================================="
puts session[:user]
... session[:user] 可以被读取。 但只要浏览器返回响应,session[:user] 不再存在,它已经变成 session['user']。
当我通过puts将会话输出到控制台时,它显示为(示例):
{"session_id"=>"e0afa20a74bb6d33e8515221c569a46c", "language"=>:de, "_csrf_token"=>"6HPNA_CnKnCIRAxA-6ejDGWrQgV4ajAqdJ52dWxbvI8=", "customer"=>{"id"=>"5b90e1ed135985225a478308", "expires"=>1772020941}, "expires"=>1772126504, "user"=>{"id"=>"58aecd757273322e58000000", "expires"=>1772202338}}
我唯一的配置是
(in config/initializers/session.store.rb):
Rails.application.config.session_store :cookie_store, key: '_sc24_session', domain: :all
以及对于Devise(config/initializers/devise.rb):
config.skip_session_storage = [:http_auth]
我完全不知道发生了什么。为什么会把会话键转换?这是我错过的某个项目配置吗?还是与Devise有关?我使用Rails 6.1.7.10,Devise 4.9.4。
然而,当我用字符串键把用户数据写入会话时,结果会是会话需要写入两次。第一次登录时(会话被写入),用户会直接被重定向到登录页,日志显示会话为空。第二次登录时就能工作。
解决方案
这种转换是由序列化策略引起的。
默认情况下Rails cookie存储依赖JSON策略。符号是Ruby的一个概念,JSON无法处理它们,因此在这个过程中所有键都会变成字符串。
如果我像这样从刚写入的会话中读取:
session[:user] = {} #{id: user.id.to_s, expires: expire_time} session[:user][:id] = user.id.to_s session[:user][:expires] = expire_time puts "==================================" puts session[:user]...
session[:user]可以被读取。
这是因为在这一点上还没有应用序列化。
但一旦浏览器把响应返回来,
session[:user]不再存在,它已经变成session['user']。
一旦发出请求,JSON序列化已经发生,现在你只有字符串化的键。
JSON序列化的替代方案,是让你显式定义Marshal策略来保留Ruby对象。理论上,你可以在 config/initializers/cookies_serializer.rb 中执行以下操作:
Rails.application.config.action_dispatch.cookies_serializer = :marshal
然而,这并不推荐。JSON被视为最安全的方法,这也是Rails在 4.1版本中将默认从Marshal切换为JSON的原因。
最佳实践是在可能的情况下保持JSON,并假设会话数据的键是字符串。作为变通方法,如果你偏好把它们当作符号来操作,可以使用 HashWithIndifferentAccess。示例:
> session_data = {"session_id"=>"e0afa20a74bb6d33e8515221c569a46c", "language"=>:de}.with_indifferent_access
=> {"session_id" => "e0afa20a74bb6d33e8515221c569a46c", "language" => :de}
> session_data[:session_id]
=> "e0afa20a74bb6d33e8515221c569a46c"
> session_data[:language]
=> :de