Tuesday 19 June 2012

Spring Security

Spring Security

Spring have done a brilliant security framework for securing pages and elements of a website simply and easily.

Concept

The concept is very straightforward.  Certain resources / pages need to be secured while others don't.  The spring framework deals with all this though a filter chain.  This means that no configuration is necessary in the controllers or jsp pages.  In testing for a login failure is impossible in a Controller because the filter takes care of it before the controller is given the request by the MessageDispatcherServlet!  Remember though that because it is a servlet filter that a few extra bits are necessary!

web.xml

As noted above the spring-security framework is a filter.  Therefore the filter needs to be added to the web.xml file so that the deployment container knows about it.  There are two entries here.  The latter clearly enables spring-security.  The first entry however is really important.  Without it the scope="request" is not possible for beans because that scope is only valid with the MessageDispatcherServlet.  Therefore to allow for session scope this additional filter must be put into the web.xml page.  Unfortunately ordering is important here.  The RequestContextFilter must be before the security filter.  This is obvious really!  If the filters are dealt with in turn then the RequestContextFilter needs to make the request scope available before the security framework filter.


    <filter>
        <filter-name>requestContextFilter</filter-name>
        <filter-class>org.springframework.web.filter.RequestContextFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Maven Dependencies

The maven dependencies required for the spring-security framework are as follows


        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-ldap</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>3.1.0</version>
        </dependency>

At the time of writing the current version is 3.1.0, however, have a look at the maven public repo to find the latest version. Search the repo at http://search.maven.org or specifically for spring-security at http://search.maven.org/#browse|875588238.

spring-context.xml

This is where the behaviour configuration starts.  It is pretty easy to get the behaviour exactly as you want it.  Here is an example,

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.1.xsd    
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <sec:http pattern="/resources/**" security="none" />
    <sec:http pattern="/login" security="none" />

    <sec:http access-denied-page="/login?access=denied" use-expressions="true" >  
        <sec:intercept-url pattern="/app/**" access="hasAnyRole({'APP_USER','APP_ADMIN'})" />
        <sec:intercept-url pattern="/app/admin/**" access="hasRole('APP_ADMIN')" />
        <sec:intercept-url pattern="/**" access="denyAll" />
        <sec:form-login login-page="/login"  authentication-success-handler-ref="authenticationSuccessHandler" authentication-failure-handler-ref="authenticationFailureHandler"/>
        <sec:logout logout-success-url="/login" invalidate-session="true" />
    </sec:http>

    <sec:authentication-manager>
        <sec:authentication-provider ref="activeDirectoryAuthProvider"></sec:authentication-provider>
    </sec:authentication-manager>
   
    <bean id="authenticationSuccessHandler" class="app.login.AppAuthenticationSuccessHandler">
        <property name="alwaysUseDefaultTargetUrl" value="false" />
        <property name="defaultTargetUrl" value="/app/home" />
    </bean>
   
    <bean id="authenticationFailureHandler" class="app.login.AppAuthenticationFailureHandler">
        <property name="defaultFailureUrl" value="/login?access=denied" />
    </bean>
   
    <bean id="appGrantedAuthoritiesMapper" class="app.login.AppGrantedAuthoritiesMapper" />

    <bean id="activeDirectoryAuthProvider" class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
        <constructor-arg value="myapp.com" />
        <constructor-arg value="ldap://myapp.com:389/" />               
        <property name="authoritiesMapper" ref="appGrantedAuthoritiesMapper" />
        <property name="useAuthenticationRequestCredentials" value="true" />
    </bean>

Here is some explanation...

<sec:http pattern="/resources/**" security="none" /> - This means that anything in the /resources/ is available.  css, images, js should live here as these don't need to be secured and in fact if you secure them it means you can't style your login page properly.

<sec:http pattern="/login" security="none" /> - Obviously you can't secure your login page because otherwise someone can't log in!!

<sec:http access-denied-page="/login?access=denied" use-expressions="true" > - the login page maps to a jsp (through a controller) therefore an error message can be added if the param access=denied is there.  So the access denied page sends the user back to the login page so that they can try again, with the addition of a failure message somewhere on the screen.

<sec:intercept-url pattern="/app/**" access="hasAnyRole({'APP_USER','APP_ADMIN'})" /> - any url from /app/ downwards can only be accessed by people in particular roles.  In this case the roles are 'APP_USER' or 'APP_ADMIN'.

<sec:intercept-url pattern="/app/admin/**" access="hasRole('APP_ADMIN')" /> - only users in the APP_ADMIN role can view urls beneath /app/admin.

<sec:intercept-url pattern="/**" access="denyAll" /> - All other pages are denied!

<sec:form-login login-page="/login"  authentication-success-handler-ref="authenticationSuccessHandler" authentication-failure-handler-ref="authenticationFailureHandler"/> - The login page is at the url /login.  When a login is successful then use the authenticationSuccessHandler bean, when login has failed use the authenticationFailureHandler bean (see later for examples and reason).

<sec:logout logout-success-url="/login" invalidate-session="true" /> - when the user logs out send them back to the login page at the url /login.

<sec:authentication-manager>
    <sec:authentication-provider ref="activeDirectoryAuthProvider"></sec:authentication-provider> 
</sec:authentication-manager> - Use the activeDirectoryAuthProvider to lookup the users.  ActiveDirectory is Microsoft's implementation of LDAP (pretty much anyway!).  Obviously other types of lookup can be performed.  The spring documentation contains a very simple example of configuring the users in the spring context itself.

Success

The success and failure handlers give access to the flow when certain criteria have been met. In the case of the success you might want to load information about a user from a database or log the fact that they have accessed the system.  A very simple way of doing this is to create a class which extends SimpleUrlAuthenticationFailureHandler and overrides the onAuthenticationSuccess() method.  Both the success and failure handler rely on a response.sendRedirect() to shift the client to the landing page or error page.  However, the SimpleUrlAuthenticationFailureHandler takes a defaultTargetUrl in the spring configuration so all that is necessary is to call super.onAuthenticationSuccess() in the subclass and everything is done for you!

    <bean id="authenticationSuccessHandler" class="app.login.AppAuthenticationSuccessHandler">
        <property name="alwaysUseDefaultTargetUrl" value="false" />
        <property name="defaultTargetUrl" value="/app/home" />
    </bean>

 



 Failure & Ajax


The failure mechanism acts exactly the same way.  Normally there is no need to override the success and failure as the standard behaviour is perfectly adequate.  However, where you have ajax requests there is a specific problem.  In the situation where ajax requests may access a session which is timed out then there is a redirect by spring to the login page.  This is a problem  for the client as it is probably expecting xml or json objects to be returned but instead will get html.  The cure for this is to create a custom AuthenticationFailureHandler such as that below,

public class AppAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler
{

    /**
     * {@inheritDoc}
     *
     * Looks at the request url and if this contains the "X-Requested-With" header which is the de-facto standard for ajax libraries.
     */
    @Override
    public void onAuthenticationFailure(final HttpServletRequest request, final HttpServletResponse response,
            final AuthenticationException authException) throws ServletException, IOException
    {

        final String requestedWith = request.getHeader("X-Requested-With");

        if (requestedWith != null && requestedWith.equals("XmlHttpRequest"))
        {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
        }
        else
        {
            super.onAuthenticationFailure(request, response, authException);
        }
    }
}

This works because Ajax requests have the 'X-Requested-With' http header.  This can be examined and if it exists return a 401 (or something) to the client.  Bare in mind that authentication has already failed at this point so now you are just deciding what to do.  This method will allow Ajax clients to check for the response code as a safe way of making sure there is no timeout.



No comments:

Post a Comment