It seems to me that Rails should provide some magical mechanism that obfuscates IDs in URLs when running in production mode. Unfortunately it doesn’t, at least not to my knowledge. So we’re stuck doing it ourselves.
There are many approaches to obfuscating database IDs in URLs – some more clean than others, some more secure. The purpose of this article isn’t to discuss the various methods, it is to simply illustrate what worked for me for one particular application.
Here are the steps to take on an existing project that already has one or more models:
1. Create a class to handle the obfuscation.
Create a class named IdCrypt, or whatever works for you, and put it in the lib directory. The following sample code uses Knuth’s integer hash to obfuscate the ID, but it can be any reversible hash that avoids collisions. If you want better security, use 128-bit encryption.
class IdCrypt
def initialize()
@last_id = 0
@encoded_id = 0
end
# the encode and decode algorithms come from Michael Greenly's blog:
# http://blog.michaelgreenly.com/2008/01/obsificating-ids-in-urls.html.
MAXID = 2147483647 # (2**31 - 1)
PRIME = 1580030173 # number suggested on the blog
PRIME_INVERSE = 59260789 # (PRIME * PRIME_INVERSE) & MAXID == 1
# encode the given id
def encode_id( clear_id )
if clear_id
id_int = Integer( clear_id ) # ensure that the id is an integer
if @last_id != id_int
@last_id = id_int
@encoded_id = (id_int * PRIME) & MAXID
end
else
@encoded_id = clear_id
end
@encoded_id
end
# reverse the obfuscation that was applied to the given id
def self.decode_id( encoded_id )
clear_id = 0
if encoded_id && (encoded_id != "")
clear_id = (Integer(encoded_id) * PRIME_INVERSE) & MAXID
end
clear_id
end
# reverse the obfuscation that was applied to the given param symbol
def self.decode_param( symbol )
params[symbol] = IdCrypt::decode_id(symbol).to_s if params[symbol]
end
end
2. Add encoding properties to each model
Add the following to each model to provide easy access to the encoded version of the id:
# get the encoded record id
def id_encoded
@id_crypt.encode_id( self.id )
end
# override of the base class method to ensure that raw user ids are not displayed in the url
def to_param
@id_crypt.encode_id( self.id )
end
protected
def after_initialize
@id_crypt = IdCrypt.new
end
3. Add a before filter to the controller
Add the following ‘before filter’ to the model’s controller to automatically decode the id param before a controller processes it:
before_filter :decode_id
4. Create the decode_id method
Add the decode_id method to application_controller.rb so that all controllers have access to it. For example:
# decode the id url param if there is one
def decode_id
params[:id] = IdCrypt::decode_id(params[:id]).to_s if params[:id]
end
5. Ensure that model objects are used as path params
Wherever a path to the model’s controller is referenced and the model id is a param, use the model object as the parameter, not the object’s id. This causes the to_param override to be called.
For example, do this:
<%= link_to 'view user profile', user_profile_path(user) %>
<%= link_to 'view user profile', :controller => 'user', :action => profile, :id => user %>
don’t do this:
<%= link_to 'view user profile', user_profile_path(user.id) %>
That should do it. Go ahead and try it out and let me know what you think. Please feel free to suggest improvements or even a completely different approach that you prefer.