Rails + PHP sharing the same session
Ya know there’s times in your life you do things that feel ‘wrong’. This somehow feels that way, but one of my buddies wanted to know how it’s done, so here it is:
I’ve got an application I maintain in PHP which has been in production for a good number of years. It’s stable, works really well, however like any application I need to keep putting in enhancements, the pain curve of doing this is exponential with the time I’ve been using Rails, so over a couple of years you can imagine the pain is pretty high. Last week I thought to myself, enough is enough. I’m sick of writing PHP, I could do this so much faster in Rails. I’ve been churning away on a rewrite in the background, in Rails as and where I have time but I wanted a ‘fix it now’ kind of solution. So, why not have the same virtual host serve out both Rails & PHP, so long as the Rails app can find out if a user is currently logged in, then I can piece by piece enhance/replace the PHP app. Turns out it was pretty damned simple. Here’s a quick guide assuming
1- Both apps are sharing the same database
2- In the session you’re storing a user_id of the logged in user
3- App uses cookies
So here we go…
1. Virtual Host Config
The first thing we need to do is get Rails to run under the same vhost as the PHP scripts are run. This is easy, just adding a ProxyPass directive into the Apache vhost for the site back to your mongrel(cluster), so lets say we had a REST resource “some_resource”, you’d add in the ProxyPass line below
<VirtualHost *:80> ... DocumentRoot "/path/to/your/php/app/html" ProxyPass /some_resource http://127.0.0.1:3000/some_resource ... </VirtualHost>
Note that in production this would pass through to a balancer of course. The unintended consequence of this is that you also can share all of your assets served from your existing app without having to duplicate them in your Rails app.
2. Sessions - PHP
The next thing for this to work is we need to know for the Rails app that the PHP app has logged a user in - we need to share the sessions. There’s not too much black magic here. When you start your session in the PHP app (well at least mine!) PHP was storing in a named cookie eg “myappname” a hash value via
session_name( 'myappname' );
. This hash value is used to look up the session tmp files stored on the server, inside those session files are the serialized objects that PHP stores in $_SESSION global. So you have three options.
1. The “read from file system and unserialize the session” route
2. Store the session in memcached and have both PHP/Rails connect to memcached
3. Use the db to store sessions and logged in user_id’s, then load the user on each page load.
Note there is a page on this on the rails wiki, devoted to approaches 1 & 2. However if you’re not storing a tonne of variables in the $_SESSION then approach number 3 - the path of least resistance - will likely be just dandy for you…
In my database I created a shared_sessions table - using a rails migration (detailed in step 3 below) This stored a unique key “session_hash”, as well as other fairly understandable columns “user_id”, “updated_at”.
As we’re logging in and out from the PHP app, this will mostly maintain the sessions table. So I added a quick couple of functions Application_SetSession() and Application_ClearSession(). Application_SetSession() simply writes into the shared_sessions table the hash value from the cookie $_COOKIE['myappname'], the corresponding user_id, as well as the time of update, it looks something like this:
function Application_SetSharedSession($user_id) { global $DB; $session_hash = $DB->real_escape_string($_COOKIE['myappname']); $qry = "REPLACE INTO shared_sessions SET `session_hash` = '{$session_hash}', `user_id` = '{$user_id}', `updated_at` = NOW()"; if ( !$DB->query($qry) ) throw new DBException($qry); }
ClearSession() clears out the appropriate shared_session record. As you can well imagine ClearSession() is called when the user logs out, SetSession() when they login as well as every page load.
Your PHP app will now be setting/clearing the shared_session records as appropriate.
3. Sessions - Rails
rails myphphack cd myphphack script/generate model SharedSession script/generate model User script/generate scaffold SomeResource name:string ...
Right now you need to fill out the SharedSession model’s migration, and delete the user model migration (it’s already in the database!). In the SharedSession migration, make sure you’re setting the unique index on session_hash, otherwise REPLACE INTO won’t work.
So, first we need to load the user on the Rails side. To do this we need look up the SharedSession model for the session hash, from the cookie that was set by PHP. It’s available of course, doesn’t care who set it we have access to it.
class ApplicationController < ActionController::Base before_filter :restore_user ... protected def restore_user unless @user = User.restore_from_php(cookies[:myappname]) redirect_to '/login.php' end end ... end
On the user model, we now need a method to find the user from the session_hash:
class User < ActiveRecord::Base has_many :shared_sessions def self.restore_from_php(session_hash) find(:first, :include => :shared_sessions, :conditions => ["shared_sessions.session_hash = ?", session_hash]) end end
Easy peasy.
4. Testing time
Run the migration, fire up a mongrel instance on 3000, hit the appropriate vhost, login to your PHP app and go to /some_resource. With any luck you’ll be able to hit the /some_resource path with a logged in user.
Now then, it’s not quite that simple
The astute ones will notice “Hey what happens if I login from PHP, see /some_resource, then come back directly to /some_resource in future - I could do this at any point in the future, it won’t log me out”. Exactly, you can handle this one of two ways. Either some nasty cron job thing to clear out the SharedSessions table, or do it as part of the load_user filter in the rails app - ie before restoring the user, SharedSession.delete_all(”updated_at < DATE_ADD(NOW(), INTERVAL -15 MINUTES)") or whatever matches your current PHP session timeout settings.
You also probably want to do some hash of the user agent, ip address etc and store this away in the shared_session table, then check against it whenever you're finding if a user exists, to try to eliminate cookie theft.
The fine print
It’s pretty simple and it works in testing… YMMV. Test the hell out of it in your own application.

No Comments, Comment or Ping
Reply to “Rails + PHP sharing the same session”