Simple Synchronizer Token with Spring MVC

Friday, 13 March 2009

Simple Synchronizer Token with Spring MVC

The Problem

If you have used Struts 1.x, you'll probably be familiar with using the synchronizer token functionality provided through the saveToken() and isTokenValid() methods of the Action class. These prevent duplicate form submission, either as a result of double-clicking a submit button, or trying to submit a form from the browser history after using the back button.

Out of the box, Spring MVC doesn't have similar functionality, although it is being addressed with Spring Web Flow:

How to prevent user from double clicking the submit in a form using spring MVCAbstractTokenTransactionSynchronizer

So how could you use the synchronizer token pattern with Spring MVC if you aren't using Spring Web Flow?

Synchronizer Token

The basic idea of the synchronizer token pattern is that you keep a value in session scope that marks a point in the flow of the web application. As each form is rendered, it includes the value of the token from that point in time. On submission, the value that was embedded in the form is included in the request. The application can then compare the "historical" request token against the current session token. If the two are the same, processing continues and the session token is given a new value, effectively making the form out of date. If the two are different, it means that the form's token is lagging behind the current session token, i.e. the form has already been submitted.

So, in implementing the synchronizer token pattern, there are three components to deal with. Firstly, providing and managing the session token itself; secondly, having forms embed and submit the historical value of the token; thirdly, providing a mechanism for the application to check the request and session tokens and act accordingly.

1. Managing the Session Token

The way I choose to do this with Spring MVC is to borrow some ideas from the way that Struts 2 addresses the problem. I start off with a session listener to set up the token in session scope:

TokenListener.java
package mypackage;

import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class TokenListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent sessionEvent) {
        HttpSession session = sessionEvent.getSession();
        session.setAttribute(
            TokenFormController.getTokenKey()
        ,   TokenFormController.nextToken()
        );
    }

}

This listener has to be declared in web.xml of course:

web.xml
...

    mypackage.TokenListener

...

We'll see the code for the TokenFormController in a moment. For the time being, note that the session listener is fired whenever the container creates a session and it initialises the synchronizer token with a generated value.

2. Embedding the Historical Token in the Form

When we want to protect a form from duplicate submission, we need to capture the value of the token, embed it in the form as it is rendered and have the historical value submitted along with the other form data. The obvious way to do this is with a hidden field in the form.

Rather than worry about exactly how to do this in each form, I use a simple tag:

/WEB-INF/tags/token.tag
<%@tag description="Synchronizer Token" import="mypackage.TokenFormController" %>


This is easily included in the form JSP:

myInputForm.jsp
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags/" %>
...

    ...
    
    ...

This example is written as if the Spring MVC dispatcher servlet is mapped to *.action. Obviously it doesn't matter and this is not a convention commonly used for Spring MVC but it keeps things clear for this example.

3. The TokenFormController

I find the simplest and most transparent way of getting the token functionality into form controllers is to use a custom controller derived from the SimpleFormController hierarchy. This subclass provides token checking and routing to an "invalid token" view. In my example the controller also handles the generation of the next token value and defines the name of the token attribute. We've seen this in action in steps 1 & 2. You may prefer to factor this out into a separate class.

As I tend to use CancellableFormController for most of my input forms, I've created my TokenFormController as a subclass of that. Here is the complete code:

TokenFormController.java
package mypackage;

import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.CancellableFormController;

public class TokenFormController extends CancellableFormController {

    private static final String TOKEN_KEY = "_synchronizerToken";
    private String invalidTokenView;

    @Override
    protected ModelAndView onSubmit(
        HttpServletRequest request
    ,   HttpServletResponse response
    ,   Object command
    ,   BindException errors
    ) throws Exception {
        if (isTokenValid(request)) {
            return super.onSubmit(request, response, command, errors);
        }
        return new ModelAndView(invalidTokenView);
    }

    private synchronized boolean isTokenValid(HttpServletRequest request) {
        HttpSession session = request.getSession();
        String sessionToken = (String)session.getAttribute(getTokenKey());
        String requestToken = request.getParameter(getTokenKey());
        if (requestToken == null) {
            // The hidden field wasn't provided
            throw new TokenException("Missing synchronizer token in request");
        }
        if (sessionToken == null) {
            // The session has lost the token.
            throw new TokenException("Missing synchronizer token in session");
        }
        if (sessionToken.equals(requestToken)) {
            // Accept the submission and increment the token so this form can't
            // be submitted again ...
            session.setAttribute(getTokenKey(), nextToken());
            return true;
        }
        return false;
    }

    public static String nextToken() {
        long seed = System.currentTimeMillis(); 
        Random r = new Random();
        r.setSeed(seed);
        return Long.toString(seed) + Long.toString(Math.abs(r.nextLong()));
    }

    public String getInvalidTokenView() {
        return invalidTokenView;
    }

    public void setInvalidTokenView(String invalidTokenView) {
        this.invalidTokenView = invalidTokenView;
    }

    public static String getTokenKey() {
        return TOKEN_KEY;
    }

}

It's all fairly straightforward if you are familar with the way the SimpleFormController hierarchy fits together. The onSubmit() method ties into the standard controller flow - you just override the usual doSubmitAction()formBackingObject(), and associated methods in your subclass to provide the controller functionality for your input form. You can pretty much forget about token processing. You'll need the TokenException unchecked exception class but this is just a trivial subclass ofRuntimeException.

The isTokenValid() method deals with the token checking. The nextToken() and getTokenKey() methods provide the next value and the name of the token respectively. Refer back to the session listener and tag to see how they make use of these.

The string attribute invalidTokenView, which is returned as the view name of the returned ModelAndView if the request and session tokens don't match, is injected using the dispatcher servlet xml file. Just the same as the way the cancelViewproperty works with the standard CancellableFormController.

A typical configuration in the dispatcher servlet xml file would look something like this:

dispatcher-servlet.xml
    
        
            
                myActionController
                ...
            
        
    
    ...
    
        
        ...
        
        
        
        
        
        
        
        
    

For clarity, I've omitted other properties I usually have in here, e.g. a service facade bean that provides access to the Model, and validator references. MyActionController is the subclass of TokenFormController that manages the input form.

That's about it. The only subtlety here, if you want the same back-button behaviour as you might expect from Struts 1.x, is the useCacheControlHeader. Setting this to false (the default is true from AbstractFormController), prevents the browser from getting a fresh copy of the input form as it works back through history.

You'll probably want to do some tweaking if you want to use this in your applications, but hopefully that's enough to give you some ideas about how to implement the synchronizer token pattern with Spring MVC. I've found it quite a productive method.

9 comments:

Anonymous said...

Hi,

Nice job. However, I think that you will have to synchronize the isTokenValid method in order to prevent two submits to enter on the same time in the method and both to be considered valid.

san-ho-zay said...

Thank you for your comment. It's good to know that someone is reading!

You make a good point and I have amended the code to reflect it.

Anonymous said...

Does this setup work for only one specific form, or can you apply this for multiple forms? If so, would you just enter multiple jsp's and action classes where applicable?

san-ho-zay said...

Once you've created the TokenFormController and token tag file, you can re-use them as often as you want.

You'd typically have multiple controllers extending TokenFormController. Obviously each one would be configured slightly differently in dispatcher-servlet.xml. Then, once it's all tied together through the XML file, all you need to remember to do is use the token tag in each JSP that generates a submit.

Chris B said...

Seems good if you don't want to jump to Web Flow. A couple of concerns though:

1) The first click will get processed but the user won't see the output; instead they'll see some error page (and may assume the transaction has failed and repeat it manually).

2) We have '@Controller' annotated POJO classes, rather than subclassing any Spring MVC controller class. Not quite sure how best to use this approach.

san-ho-zay said...

The first point is very valid and has always been a potential problem with this approach in Struts. Obviously the user can't resumbit the filled in form again and I guess it's down to the application applying case-specific validation on any further transactions.

Ever used one of those self-service checkouts at the supermarket? They use a timeout so that you don't double-submit a tin of beans as you wave it around under the laser. It still has to allow you to put another tin of beans through afterwards though.

With your annotations, could you use your own base class containing the token processing?

Anonymous said...

'I think that you will have to synchronize the isTokenValid method in order to prevent two submits to enter'

Controllers are supposed to be thread-safe are they not? Otherwise why not synchro on protected ModelAndView onSubmit also?

san-ho-zay said...

Controllers are called from the dispatcher servlet and, as with any servlet, run in the scope of individual threads on a single instance of the servlet. Potential exists for synchronization issues where multiple threads are updating a shared resource. 

In this case, the token, stored in session scope, is a shared resource for multiple threads in the same browser session. You could argue that protecting the isTokenValid() method is ultra-defensive programming, on the basis that you would have to work quite hard to get two threads in the same browser session submitting protected forms at exactly the same time. Nevertheless, the possibility exists and, as synchronization is unlikely to incur much performance overhead in terms of threads waiting, it seems reasonable to adopt the ultra-defensive approach.

Anonymous said...

How does this solution take into accoutn other Ajax requests from same form ?

Newer Post Older Post Home

你可能感兴趣的:(Simple Synchronizer Token with Spring MVC)