Monday, July 18, 2011

Redirect after login to requested page with Spring after CSRF protection

Bookmarks, typing URLs directly in the address bar and getting to the requested page after login are functionalities you should not break as they impact user experience.

Once you have protected your Spring website against CSRF using the Synchronizer Token Pattern you will find that the redirection to the requested page after login functionality (You request a page, the login form shows up and after that you are taken to the page you originally requested) will be broken.

Basically the user might access a bookmark or just type a URL without the security token and the redirection will use the provided (and expired) security token or no security token at all. Of course you need to hook into Spring in order to change the default functionality.

First you use "authentication-success-handler-ref" form-login property in the Spring security context:
<beans:bean id="customAuthenticationHandler" class="com.nestorurquiza.web.handler.CustomAuthenticationHandler" />
    
<form-login login-page="/login"
            authentication-success-handler-ref="customAuthenticationHandler"
            authentication-failure-url="/login?error=authorizationFailed" />

Then implement the custom handler. Code should speak for itself.
package com.nestorurquiza.web.handler;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;

import com.nestorurquiza.utils.UrlTool;
import com.nestorurquiza.web.WebConstants;

public class CustomAuthenticationHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
        // TODO Auto-generated method stub
        String ctoken = (String) request.getSession().getAttribute(WebConstants.CSRF_TOKEN);
        DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST_KEY");
        if( defaultSavedRequest != null && ctoken != null ) {
            String requestUrl = defaultSavedRequest.getRequestURL() + "?" + defaultSavedRequest.getQueryString();
            requestUrl = UrlTool.addParamToURL(requestUrl, WebConstants.CSRF_TOKEN, ctoken, true);
            getRedirectStrategy().sendRedirect(request, response, requestUrl);
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

Here is the little useful class that allows to override the ctoken parameter (or any other url parameter)
package com.nestorurquiza.utils;

public class UrlTool {
    public static String addParamToURL(String url, String param, String value,
            boolean replace) {
        if (replace == true)
            url = removeParamFromURL(url, param);
        return url + ((url.indexOf("?") == -1) ? "?" : "&") + param + "="
                + value;
    }

    public static String removeParamFromURL(String url, String param) {
        String sep = "&";
        int startIndex = url.indexOf(sep + param + "=");
        boolean firstParam = false;
        if (startIndex == -1) {
            startIndex = url.indexOf("?" + param + "=");
            if (startIndex != -1) {
                startIndex++;
                firstParam = true;
            }
        }

        if (startIndex != -1) {
            String startUrl = url.substring(0, startIndex);
            String endUrl = "";
            int endIndex = url.indexOf(sep, startIndex + 1);
            if(firstParam && endIndex != 1) {
                //remove separator from remaining url
                endUrl = url.substring(endIndex + 1);
            }
            return startUrl + endUrl;
        }

        return url;
    }

}

Wednesday, July 06, 2011

Caching with Spring ehcache and annotations

Update Oct 2012: In fact Spring supports now (JSR-107 AKA JCache although partially as the spec is not still ready as of 2012

Caching is trivial up to the moment you start wanting to cache too many entities. At that point you realize caching is actually a cross cutting concern which basically should be done the easy way, read using Inversion Of Control. But caching is also about where you cache: memory, file system?

If you are using Java then Spring in combination with ehCache can be used to provide caching while abstracting the developer from the details. To be able to achieve caching with minimum effort I recommend using ehcache spring annotations project. Note there is no need to add ehcache dependency as it is added by ehcache-spring-annotations project. Note that Spring 3.1 provides native support so probably it is a better idea to go that route.

Here are the dependencies I used for Spring 3.0.4:
<!-- ehcache -->
        <dependency>
            <groupId>com.googlecode.ehcache-spring-annotations</groupId>
            <artifactId>ehcache-spring-annotations</artifactId>
            <version>1.1.2</version>
            <type>jar</type>
        </dependency>

Create WEB-INF/ehcache.xml with the below content:
<?xml version="1.0" encoding="UTF-8"?>
  <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      <defaultCache eternal="true" maxElementsInMemory="100" overflowToDisk="false" />
      <cache name="findAllClients" maxElementsInMemory="10000" eternal="true" overflowToDisk="false" />
  </ehcache>

In application context add the below lines:
<beans ...xmlns:ehcache="http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring"...
...
xsi:schemaLocation="
...
http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.1.xsd
...
<!-- ehcache -->
    <ehcache:annotation-driven />
 
    <ehcache:config cache-manager="cacheManager">
        <ehcache:evict-expired-elements interval="60" />
    </ehcache:config>
 
    <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation"  value="/WEB-INF/ehcache.xml"/>
    </bean>
...

Look for a method that returns a collection of entities and annotate it like:
@Cacheable(cacheName = "findAllClients")
    public List<Client> findAll() {

If there is a method that inserts an entity related to the created cache (with name "findAllClients" in this case) then we annotate it so it cleans the cache after insertion:
@TriggersRemove(cacheName = "findAllClients", when = When.AFTER_METHOD_INVOCATION, removeAll = true)
    public void addClient(Client client) {

Of course there are cases where we are just consuming a collection let us say from a web service. We would like to force the cache to be cleaned even though we are never inserting an entity. Here is where you need to use a Controller that can be invoked to clean all or specific caches with a URL like:
http://localhost:8080/ehCache/remove?cacheName=findAllClients&cacheName=anotherCacheToRemove

Here is the Controller that will make this possible:
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/ehCache/*")
public class EhCacheController extends RootController {
    private static final String EHCACHE_SHOW_PATH = "/ehCache/show";
    
    @Autowired
    private CacheManager cacheManager;

    /**
     * Removes all caches if no parameter is passed
     * Removes all caches for the specified "cacheName" parameters
     * 
     * @param request
     * @param response
     * @return
     */
    
    @RequestMapping("/remove")
    public ModelAndView home(HttpServletRequest request, HttpServletResponse response) {
        ControllerContext ctx = new ControllerContext(request, response);
        init(ctx);
        
        String[] storedCacheNames = cacheManager.getCacheNames();
        String[] cacheNames = ctx.getParameterValues("cacheName");
        
        List<Cache> caches = new ArrayList<Cache> (storedCacheNames.length);
        
        for( String storedCacheName : storedCacheNames ){
            Cache storedCache = cacheManager.getCache(storedCacheName);
            if( cacheNames == null ) {
                storedCache.removeAll();
            } else {
                for( String cacheName : cacheNames ) {
                    if( cacheName.equalsIgnoreCase(storedCacheName) ) {
                        storedCache.removeAll();
                    }
                }
            }
            caches.add(storedCache);
        }
        
        ctx.setRequestAttribute("caches", caches);

        return getModelAndView(ctx, EHCACHE_SHOW_PATH);
    }
}

The show.jsp would be something like:
<%@ include file="/WEB-INF/jsp/includes.jsp" %>
<%@ include file="/WEB-INF/jsp/header.jsp" %>
<div class="global_error"><c:out value="${csrfError}" /></div>
<div><c:out value="${caches}" /></div>
<%@ include file="/WEB-INF/jsp/footer.jsp" %>

To add more caching you will need to add the cache entry in the ehcache.xml (thing that I do not like to be honest as I think the annotation should be parsed and if there is no declaration for it in XML then use just the default values or better allow customization as part of the annotation itself) and you need to annotate the method which return value will be cached. To clean that cache you can have either one or a combination of the @TriggersRemove annotation and a URL for cache cleaning like explained before.

Followers