In the past, I've dealt with creating dynamic session timeouts in my ColdFusion applications. I've typically done this to minimize the memory footprint created by site traffic spikes caused by bots (such as the GoogleBot) that spider the web site. Sometimes, however, I see that people want to delay session persistence in their ColdFusion applications for standard users as well as bots - the caveat being that the user should have full session functionality after they've logged into the given web site. Since I've never used this specific approach before, I figured I would give it a shot.
A user's session duration is determined by the SessionTimeout property established in the Application.cfc ColdFusion framework component. This timeout is defined on a per-page-request basis. This means that a user can have one session timeout on one page request and then a completely different session timeout on a subsequent page request. This flexibility is part of what makes the ColdFusion framework so incredibly powerful and happens to be the feature we'll leverage to create delayed persistence sessions.
At the time the session timeout is defined - in the pseudo construct of the Application.cfc component - we don't have access to the user's session. This is because the current page request is not yet associated with ColdFusion's application memory space. As such, we cannot rely on session-based information. What we can rely on, however, is cookie information. At the time the Application.cfc pseudo constructor runs, ColdFusion has already parsed the HTTP request and initialized the cookie scope. As such, we will use a cookie-based flag to determine the timeout of the user's session based on the current page request.
Typically, when dealing with dynamic session timeouts, I like to give every user a session, even if it's one that times out quickly. This allows the rest of the page request to operate under the assumption that a session exists. While this is not necessary, I have found that it makes site-wide logic easier to program. That's why, in the following Application.cfc, you'll see that I'm not turning on and off session management, but rather adjusting session timeouts.
<cfcomponent output="false" hint="I define application settings and event handlers."> <!--- Define the application settings. ---> <cfset this.name = hash( getCurrentTemplatePath() ) /> <cfset this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 ) /> <cfset this.sessionManagement = true /> <!--- By default, all of our new sessions will be given a very short timeout. This will be true for all users, spiders, and bots. We want session to always be enabled since our page request might require it. ---> <cfset this.sessionTimeout = createTimeSpan( 0, 0, 0, 2 ) /> <!--- Session timeout is going to be dictated by a cookie that we set on the user. If the cookie key exists, then this user is not using "persisted" sessions. ---> <cfif structKeyExists( cookie, "persistSession" )> <!--- This user is now using fully-enabled sessions. Let's change the session timeout at this point to be more agreeable with standard usage. To do this, all we have to do is override the existing sessionTimeout. NOTE: We are going from 2 seconds to 5 minutes. ---> <cfset this.sessionTimeout = createTimeSpan( 0, 0, 5, 0 ) /> </cfif> <!--- Define page request settings. ---> <cfsetting requesttimeout="10" showdebugoutput="false" /> <cffunction name="onSessionStart" access="public" returntype="void" output="false" hint="I initialize the session."> <!--- Set up a hit count variable so that we can see how many page requests are recorded in this user's session. ---> <cfset session.hitCount = 0 /> <!--- If the user already has full session persistence, call the delayed session initialization. This would happen if a user with an expired session returned to the site (with a non-expired persistSession cooke). ---> <cfif structKeyExists( cookie, "persistSession" )> <!--- Fully initialize user's session. ---> <cfset this.onSessionStartDelayed() /> </cfif> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onSessionStartDelayed" access="public" returntype="void" output="false" hint="I intialize the delayed session (once the user has logged-in as is using full session capabilities)."> <!--- This is just an idea - you might want to put further session initialization information here so that it's in a centralized place. ---> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onRequestStart" access="public" returntype="boolean" output="false" hint="I initialize the page request."> <!--- Increment the session hit count. Notice that we can act here regardless of the current session timeout because every page request is guaranteed to have a session (even if it only lasts 2 seconds). ---> <cfset session.hitCount++ /> <!--- Return true so the page can process. ---> <cfreturn true /> </cffunction> </cfcomponent>
As you can see, all users start off with a session timeout of 2 seconds. Then, based on the existence of the cookie - persistSession - the session is alternately set to 5 minutes (the full length of a standard user's session timeout). This ColdFusion framework component has the standard onSessionStart() event handler that will be called every time a session starts. I have also added an additional event handler called onSessionStartDelayed(). This is where you can put any additional initialization logic that will be needed when switching the user over to full session functionality.
You'll notice that onSessionStart() event handler checks to see if the persistSession cookie exists. If it does, it turns around and explicitly executes the onSessionStartDelayed() event handler. This logic is put in place in case where a user returns to the site with an expired ColdFusion session and a non-expired persistSession cookie. In this way, we won't be depending on the "login" work flow to give user's appropriate session functionality.
To test the session persistence behavior, I've defined a basic hitCount variable that will be incremented with each page request. I then output this value in the demo index.cfm page:
<cfoutput> <h1> Delaying ColdFusion Session Persistence </h1> <p> Hit Count: #session.hitCount# ( <a href="index.cfm">refresh</a> ) </p> <p> Persist Session: #structKeyExists( cookie, "persistSession" )# </p> <p> <a href="login.cfm">Login</a> | <a href="logout.cfm">Logout</a> </p> </cfoutput>
Here, I am outputting the hitCount as well as the existence of the persistSession cookie. As I refresh this page quickly, the hitCount goes up. But, if I delay too long (beyond the ~2 second session timeout), my hitCount is reset. This will be the standard behavior of the non-logged-in user.
Now, let's take a look at the login page - login.cfm. This is where will we be logging the user into the system and altering their session functionality.
<!--- When we log the user in, the first thing we want to do is set the session persistence flag. This will convert the user's session from a short-term to a longer-term engagement. NOTE: We do NOT want this cookie to expire before the session times out. This would cause very odd behavior if the user were to come back to the site within the session timeout limit (after this cookie expired). ---> <cfcookie name="persistSession" value="true" expires="never" /> <!--- Now that we are using a full session, perform any further initialization that might be required on this session object. To do this, we will invoke the onSessionStartDelayed() method on the ColdFusion framework component, Application.cfc. NOTE: We are leveraging the Application.cfc component to centralize the session initializtion logic (in case this needs to be altered or re-distributed later on). ---> <cfset createObject( "component", "Application" ) .onSessionStartDelayed() /> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
The logic on this page is very minimal so as to focus on the task at hand. When logging the user into the system, the first thing we do is set the cookie, persistSession. If you remember from earlier, it's this cookie that determines the user's session timeout in the Application.cfc pseudo constructor. For this demo, I've set the cookie to never expire. This is not really what we want to do, but for our purposes, it was the best option. We could have created a session cookie which expires when the user closes the browser; or, we could have created a cookie with a more explicit expiration date. Since cookies and sessions are managed differently by the ColdFusion framework, however, neither of these later two approaches really fits the bill. Ideally, both our session IDs and this cookie would be created as session cookies that would simultaneously expire when the user closed their browser.
Cookie expiration details aside, once the cookie is set, we then create an instance of the Application.cfc ColdFusion framework component and invoke the onSessionStartDelayed() event handler. This will execute any additional session initialization logic that needs to be applied. The reason I perform additional initialization in this manner, rather than directly in the current page, is because I like to keep all the framework-based initialization code inside the ColdFusion framework. Plus, if this code needed to be executed from several different places, this centralization prevents duplication of business logic.
Once the cookie is set and the session fully initialized, I then forward the user back to the index.cfm page. While this was not true in older versions of ColdFusion, CFLocation calls successfully pass all of our cookie information back to the client. As such, the persistSession cookie is persisted to the user's browser and is subsequently passed back to the ColdFusion application on the next page request (where the session timeout is dynamically defined).
To log the user out of the application, the logout.cfm page simply expires all of the cookies that have been set. This will include our persistSession cookie as well as our ColdFusion session tokens.
<!--- To clear the session, all we really need to do is clear all the cookie values (since in this case, CFID and CFTOKEN are being stored as cookies). NOTE: This will also expire our "persistSession" cookie flag that is used to determine session timeout. ---> <cfloop item="name" collection="#cookie#"> <!--- Expire this cookie. ---> <cfcookie name="#name#" value="" expires="now" /> </cfloop> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
Once the persistSession cookie is expired, subsequent page requests to the ColdFusion application will lead to the original, short session timeout of 2 seconds.
I've never used this approach personally, so I am sure there are some points that I'm missing. The one area that I know I am not 100% happy with is the way in which the persistSession cookie is set; using a "never" expiration approach means that a return user with an expired session will immediately be given full session functionality. Of course, if a user has previously presented with a desire to login, this might not be such a bad thing. At the very least, I hope this leads to some good conversation.
Want to use code from this post? Check out the license.