session 原理


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:sessions:create
~\$ 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] %>

如果想要安插在URL中,可以在控制器中定義:

def default_url_options(options = nil)
    { request.session_options[:key] => request.session_options[:id] }
end

default_url_options的傳回值是個Hash,如此透過URL輔助方法產生的URL,會自動加上這個Hash所建立的請求參數。

想要檢視儲存session設定的資料庫,可以使用ActiveRecord::SessionStore::Session,欄位有id、session_id、data、created_at、updated_at。例如,想要定時清資料庫上的session資料,例如:

> ActiveRecord::SessionStore::Session.all.each { |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

如果使用資料庫儲存session設定資料,呼叫reset_session時,若可從客戶端取得session id,則會將資料庫中session_id欄位相同的該筆資料刪除,而後建立新的一筆資料作為儲存session設定資料之用,但對於使用者自行關閉瀏覽器的情況,這個作法就沒用了,可以在伺服端寫個定時清理資料庫中無效資料的程式來搭配。例如: