Sessions, cookies với Rails

Cookies, sessions là các đối tượng khá đặc biệt mà Rails cho phép bạn thao tác với chúng tương tự như một đối tượng kiểu hash. Chúng là nơi mà dữ liệu được lưu trữ khi thực hiện một request và bạn có thể đọc được dữ liệu này trong các request tiếp theo.

Cookies

Cookies là dữ liệu được lưu trữ trên browser dưới dạng key-value, mỗi cookie (một cặp key-value) tồn tại cho đến khi nó bị hết hạn (expired). Mỗi lần tạo một request thì browser sẽ gửi hết cookies hiện tại nên server, như vậy khi bạn đặt một cookie nào đó thì tất cả những request tiếp theo server đều có thể đọc được dữ liệu cookie này cho đến khi nó hết hạn. Có thể thấy sử dụng cookies rất thuận tiện để lưu những dữ liệu cần thiết cho các request tiếp theo. Tuy nhiên bạn cần lưu ý không nên lạm dụng cookies để lưu những giá trị không cần thiết bởi cookies bị giới hạn 4 kilobytes. Như vậy nếu trong trường hợp ta đặt cookies nhiều hơn 4kb thì sao, lúc đó cookies sẽ tự động xóa bớt những cookies cũ nhất đi. Bởi vì mỗi một lần tạo request thì browser đều gửi toàn bộ cookies lên server điều đó có nghĩa khi bạn set càng nhiều cookies thì request của bạn càng lớn làm ảnh hưởng tới hiệu năng của hệ thống. Một điểm chú ý nữa là với những dữ liệu quan trọng bạn cần bảo mật thì bạn nên mã hóa với cookies.signed. Dưới đây là một số ví dụ sử dụng cookies trong Rails cũng khá đơn giản

# cookie thông thường
cookies[:demo_normal] = "normal"

# giá trị của cookie sẽ được mã hóa
# cookie này được mã hóa với key là `secrets.secret_key_base`.
cookies.signed[:demo_signed] = "signed"

# giá trị của cookie sẽ được mã hóa tương tự như cookies.signed
# cookie này được mã hóa với key là `secrets.secret_key_base`.
cookies.encrypted[:demo_encrypted] = "encrypted"

# khi dùng http only thì cookie sẽ ko thể đọc được bằng javascript, và cookie sẽ hết hạn sau thời gian expires
cookies[:demo_httponly] = {value: "http only", expires: Time.current + 1.hour, httponly: true}

# khi dùng cookies.permanent cookie này sẽ hết hạn sau một thời gian rất dài  khoảng 20 năm từ thời điểm hiện tại
cookies.permanent[:demo_permanent] = "permanent"

Lưu ý là khi bạn set cookie mà không có thời gian expires thì cookie đó sẽ bị expires theo kiểu của session (expires khi trình duyệt bị đóng).

Chắc cũng nhiều bạn thắc mắc sau khi chạy những lệnh trên thì làm thế nào để cookie lại xuất hiện ở trên browser. Để đặt giá trị cookie cho browser thì trong phần headers của kết quả (response) sẽ có thêm các thẻ Set-Cookie dùng để đặt giá trị cookie cho trang web này. Dưới đây là kết quả chạy thử

Response headers

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Etag: W/"7dfeec36fcf54c1bf3063bd2bb8daf80"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: fe5db933-6061-490f-9303-4f7777c31c15
X-Runtime: 6.032737
Server: WEBrick/1.3.1 (Ruby/2.3.0/2015-12-25)
Date: Wed, 26 Oct 2016 14:05:54 GMT
Content-Length: 1761
Connection: Keep-Alive
Set-Cookie: demo_normal=normal; path=/
Set-Cookie: demo_signed=InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65; path=/
Set-Cookie: demo_encrypted=ZzNIL2hVL0dUSUtNMVZ4M2JWNFcrZz09LS1QcHRoWUZ1U3NvQXIyWWw1ejhoNm9RPT0%3D--73de26344c37509a1806fa1a7c0c57680b15b9c4; path=/
Set-Cookie: demo_httponly=http+only; path=/; expires=Wed, 26 Oct 2016 15:05:54 -0000; HttpOnly
Set-Cookie: _demo_rails_session=RXBRSEUxYXZwdEdHUERERG8xTXI3c0U2OGJ2T0tZOCszZm14MXNVejdqQzB3RmRPeXgzQm1leEgwamdPbjQ5NU8ycFZ1cXNTRi9FdThmY1ZlNUticTVLRmRjWFpPaEFzRWtDVjJxUFpoUHFzSW5XUDZhMlFXVktOMi94NTVwUkN4SnFMeVN6Yk5BUWpBYWc3ampTRTVRPT0tLSs2bDJyWDBIeXdyR016bnNnTHF3cXc9PQ%3D%3D--e76b4a116d5cc5768871b8118a838e0a7204de37; path=/; HttpOnly
> document.cookie
"demo_normal=normal; demo_signed=InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65; demo_encrypted=RDFVYkdJaU9IRVdGZENBY2hvcWhKdz09LS13OTg0bkwzd2h6RHpKSVBjT1lPTjJBPT0%3D--0da70f7acf3e3413bc0ed893566a144d9eaa468c; demo_permanent=permanent"

Ta thấy vỗi mỗi lệnh set giá trị cookie ở trên sẽ tương ứng với một thẻ Set-Cookie trong headers của kết quả trả về. Nhìn vào response headers trên ta có thể thấy các thẻ Set-Cookie tương ứng với các lệnh như sau:

Lệnh của Rails Headers tương ứng Giải thích
cookies[:demo_normal] = "normal" Set-Cookie: demo_normal=normal; path=/ Set một cookie thường vào path là trang web hiện tại
cookies.signed[:demo_signed] = "signed" Set-Cookie: demo_signed=InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65; path=/ Bạn có thể thấy với dữ liệu ban đầu là "signed" giờ đã được mã hóa thành InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65
cookies.encrypted[:demo_encrypted] demo_encrypted=ZzNIL2hVL0dUSUtNMVZ4M2JWNFcrZz09LS1QcHRoWUZ1U3NvQXIyWWw1ejhoNm9RPT0%3D--73de26344c37509a1806fa1a7c0c57680b15b9c4; path=/ Tương tự với cookie signed thì dữ liệu của cookie encrypted cũng đã được mã hóa
cookies[:demo_httponly] = {value: "http only", expires: Time.current + 1.hour, httponly: true} Set-Cookie: demo_httponly=http+only; path=/; expires=Wed, 26 Oct 2016 15:05:54 -0000; HttpOnly Với lệnh này bạn có thể dễ dàng thấy thêm có một thuộc tính là expires là thời điểm hết hạn của cookie và một flag là HttpOnly nghĩa là cookie này dùng để gửi nhận giữa client và server javascript trên browser sẽ không đọc được cookie này
cookies.permanent[:demo_permanent] = "permanent" Set-Cookie:demo_permanent=permanent; path=/; expires=Sun, 26 Oct 2036 14:58:59 -0000 Với lệnh này sẽ set thuộc tính expires có giá trị là sau 20 năm kể từ thời điểm hiện tại. Tương đương với cookies này sẽ luôn tồn tại

Trong những request tiếp theo thì browser sẽ tự đẩy các cookies hiện tại lên server thông qua request headers

Cookie:demo_normal=normal; demo_signed=InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65; demo_encrypted=WXR3a0dsYmUvMDhvMDQwb00xY0E2dz09LS1FUG5ybDBhNzBOKzMrU2xVMGtackpRPT0%3D--8d1fdb6bacbc05ac188c11927cda2caf8ba6a033; demo_httponly=http+only; demo_permanent=permanent; _demo_rails_session=VkVlSDdFblRoUTVGdkV2WXYyWEZ0aG8zdEpUTkw3TysyZlFzWDRLMC9Yc3FxMFgwS0FYNnF4NUZ1dmQ4NnFDcVNsSGdEa0pmaUdnNkJFU1BXTFRML05EdkhOU1FPTGtmNTczd3RqVS84SDRSQjBRcDRBV1BMTGl3VE9vYVhYZEp1NDJlbHQvNWZqZkMzZG9sd0tkbnhBPT0tLXcrMTcxYUJ2Ylo5cE9UKytHVG9yUlE9PQ%3D%3D--57045b429954ac4af107414890c0e568c338c838Cookie:demo_normal=normal; demo_signed=InNpZ25lZCI%3D--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65; demo_encrypted=WXR3a0dsYmUvMDhvMDQwb00xY0E2dz09LS1FUG5ybDBhNzBOKzMrU2xVMGtackpRPT0%3D--8d1fdb6bacbc05ac188c11927cda2caf8ba6a033; demo_httponly=http+only; demo_permanent=permanent; _demo_rails_session=VkVlSDdFblRoUTVGdkV2WXYyWEZ0aG8zdEpUTkw3TysyZlFzWDRLMC9Yc3FxMFgwS0FYNnF4NUZ1dmQ4NnFDcVNsSGdEa0pmaUdnNkJFU1BXTFRML05EdkhOU1FPTGtmNTczd3RqVS84SDRSQjBRcDRBV1BMTGl3VE9vYVhYZEp1NDJlbHQvNWZqZkMzZG9sd0tkbnhBPT0tLXcrMTcxYUJ2Ylo5cE9UKytHVG9yUlE9PQ%3D%3D--57045b429954ac4af107414890c0e568c338c838

Kiểm tra trên server ta có

> cookies.to_h
=> {"demo_normal"=>"normal",
 "demo_signed"=>"InNpZ25lZCI=--365ab5f60e974e4f0ee22c7dc510d3eb00b13b65",
 "demo_encrypted"=>
  "THNwVUlwdXBVUFNlNmVIU1VKbHNWdz09LS1DeWNYMU9VTlduUnJKZUszQm1XbUNRPT0=--bcfecad9ae72a6b7b1d24727dc67ae9c0f7c56c2",
 "demo_httponly"=>"http only",
 "demo_permanent"=>"permanent",
 "_demo_rails_session"=>
  "QUFYRmJ0WVQxUit1ZTdTVlRrdlkrKzFHQnlDbnM2b1Q3cVM4bG0vdVRrSkY1SnY1SjZsSEZKdVlnSWtUQ1ZDYTdDRUk2eERsNm9wd0l2bnhrOGdNVVlMUkxielRHdjRGcnkzWGpwRDcvZUJOMWd6OTVLNGtNZDlSTlNhbzQrMXlrT2Ixamt3dVRDRVJmSmZkNnVzdm1BPT0tLXovVlgvbUNSM2F6b2IwdUZEUkJQcGc9PQ==--6517a5306b31e1521ddceda4c95852fa302792d0"}
> cookies[:demo_normal]
=> "normal"
> cookies.signed[:demo_signed]
=> "signed"
> cookies.encrypted[:demo_encrypted]
=> "encrypted"

Bạn có thể thấy có thêm một thẻ Set-Cookie khá lạ như sau

Set-Cookie: _demo_rails_session=RXBRSEUxYXZwdEdHUERERG8xTXI3c0U2OGJ2T0tZOCszZm14MXNVejdqQzB3RmRPeXgzQm1leEgwamdPbjQ5NU8ycFZ1cXNTRi9FdThmY1ZlNUticTVLRmRjWFpPaEFzRWtDVjJxUFpoUHFzSW5XUDZhMlFXVktOMi94NTVwUkN4SnFMeVN6Yk5BUWpBYWc3ampTRTVRPT0tLSs2bDJyWDBIeXdyR016bnNnTHF3cXc9PQ%3D%3D--e76b4a116d5cc5768871b8118a838e0a7204de37; path=/; HttpOnly

Thẻ này dùng để đặt giá trị của session id đã được mã hóa vào trong cookie. Vậy session là gì ta cùng tìm hiểu ngay bây giờ.

Sessions

Bản chất HTTP là một giao thức không trạng thái, Session làm cho nó có trạng thái.

Mỗi khi người dùng truy cập vào hệ thống, Rails sẽ kiểm tra xem request này đã có session chưa thông qua cookies mà trình duyệt gửi lên, nếu chưa có session thì Rails sẽ tự động tạo ra một session mới và set cho cookies id (id đã được mã hóa) của session này. Giá trị của cookie lưu session di này sẽ bị mất khi trình duyệt đóng => khi mở lại trình duyệt và request lại sẽ tạo ra một session mới.

Mọi session đều có id – 32 kí tự để xác định danh, nghĩa là tại một thời điểm thì session id là duy nhất.

Do đặc điểm của session và cookies khá giống nhau nên nhiều bạn nhầm lẫn. Có thể hiểu đơn giản như sau với cookies bạn sẽ bị giới hạn dung lượng lưu trữ là 4kb và cookies lưu trữ trên browser. Còn session thì khác dữ liệu của session được lưu trữ trên server (bộ nhớ trong hoặc trong database mysql, redis ...) nên không bị giới hạn dung lượng. Cookie chỉ lưu giá trị của session id sau đó server sẽ đọc giá trị session id từ cookies và lấy ra session tương ứng.

Bạn có thể sử dụng đọc ghi vào session tương tự như với một hash. Điểm khác biệt là bạn không thể set expires cho từng key cho session, như vậy cặp key-value trong session sẽ tồn tại cho đến khi cả session bị xóa, hoặc ta chủ động xóa key này.

Dưới đây là một số thao tác cơ bạn với session

> session.to_hash
=> {"_csrf_token"=>"zOO9DV0SOrxouyy/VWs6G+e6b98ZabC7CftOlj/UceA=",
"session_id"=>"d10e1cf53b22396bb0f6a952918fa903"}
> session[:user_id] = 1
> session[:test] = "test"
> session.to_hash
=> {"_csrf_token"=>"zOO9DV0SOrxouyy/VWs6G+e6b98ZabC7CftOlj/UceA=",
 "session_id"=>"d10e1cf53b22396bb0f6a952918fa903",
 "user_id"=>1,
 "test"=>"test"}
> session.delete(:test) # xóa session[:test] có thể dùng session[:test] = nil
> session.to_hash
=> {"_csrf_token"=>"zOO9DV0SOrxouyy/VWs6G+e6b98ZabC7CftOlj/UceA=",
"session_id"=>"d10e1cf53b22396bb0f6a952918fa903", "user_id"=>1}
> session.clear # chú ý lệnh này chỉ xóa dữ liệu trong session chứ không phải xóa cả session id, request tiếp theo vẫn sử dụng session id này
> session.to_hash
=> {}