Chapter 13. Session Management

 

I’d hate to wake up some morning and find out that you weren’t you!

 
 --Dr. Miles J. Binnell (Kevin McCarthy) in “Invasion of the Body Snatchers” (Allied Artists, 1956)

HTTP is a stateless protocol. Without the concept of a session (a concept not unique to Rails), there’d be no way to know that any HTTP request was related to another one. You’d never have an easy way to know who is accessing your application! Identification of your user (and presumably, authentication) would have to happen on each and every request handled by the server.[1]

Luckily, whenever a new user accesses our Rails application, a new session is automatically created. Using the session, we can maintain just enough server-side state to make our lives as web programmers significantly easier.

We use the word session to refer both to the time that a user is actively using the application, as well as to refer to the persistent hash data structure that we keep around for that user. That data structure takes the form of a hash, identified by a unique session id, a 32-character string of random hex numbers. When a new session is created, Rails automatically sends a cookie to the browser containing the session id, for future reference. From that point on, each request from the browser sends the session id back to the server, and continuity can be maintained.

The Rails Way to design web applications dictates minimal use of the session for storage of stateful data. In keeping with the “share nothing” philosophy embraced by Rails, the proper place for persistent storage of data is the database, period. The bottom line is that the longer you keep objects in the user’s session hash, the more problems you create for yourself in trying to keep those objects from becoming stale (in other words, out of date in relation to the database).

This chapter deals with matters related to session use, starting with the question of what to put in the session.

What to Store in the Session

Deciding what to store in the session hash does not have to be super-difficult, if you simply commit to storing as little as possible in it. Generally speaking, integers (for key values) and short string messages are okay. Objects are not.

The Current User

There is one important integer that most Rails applications store in the session, and that is the current_user_id. Not the current user object, but its id. Even if you roll your own login and authentication code (which you shouldn’t do), don’t store the entire User (or Person) in the session while the user is logged in. (See Chapter 14, “Login and Authentication,” for more information about keeping track of the current user.) The authentication system should take care of loading the user instance from the database prior to each request and making it available in a consistent fashion, via a method on your ApplicationController. In particular, following this advice will ensure that you are able to disable access to given users without having to wait for their session to expire.

Session Use Guidelines

Here are some more general guidelines on storing objects in the session:

  • They must be serializable by Ruby’s Marshal API, which excludes certain types of objects such as a database connection and other types of I/O objects.

  • Large object graphs may exceed the size available for session storage. Whether this limitation is in effect for you depends on the session store chosen and is covered later in the chapter.

  • Critical data should not be stored in the session, since it can be suddenly lost by the user ending his session (by closing the browser or clearing his cookies).

  • Objects with attributes that change often should not be kept in the session. Think of a situation where you store a model with counter cache attributes, and constantly refer to it out of the session rather than pulling it from the database. The counters would never be up-to-date.

  • Modifying the structure of an object and keeping old versions of it stored in the session is a recipe for disaster. Deployment scripts should clear old sessions to prevent this sort of problem from occurring, but with certain types of session stores, such as the cookie store, this problem is hard to mitigate. The simple answer (again) is to just not keep objects in the session.

Session Options

A number of basic session options are available for use in controllers, via the session class method. Calls to session can be placed in ApplicationController, or at the top of specific controllers in your application.

For example, some applications do not need to track user sessions, in which case you can get a tremendous performance boost by turning off that part of Rails’ request handling:

# turn off session management for all actions.
session :off

As with other configuration-type class methods in controllers, the :except and :only options are supported:

# turn off session management for all actions _except_ foo and bar.
session :off, :except => %w(foo bar)

# turn off session management for only the atom and rss actions.
session :off, :only => %w(atom rss)

The :if option is also available and is useful for things like checking to see if a particular request’s attributes merit a session or not:

# the session will only be disabled for 'foo',
# and only if it is requested as a web service
session :off, :only => :foo, :if => lambda { |req| req.parameters[:ws]
}

Disabling Sessions for Robots

If you are running a public website, web-crawling and spidering agents (also known as robots) will eventually find you. Since they don’t support cookies, every request they make causes a new session to be created, a totally unnecessary burden on your server.

Turning off sessions specifically for robots is pretty easy, since they identify themselves via the user agent header of the HTTP request. All you have to do is add session :off to your ApplicationController with a dynamic :if condition, as showing in Listing 13.1.

Example 13.1. Disabling Sessions for Robots by Inspecting the User Agent String

class ApplicationController < ActionController::Base
  session :off, :if => lambda {|req| req.user_agent =~
/(Google|Slurp)/i}

A typical Googlebot identifies itself as: “Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)”. Yahoo’s robot identifies itself as: “Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)”. It’s worth doing some research using your web server’s access logs to determine which robots are visiting your site and which user agent strings you need to match. Then add those strings to the regular expression inside the lambda block.

There’s one more aspect to this technique, having to do with testing. As of this writing, the TestRequest class does not have a user_agent method. This means that your controller tests or specs will blow up after adding a call to req.user_agent as shown in Listing 13.1. Thankfully, Ruby’s open classes mean we can remedy the situation with ease. Just add the code in Listing 13.2 to your test_helper.rb or spec_helper.rb file.

Example 13.2. Monkeypatching the TestRequest Class to include user_agent

class ActionController::TestRequest
  def user_agent
    "Mozilla/5.0"
  end
end

Selectively Enabling Sessions

Suppose you have a truly nonstateful application and have turned off sessions for the entire thing in your ApplicationController:

class ApplicationController < ActionController::Base
  session :off

You may still want to selectively enable sessions for certain controllers, for instance, an administrative console. You can’t say session :on in a subclass of ApplicationController—it won’t work, but surprisingly, you can say session :disable => false.

class AdminController < ApplicationController
  session :disable => false

Secure Sessions

Sometimes you need to set up your Rails app so that sessions only work with HTTPS:

# the session will only work over HTTPS
session :session_secure => true

The :session_secure option can also be used in conjunction with :only, :except, and :if options, to secure only specific parts of your application. Keep in mind that your web server will need to be set up to work securely in order for this option to work.

Storage Mechanisms

The mechanism via which sessions are persisted on your Rails server can vary and you should pick the one most suited to your particular needs. I estimate that 80% or more of production Rails deployments use the ActiveRecord Session Store.

ActiveRecord SessionStore

Rails’ default behavior is to store session data as files in the /tmp/sessions folder of your project, which is fine for experimentation and very small applications. For larger applications, it is advisable to minimize the amount of interaction Rails does with the filesystem, and that includes sessions.

There are a number of options for optimizing session storage, but the most common is to use ActiveRecord so that session data is stored in the database. In fact, it’s so common that the tools to switch over to this setup are already built into Rails.

The first step is to create the necessary migration, using a rake task provided for that very purpose, and run the migration to actually create the new table:

$ rake db:sessions:create
      exists  db/migrate
      create  db/migrate/009_add_sessions.rb

$ rake db:migrate
(in /Users/obie/prorails/time_and_expenses)
== AddSessions: migrating
==============================================
-- create_table(:sessions)
   -> 0.0049s
-- add_index(:sessions, :session_id)
   -> 0.0033s
-- add_index(:sessions, :updated_at)
   -> 0.0032s
== AddSessions: migrated (0.0122s)
=====================================

The second (and final) step is to tell Rails to use the new sessions table to store sessions, via a setting in config/environment.rb:

config.action_controller.session_store = :active_record_store

That’s all there is to it.

PStore (File-Based)

The default session storage mechanism for Rails is to keep the session data as PStore-formatted files in a tmp directory. The files will contain the contents of session hashes in their native serialized form. You don’t have to change any settings to use this option.

If you are running a high-traffic site, you definitely do not want to use this option! It’s slow, because marshalling/unmarshalling data structures in Ruby is slow. The server will also suffer severe strainage from having to maintain thousands of session files in a single directory, possibly running out of file descriptors. I’ve seen (and blogged about) a live Rails site going offline because the partition holding their session files ran out of space!

DRb Session Storage

DRb is Ruby’s remoting service. It lets Ruby processes easily share objects over the network. In order to use DRb session storage, you need to run a separate DRb server process that will serve as the session repository. When you start bringing additional processes into the picture, the entire deployment becomes more complicated to maintain properly. So unless you really need to consider this option for performance reasons, you’re better off sticking with the ActiveRecord Session Store.

At the time I’m writing this, DRb session storage is really unpopular and according to some blogs, it doesn’t even work properly. If you do want to experiment with DRb and Rails sessions, search for the drb_server.rb script in your Rails source code.

config.action_controller.session_store = :drb_store

If you think you’re outgrowing the ActiveRecord Session Store, look into Stefan Kaes’ extra-super-optimized version[2] or consider memcache session storage.

memcache Session Storage

If you are running an extremely high-traffic Rails deployment, you’re probably already leveraging memcache in some way or another. memcache is a remote-process memory cache that helps power some of the most highly trafficked sites on the Internet.

The memcache session storage option lets you use your memcache server as the repository for session data and is blazing fast. It’s also nice because it has built-in expiration, meaning you don’t have to expire old sessions yourself. However, it is also much more complicated to set up and maintain.[3]

config.action_controller.session_store = :mem_cache_store

In order to get the memcache session storage working, you’ll need to make sure that memcache’s settings are included in your environment.rb file:

require 'memcache'
memcache_options = {
  :c_threshold => 10_000,
  :compression => true,
  :debug => false,
  :namespace => :app-#{RAILS_ENV}",
  :readonly => false,
  :urlencode => false
}

CACHE = MemCache.new memcache_options
CACHE.servers = 'localhost:11211'

ActionController::Base.session_options[:expires] = 1800
ActionController::Base.session_options[:cache] = CACHE

The Controversial CookieStore

In February 2007, core-team member Jeremy Kemper made a pretty bold commit to Rails. He changed the default session storage mechanism from the venerable PStore to a new system based on a CookieStore. His commit message summed it up well:

Introduce a cookie-based session store as the Rails default. Sessions typically contain at most a user_id and flash message; both fit within the 4K cookie size limit. A secure hash is included with the cookie to ensure data integrity (a user cannot alter his user_id without knowing the secret key included in the hash). If you have more than 4K of session data or don’t want your data to be visible to the user, pick another session store. Cookie-based sessions are dramatically faster than the alternatives.

In order to use the CookieStore, you have to be running Rails 2.0 (where it is the default) or add the following configuration to environment.rb:

config.action_controller.session = {
  :session_key => '_my_app_session',
  :secret      => 'some_really_long_and_hashed_key'
}

I describe the CookieStore as controversial because of the fallout over making it the new default session storage mechanism. For one, it imposes a very strict size limit, only 4K. A significant size constraint like that is fine if you’re following the Rails way, and not storing anything other than integers and short strings in the session. If you’re bucking the guidelines, well, you might have an issue with it.

Lots of people also complained about the inherent insecurity of storing session information, including the current user information on the user’s browser. However, there are security measures in place that make the cookie store hard to crack. For instance, you’d need to be able to compromise SHA512, and that is somewhat difficult to do.

If you want better security[4], for instance, you can easily override the existing hashing code:

class CGI::Session::CookieStore
  def generate_digest(data)
    # replace this line with your own encryption logic
    Digest::SHA512.hexdigest "#{data}#{@secret}"
  end
end

Another problem with cookie-based session storage is its vulnerability to replay attacks, which generated an enormous message thread on the rails-core mailing list. S. Robert James kicked off the thread[5] by describing a replay attack:

Example:

  1. User receives credits, stored in his session.

  2. User buys something.

  3. User gets his new, lower credits stored in his session.

  4. Evil hacker takes his saved cookie from step #1 and pastes it back in his browser’s cookie jar. Now he’s gotten his credits back.

This is normally solved using something called nonce —each signing includes a once-only code, and the signer keeps track of all of the codes, and rejects any message with the code repeated. But that’s very hard to do here, since there may be several app servers (mongrels).

Of course, we could store the nonce in the DB—but that defeats the entire purpose!

The short answer is: Do not store sensitive data in the session. Ever. The longer answer is that coordination of nonces across multiple servers would require remote process interaction on a per-request basis, which negates the benefits of using the cookie session storage to begin with.

The cookie session storage also has potential issues with replay attacks that let malicious users on shared computers use stolen cookies to log in to an application that the user thought he had logged out of. The bottom line is that if you decide to use the cookie session storage, please think long and hard about the implications of doing so.

Timing Out and Session Life Cycle

Quite often you are required to time out a user’s session if they are idle for a certain length of time. Amazingly, this basic functionality is not available by default in Rails. Using built-in session options, you can set a specific expiry time; however, when your Rails application starts up in production mode, the expiry time will be set just once. That’s fine if you are setting your expiry time far in the future (beyond the time that you are likely to restart your server processes).

What if you want to set your timeout in the near future? Say perhaps, 20 minutes from time of session creation? Would the following work?

class ApplicationController < ActionController::Base
  session :session_expires => 20.minutes.from_now
end

The problem is that 20 minutes from now (the time that the server process is started) will soon become a time in the past. When the session expiry date is in the past, every new request will cause a new session to be created, and much havoc and misery will transpire.

Session Timeout Plugin for Rails

Luckily, there is a well-proven plugin that solves our problem. It is written by Luke Redpath and is available at http://opensource.agileevolved.com/trac/wiki/SessionTimeout.

After installing the plugin in your application, there will be a session_times_out_in method available for use in ApplicationController. The first parameter is the duration in seconds after which the session should expire, and you can use Rails’ convenience methods to keep your code very readable.

For example, let’s implement that 20-minute timeout we were discussing a minute ago:

class ApplicationController < ActionController::Base
  session_times_out_in 20.minutes
end

The second (optional) parameter allows you to specify a callback method to be invoked when a request comes in that causes the session to be expired. It might be necessary to do some end-of-session cleanup, or redirect the user to a particular place, things of that nature.

As is common with Rails callbacks, the second parameter value can be a symbol, referring to a method:

class ApplicationController < ActionController::Base
  session_times_out_in 20.minutes, :after_timeout => :log_timeout_msg

  private

  def log_timeout_msg
    logger.info "Session expired"
  end
end

Or you can specify a Proc or lambda. It will be passed an instance of the current controller, which I’ve ignored in the example:

class ApplicationController < ActionController::Base
  session_times_out_in 20.minutes,
    :after_timeout => proc {|controller| logger.info "Session expired"
}
end

Elegant, isn’t it?

Tracking Active Sessions

Quite often we want a way to display the number of active users of an application. If you are using ActiveRecord Session Store, this is very easy to accomplish. The sessions table gets created by default with an updated_at column. Assuming your definition of active is one hour, the code to count active users would look like this:

CGI::Session::ActiveRecordStore::Session.count :conditions =>
["updated_at > ?", 1.hour.ago ]

In fact, objects found using the Session class can be used just like any other ActiveRecord instance. The session contents are stored in the data attribute, serialized (in a non-human-readable way) into a text column, so it isn’t possible (by default) to query sessions by their content. Want to see what it looks like?

>> CGI::Session::ActiveRecordStore::Session.find:first
=> #<CGI::Session::ActiveRecordStore::Session:0x26fe65c

@attributes={"updated_at"=>"2006-11-29 02:06:01",
"session_id"=>"73bb9cd7fd19a5c1cae8cd0fda0cb6bb", "id"=>"1",
"data"=>"BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNo
SG
FzaHsABjoKQHVzZWR7AA==
"}>

So if you want to query for data contained inside the session data, say for instance, to display a list of names of the current users, then it’s going to take a little bit more work. In a nutshell, you’d have to add another column to the sessions table and add an after_filter to your ApplicationController that stores the data you want on a per-request basis. (The full implementation is left as an exercise for the reader.)

Enhanced Session Security

Erik Elmore gives a very long and detailed description of how to write a “paranoid” session store on his blog.[6] Among other things, his implementation provides protection against session-fixation attacks (by capturing IP addresses as part of the session record), a facility for being able to see which users are “online,” and the ability to administratively end sessions of troublemakers. It’s also “lightning fast” because it uses the database directly instead of instantiating ActiveRecord objects. His article may not be entirely applicable to you, particularly since it is written for MySQL, but is definitely worth examination if you will be tackling advanced session work.

Cleaning Up Old Sessions

If you’re using Luke Redpath’s session_timeout plugin and ActiveRecordStore, cleaning up old sessions is really easy. Remember that the session timeout gives you an :after_timeout option?

You can also write your own little utilities for maintaining your sessions. Listing 13.3 is a class that you can add to your /lib folder and invoke from the production console or a script whenever you need to do so.

Example 13.3. SessionMaintenance Class for Cleaning Up Old Sessions

class SessionMaintenance

 def self.cleanup(period=24.hours.ago)
   session_store = CGI::Session::ActiveRecordStore::Session
   session_store.destroy_all( ['updated_at < ?', period] )
 end

end

Cookies

This section is about using cookies, not the cookie session store. The cookie container, as it’s known, looks like a hash, and is available via the cookies method in the scope of controllers. Lots of Rails developers use cookies to store user preferences and other small nonsensitive bits of data. Always be careful not to store sensitive data in cookies, since they can easily be read and modified by malicious users. The database is a more appropriate place to store sensitive data.

Contrary to what at least some developers might expect, the cookies container is not available by default in view templates or helpers. If necessary, and in accordance with proper model-view-controller practice, you should set an instance variable in your controller with the value of a cookie for use in your view:

@list_mode = cookies[:list_mode] or 'expanded'

If you are really intent on being able to access cookies in your helpers or views, there is a simple solution. Simply declare cookies to be a helper method:

class MyController < ActionController::Base
  helper_method :cookies

Reading and Writing Cookies

The cookie container is filled with cookies received along with the request, and sends out any cookies that you write to it with the response. Note that cookies are read by value, so you won’t get the cookie object itself back, just the value it holds as a string (or as an array of strings if it holds multiple values). That’s a limitation, but I’m not sure how severe of a limitation it is in practice.

To create or update cookies, you simply assign values using the brackets operator. You may assign either a single string value or a hash containing options, such as :expires, which takes a number of seconds before which the cookie should be deleted by the browser. Remember that Rails convenience methods for time are useful here:

# writing a simple session cookie
cookies[:list_mode] = params[:list_mode]

# specifying options, curly brackets are needed to avoid syntax error
cookies[:recheck] = {:value => "false", :expires => Time.now +
5.minutes}

I find the :path options useful in allowing you to set options specific to particular sections or even particular records of your application. The :path option is set to '1', the root of your application, by default.

The :domain option allows you to specify a domain, which is most often used when you are serving up your application from a particular host, but want to set cookies for the whole domain.

cookies[:login] = {:value => @user.security_token,
                   :domain => '.domain.com',
                   :expires => Time.now.next_year }

Cookies can also be written using the :secure option, and Rails will only ever transmit them over a secure HTTPS connection:

# writing a simple session cookie
cookies[:account_number] = { :value => @account.number, :secure =>
true }

Finally, you can delete cookies using the delete method:

cookies.delete :list_mode

Conclusion

Deciding how to use the session is one of the more challenging tasks that faces a web application developer. That’s why we put a couple of sections about it right in the beginning of this chapter. We also covered the various options available for configuring sessions, including storage mechanisms and methods for timing out sessions and the session lifecycle.

Moving on, we’ll continue with a related topic: login and authentication.

References

1.

If you are really new to web programming and want a very thorough explanation of how web-based session management works, you may want to read the information available at http://www.technicalinfo.net/papers/WebBasedSessionManagement.html.

2.

Find Stefan Kaes’ super-optimized ActiveRecord SessionStore at http://railsexpress.de/blog/articles/2005/12/19/roll-your-own-sql-session-store.

3.

Geoffrey Grosenbach has a fantastic tutorial on memcache basics at http://nubyonrails.com/articles/2006/08/17/memcached-basics-for-rails.

4.

My fellow cabooser Courtenay wrote a great blog post about cookie session storage at http://blog.caboo.se/articles/2007/2/21/new-controversial-default-rails-session-storage-cookies.

5.

If you want to read the whole thread (all 83 messages of it), simply search Google for “Replay attacks with cookie session.” The results should include a link to the topic on the Ruby on Rails: Core Google Group.

6.

http://burningtimes.net/articles/2006/10/15/paranoid-rails-session-storage

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.156.93