Rails預設使用 Cookie 來儲存session設定的資料,也就是說設定給session的資料,都會發送給客戶端儲存為Cookie,Rails預設就有一些資料會使用session儲存,因此即使沒作什麼Cookie操作,瀏覽器也會收到這段Cookie標頭設定:
Set-Cookie _test_session=BAh7B0kiD3Nlc3Np...略3D%3D--37d9137b3f0790190e3dea8cfa6cf79779a715f6; path=/; HttpOnly
這個預設的Cookie標頭中,包括了session設定的資料,預設session中就會有session_id與_csrf_token資料,Rails將session設定的資料作BASE64編碼,並將該資料進行訊息摘要後,附在原BASE64編碼的資料之後,實際處理的片段原始碼是:
session_data = coder.encode(session)
if @secret
session_data = "#{session_data}--#{generate_hmac(session_data, @secret)}"
end
所以收到的Cookie資料中,--之前是BASE64編碼(所以別用session放信用卡這類的訊息),--之後是訊息摘要,目的是為了保證Cookie的完整性,產生訊息摘要時的@secret,可以設定:
config.action_dispatch.session = {
:key => '_app_session',
:secret => '0x0dkfj3927dkc7djdh36rkckdfzsg...'
}
:key就是設定Cookie的鍵名稱(像是先前_test_session這樣的名稱),:secret建議設定超過30字元的無意義混合內容。
Rails預設使用Cookie來儲存session設定的訊息,好處是伺服端不會有清除session設定的需求,伺服器實際上收到的Cookie中包括全部session設定的訊息,因此實際上不需要session id,不過因為儲存為Cookie,因此有總容量4KB的限制。
在 讀取請求本體 中提過,為了避免 跨站偽照請求,預設Rails對於非GET的請求,都要求有個authenticity_token參數,如果你使用Rails的HTML輔助方法,如form_tag或form_for,就會自動在表單中建立帶有authenticity_token參數的隱藏欄位,值則來自ActionController::RequestForgeryProtection的form_authenticity_token方法,原始碼為:
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 99
def form_authenticity_token
session[:_csrf_token] ||= SecureRandom.base64(32)
end
如果是第一次造訪,會隨機產生一個BASE64編碼,並儲存在session[:_csrf_token],驗證請求中是否有authenticity_token的原始碼是:
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 74
def verify_authenticity_token
unless verified_request?
logger.debug "WARNING: Can't verify CSRF token authenticity" if logger
handle_unverified_request
end
end
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 83
def handle_unverified_request
reset_session
end
# File actionpack/lib/action_controller/metal/request_forgery_protection.rb, line 92
def verified_request?
!protect_against_forgery? || request.get? ||
form_authenticity_token == params[request_forgery_protection_token] ||
form_authenticity_token == request.headers['X-CSRF-Token']
end
簡單地說,控制器會檢查authenticity_token參數值是否等於form_authenticity_token的傳回值,如果不是,會執行reset_session方法,因此重置了session設定的資料,從而避免了跨站偽造請求問題。
如果要用程式調整session的選項,可以透過request.session_option方法,這會傳回Rack::Session::Abstract::OptionsHash實例,你可以在其中設定選項,設定的內容將影響儲存session資料的Cookie。例如:secure可要求透過HTTPS傳送,:domain可設定Cookie的Domain,:path可設定Cookie的path,而:expire_after可設定有效期限,也就是設定session資料的存活期,單位是秒(以上也可以在config.action_dispatch.session中設定)。例如:
request.session_options[:expire_after] = 600 # 10 分鐘後失效
如上設定之後,會收到以下的標頭:
Set_Cookie _test_session=BAh7B0ki略...; path=/; expires=Wed, 04-Jan-2012 02:05:44 GMT; HttpOnly
這可以防止客戶閒置過久,自動讓儲存session的Cookie失效,也就是相當於讓session設定資料失效,不過如果有Cookie存活期限,使用者直接關掉瀏覽器後再開啟,並連接應用程式,由於儲存session的Cookie資料仍然有效,使用者此時還是可以取得session設定資料。如果這不是你想要的行為,那就用撰寫程式方式控制使用者閒置時間。例如:
before_filter :check_session
def check_session
reset_session if session[:last_active] and session[:last_active] < 10.minute.ago
session[:last_active] = Time.now
end
可以將session資料儲存在資料庫中,這需要建立資料庫:
~\$ rake db:migrate
將config/initializers/session_store.rb中的:
Test::Application.config.session_store :cookie_store, key: '_test_session'
改為:
Test::Application.config.session_store :active_record_store, key: '_test_session'
重新啟動Rails伺器之後,會發現收到以下標頭:
Set-Cookie _test_session=a11f22764b5bd99ee535ae382646942c; path=/; HttpOnly
Rails只傳送session id,session設定的資料則儲存在伺服端的資料庫,瀏覽器每次發送session id給Rails,Rails用session id找出資料庫中哪一筆資料是你的sesion設定資料。
如果客戶端關掉Cookie的話怎麼辦?可以在將config/initializers/session_store.rb中設定:cookie_only為false:
Test::Application.config.session_store :cookie_store, key: '_test_session', cookie_only: false
如此將允許從請求參數中讀取session id,你可以將session id請求參數安插在URL中,或者是隱藏欄位中一併發送給伺服器,就可以取得對應的session設定資料,可透過request.session_options[:id]來取得session id,request.session_options[:key]可取得儲存Cookie的名稱。例如若打算採隱藏欄位,可以在表單上加入:
<%= hidden_field_tag request.session_options[:key], request.session_options[:id] %>
def default_url_options(options = nil)
{ request.session_options[:key] => request.session_options[:id] }
end
想要檢視儲存session設定的資料庫,可以使用ActiveRecord::SessionStore::Session,欄位有id、session_id、data、created_at、updated_at。例如,想要定時清資料庫上的session資料,例如:
* print [session.session_id, session.data], "\n"
> }
["18c658b41d4a26db2875b362a3f6c42b", {"_csrf_token"=>"k1JEor+GeSOVZtKkmr2XPvJEzn3vJiLB52qEXO0cum4="}]
["1fa0edebe5c2b6237455c494fd5017a8", {"_csrf_token"=>"P/y1FW4hDfewPBbKHdHKt00Lx8HIFn7eXvs4ul/d5FM="}]
如果要定時清理閒置的session設定資料,可以先如下:
before_filter :check_session
def check_session
reset_session if session[:last_active] and session[:last_active] < 10.minute.ago
session[:last_active] = Time.now
end