Tuesday, September 21, 2010

Spring Security 3 Ajaxified Login with jQuery (1.4.x)

On a recent project at work, I was tasked with porting a web app (or at least certain views of the UI) to a mobile device friendly UI. For whatever reason, the mandate was to make it "slick" on an iPhone. Since the project already uses jQuery extensively, it naturally made sense to use a framework that is compatible with jQuery, and mobile device. I chose (for a prototype) jQtouch, as jQuery mobile isn't slated for release until end of year.

Too many pros to talk about this framework it kicks ass. Unfortunately, looks like everything runs on a single (index.*) page, with separate divs which forms the panels.

My first hiccup using jQtouch was the authentication process. I'd hoped to be able to re-use ALL of our customized authentication code. We use Spring Security, along with some Roll-Your-Own UserdDetailsService. After some research, I determined that I could run multiple Authentication Processing filters on Spring Security's filter chain. Here's how I made the whole thing work using an ajaxified login form with Spring's UsernamePasswordAuthenticationFilter, and LogoutFilter.

first the application-context beans required:


 
     
     
  
   
       
      
  
     
      
       
      
     
 
 
 
 
  
   
  
  
   
    
    
   
  
     
 


Then, I added these beans to the existing Spring Security Configuration's filter chain (pay specific to the last 2 lines);

     
     
        
        
        
        
        
        
        
    

Don't forget to update the web.xml with the new filters;


    mobileAuthenticationProcessingFilter
    org.springframework.web.filter.DelegatingFilterProxy
  
  
    mobileAuthenticationProcessingFilter
    /*
  
  
  
    mobileAuthenticationInvalidationFilter
    org.springframework.web.filter.DelegatingFilterProxy
  
  
    mobileAuthenticationInvalidationFilter
    /*
  

Since I don't want to redirect after login, I need to override the default behaviour in the MobileAuthenticationProcessingFilter:

public class MobileAuthenticationProcessingFilter extends UsernamePasswordAuthenticationFilter {
 
 private static Logger log = Logger.getLogger(MobileAuthenticationProcessingFilter.class);

 @Override
 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
   Authentication authResult) throws IOException, ServletException {

  super.successfulAuthentication(request, response, authResult);
  HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response);
  responseWrapper.setContentType("text/json");
  String json = "{authenticated:true, navTag : \"" + request.getRequestURI() + "#Crew \"}";
  log.debug("Successful Authentication, writing JSON success Response: " + json );

  renderResponse(responseWrapper, json);
 }

 @Override
 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException failed) throws IOException, ServletException {
  /*
   * Hacky, but works.  What's happening is that the normal "onAuthenticationFailure" method
   * is sending a redirect, which is screwing up the ajax call to authenticate.  Inb this case, 
   * we'll let the AbstractAuthenticationFilter populate the session scope with the EXCEPTION_KEY,
   * but then gracefully STFU.
   */
  super.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler() {
   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
     AuthenticationException exception) throws IOException, ServletException {
    // do nothing
   }
  });
  
  super.unsuccessfulAuthentication(request, response, failed);
  
  HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response);
  String failureReason = request.getSession().getAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY) != null ?  
    ((Exception)request.getSession().getAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage() :
     "Invalid login attempt, check your authentication credentials.";
  responseWrapper.setContentType("text/json");
  String json = "{ authenticated: false,\"errors\": { \"reason\": \" " + failureReason + " \"} }";
  log.debug("Failed Authentication, writing JSON failure Response: " + json);
  renderResponse(responseWrapper, json);

 }

 private void renderResponse(HttpServletResponseWrapper responseWrapper, String json) throws IOException {
  Writer out = responseWrapper.getWriter();
  out.write(json);
  out.close();
 }
}

Additionally, if you noticed in the application-context bean we provided the com.denlab.web.filter.MobileLogoutSuccessHandler class as a constructor arg, which also overrides the behaviour of the default:

public class MobileLogoutSuccessHandler implements LogoutSuccessHandler {

 @Override
 public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) throws IOException, ServletException {
  
  //Do Nothing, we'll write a response later.
  Logger.getLogger(this.getClass()).debug("Logging out");
  
 }

}

Now that we've configured spring, implemented our classes, all that's left is to provide the login form, inside the index.jsp, with Spring's default params:




And a logout button:


   Logout
  

Use jQuery to intercept the form submission, and fire the ajax event:
 

When the principal authenticates, the MobileAuthenticationProcessingFilter will prevent the response redirect and instead write a json object directly to the response.

3 comments:

  1. Actually, this will get you started, but doesn't really cut it. I know I'd re-written some of this stuff so that it "actually" worked...I'll have to come back & clean this up later.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete