Thursday, August 16, 2012

Struts2 - Write your own Interceptor for Session checking

Interceptor Model is another great feature of Struts2.

Interceptors are similar to Servlet Filters (actually, both of them are based on intercepting filter),but more configurable and powerful (note that they need to be Thread-Safe).

SHORT STORY
Basically, every request to an Action is filtered through an Interceptor Stack, a specific list of Interceptors for that specific Action.
Any Interceptor performs some simple operations, then decides if the request can proceeds down the stack, to the next Interceptor (or to the Action, if we're on the last Interceptor), or it has to be redirected (back, or) somewhere else.
When not specified, a request passes through the DEFAULT INTERCEPTOR STACK, that covers most of the needs we may have.
But when our page has specific requirements, we can use a CUSTOM INTERCEPTOR STACK, made by a different number and kind of Interceptors (either available from Struts2 or, if the Interceptor with the logic that we want doesn't exist, created by ourselves).
As Apache Wiki says, "Each and every Interceptor is pluggable, so you can decide exactly which features an Action needs to support."
We can make all the custom stacks we want.
Here is the list of the Interceptors https://cwiki.apache.org/WW/interceptors.html#Interceptors-TheDefaultConfiguration
available on Struts2, and we can notice the Default Stack too, containing the majority of them.


SCENARIO
We want to perform a "Valid Session Checking" on our Actions, without writing any code into themselves or their configuration in Struts.xml.
The goal is NOT to handle user authentication (totally another pair of shoes), but to ensure that our Session is "certified" (think about an Web Application loading data into Session at the beginning, and expecting to find that data in the Session later, during its whole life cycle...).


WE NEED...
one "entry-point" Action (setting up a "mock" Session Validity Token), under the control of Default Interceptor Stack;
one Custom "SessionChecker" Interceptor;
one Custom Interceptor Stack (containing all the default interceptors PLUS our sessionCheckerInterceptor);
one Global Result "sessionExpired" on Struts.xml, redirecting to sessionExpired.jsp.


LET'S START
All of our Actions will extend a base Action called BaseAction:

package com.andrealigios.struts2validation.presentation.action;

    public abstract class BaseAction extends ActionSupport implements SessionAware, ServletResponseAware, ServletRequestAware{

        private static final long serialVersionUID = 1L; 
        public final static String SESSION_VALIDATION_TOKEN_KEY = "SESSION_VALIDATION_TOKEN_KEY";
        public final static boolean SESSION_VALIDATION_TOKEN_VALUE = true;

        private HttpServletRequest req;    

    
        /*  bla 
            bla 
            yada 
            yada */
        public HttpServletRequest getServletRequest() {
            return req;
        }

    }


We need an entry-point Action (let's call it SessionSetupAction), to put a token in session; we will try to read it later from our Interceptor,
to check if session is valid (if session is expired and replaced by a new one, our token won't be there);
In the example, the token is a boolean; in real world applications, obviously we will use something different (generated, unique, salted, obfuscated, crypted stuff...), to prevent attacks and, for example, avoid a user to access the same session of a previous user on the same browser if not every windows/tabs were closed between the logout of the first user and the login of the second one (and the logout procedure didn't destroy properly the session).

The SessionSetupAction must use the Default Interceptors Stack, because when we run this Action, we haven't put the token in the session yet, so we must bypass the sessionCheckerInterecptor this time.

    package com.andrealigios.struts2validation.presentation.action;    

    public class SessionSetupAction extends BaseAction {

        private static final long serialVersionUID = 1L;

         public String execute() throws Exception {
            try {
                getServletRequest().getSession().setAttribute(SESSION_VALIDATION_TOKEN_KEY, true);
            } catch (Exception e){
                // Mock exception log...
                e.printStackTrace();
            }

            return SUCCESS;
        }

    }



   
   
Let's write the first (and only, in our example) of "n" business Actions for our Web Application. Let's call it FirstRealPageAction:
   
    package com.andrealigios.struts2validation.presentation.action;    

    public class FirstRealPageAction extends BaseAction {

        private static final long serialVersionUID = 1L;

        public String execute() throws Exception {

            try {
                // Do business
                // Do business
                // Do business              

                return SUCCESS;
            } catch (Exception e){
                // Mock exception log...
                e.printStackTrace();
                return ERROR;
            }

        }

    }    




   
Now it's the turn of the JSPs:

    <!-- firstRealPage.jsp -->

    <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> 
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
        <head>
            <title>First Real Page</title>
        </head>

        <body>
            <p>
                This is one of the Web Application pages.  <br/>

                You are here, then the token is correctly loaded into the Session.
            </p>
        </body>
    </html>



    


    <!-- error.jsp              -->

    <%@ page isErrorPage="true" %>
    <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
        <head>
            <title>Error</title>
        </head>

        <body>
            <p>
                An error occurred!
            </p>
        </body>
    </html>





        

    <!-- sessionExpired.jsp       -->

    <%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    <html>
        <head>
            <title>Session Expired</title>
        </head>

        <body>
            <p>
                This is the Session Expired page.<br/>

                You are here because: <br/>                

                1) Your session was properly set up, but is expired; <br/>                

                2) Your session was NOT properly set up (for example, you tried to reach this page directly by URL). <br/>
                
                Press the button to pass through the sessionSetup Action and configure your Session properly.
            </p>

            <s:submit action="sessionSetup" value="Restart from the Entry point" />

        </body>
    </html>

    





   
At this point, we only miss the configuration and the interceptor itself. The configuration, quite long but (i hope) well-commented, is the following:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" 
"http://struts.apache.org/dtds/struts-2.0.dtd">

    <struts>        

        <package name="default-package" extends="struts-default">

            <interceptors>

                <!-- ========================================================== -->
                <!-- DECLARATION OF THE DEFAULT INTERCEPTOR STACK               -->
                <!-- ========================================================== -->                        

                <interceptor-stack name="defaultStack">
                    <interceptor-ref name="exception"/>
                    <interceptor-ref name="alias"/>
                    <interceptor-ref name="servletConfig"/>
                    <interceptor-ref name="i18n"/>
                    <interceptor-ref name="prepare"/>
                    <interceptor-ref name="chain"/>
                    <interceptor-ref name="scopedModelDriven"/>
                    <interceptor-ref name="modelDriven"/>
                    <interceptor-ref name="fileUpload"/>
                    <interceptor-ref name="checkbox"/>
                    <interceptor-ref name="multiselect"/>
                    <interceptor-ref name="staticParams"/>
                    <interceptor-ref name="actionMappingParams"/>
                    <interceptor-ref name="params">
                        <param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
                    </interceptor-ref>
                    <interceptor-ref name="conversionError"/>
                    <interceptor-ref name="validation">
                        <param name="excludeMethods">input,back,cancel,browse</param>
                    </interceptor-ref>
                    <interceptor-ref name="workflow">
                        <param name="excludeMethods">input,back,cancel,browse</param>
                    </interceptor-ref>
                    <interceptor-ref name="debugging"/>
                </interceptor-stack>

            </interceptors>



            <!-- ========================================================== -->
            <!-- SIMPLE REDIRECT TO ENTRY POINT WHEN NO PAGE SPECIFIED      -->    
            <!-- ========================================================== -->

            <action name="welcome">    
                <result type="redirect-action">
                    <param name="actionName">sessionSetup</param>
                </result>  
            </action>



            <!-- ========================================================== -->
            <!-- ENTRY POINT ACTION, UNDER DEFAULT INTERCEPTOR STACK        -->
            <!-- ========================================================== -->        

            <action name="sessionSetup" class="com.andrealigios.struts2validation.presentation.action.SessionSetupAction">    
                <result type="redirect-action">
                    <param name="actionName">firstRealPage</param>
                </result>  
            </action>
            
        </package>



        <package name="our-custom-package" extends="struts-default">

            <interceptors>

                <!-- ========================================================== -->
                <!-- DECLARATION OF OUR CUSTOM INTERCEPTOR FOR SESSION CHECKING -->
                <!-- ========================================================== -->
                <interceptor name="our-mock-session-checker-interceptor" 
                            class="com.andrealigios.struts2validation.presentation.interceptor.MockSessionCheckerInterceptor"/>
                <!-- ========================================================== -->
 
                <!-- ============================================== -->
                <!-- DECLARATION OF OUR CUSTOM INTERCEPTOR STACK,   -->
                <!-- THAT WILL USE OUR CUSTOM INTERCEPTOR           -->
                <!-- ============================================== -->            
                <interceptor-stack name="ourCustomStack">

                    <interceptor-ref name="exception"/>
                    <!-- ===================================== -->
                    <!-- WE PUT OUR INTERCEPTOR REFERENCE HERE -->
                    <!-- ===================================== -->
                    <interceptor-ref name="our-mock-session-checker-interceptor"/>
                    <!-- ===================================== -->        
                    <interceptor-ref name="alias"/>
                    <interceptor-ref name="servletConfig"/>
                    <interceptor-ref name="i18n"/>
                    <interceptor-ref name="prepare"/>
                    <interceptor-ref name="chain"/>
                    <interceptor-ref name="scopedModelDriven"/>
                    <interceptor-ref name="modelDriven"/>
                    <interceptor-ref name="fileUpload"/>
                    <interceptor-ref name="checkbox"/>
                    <interceptor-ref name="multiselect"/>
                    <interceptor-ref name="staticParams"/>
                    <interceptor-ref name="actionMappingParams"/>
                    <interceptor-ref name="params">
                        <param name="excludeParams">dojo\..*,^struts\..*,^session\..*,^request\..*,^application\..*,^servlet(Request|Response)\..*,parameters\...*</param>
                    </interceptor-ref>
                    <interceptor-ref name="conversionError"/>
                    <interceptor-ref name="validation">
                        <param name="excludeMethods">input,back,cancel,browse</param>
                    </interceptor-ref>
                    <interceptor-ref name="workflow">
                        <param name="excludeMethods">input,back,cancel,browse</param>
                    </interceptor-ref>
                    <interceptor-ref name="debugging"/>
                </interceptor-stack>            
                <!-- ============================================== -->            
 
             </interceptors>



            <!-- ===================================================== -->
            <!-- THE STACK WE WILL USE FOR THE ACTIONS OF THIS PACKAGE -->
            <!-- ===================================================== -->
            <default-interceptor-ref name="ourCustomStack" />
            <!-- ===================================================== -->


            <global-results>
                <!-- ===================================================== -->
                <!-- THE GLOBAL RESULT USED TO REDIRECT A REQUEST TO THE   -->
                <!-- "SESSION EXPIRED" PAGE FROM WITHIN OUR INTERCEPTOR    -->
                <!-- ===================================================== -->
                <result name="sessionExpired">jsp/sessionExpired.jsp</result>
                <!-- ===================================================== -->
            </global-results>



            <action name="firstRealPage" class="com.andrealigios.struts2validation.presentation.action.FirstRealPageAction">
                <result name="success">jsp/firstRealPage.jsp</param>            
                <result name="error">jsp/error.jsp</param>
            </action>

        </package>

    </struts>




Got it ? When the Actions of the package "our-custom-package" get called, the request passes into our Interceptor, that will perform the check on the token.
Note that it's possible to use different stacks into one package, instead of different packages.

Final part, the one that brought you here: our custom Interceptor.


package com.andrealigios.struts2validation.presentation.interceptor;


import javax.servlet.http.HttpServletRequest;
import org.apache.struts2.StrutsStatics;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
 

public class MockSessionCheckerInterceptor extends AbstractInterceptor implements Constants{



    private static final long serialVersionUID = 1L;    

    public String intercept(ActionInvocation invocation) throws Exception {

        final ActionContext context = invocation.getInvocationContext();

        try {                        

                HttpServletRequest request = (HttpServletRequest) context.get(StrutsStatics.HTTP_REQUEST);                                

                String sessionToken = (String) request.getSession().getAttribute(SESSION_VALIDATION_TOKEN_KEY);                

                if (sessionToken==null || !sessionToken.equals(SESSION_VALIDATION_TOKEN_VALUE))

                    return "sessionExpired";

                    /*     If the session Token is not there (or is different from what we want), 
                            uses the Global Result "sessionExpired" to redirect the request
                            to a page that will notify the user, giving him the opportunity to 
                            restart    the Session in the right way.
                    */

                 /* Else, everything is fine, let's proceed down the stack ! */

                return invocation.invoke();

        } catch (Exception e){
            e.printStackTrace();
            return ERROR; // not defined in our example...
        } 

    }





   
The coding is over.
If you set up your simple Web Application like this, and try to access directly your "firstRealPage", you should be redirected to the sessionExpired page; the same thing should happen if you stay too much in the "firstRealPage" page and try to reload it...

Despite the messy exposition, I hope you enjoyed this ;)