středa 23. listopadu 2005

Simple protection for web forms with Jcaptcha and Spring MVC

Time to time we need web forms protection before robot submision for example spammers, DOS attack and so on. The saftest and commonly used way how distinguish between robot and human is CAPTCHA. CAPTCHA is acronym for Completely Automated Public Test to tell Computers and Humans Apart.

In context web form is CAPTCHA presented as pair which contain dynamicly generated picture (image challenge) and input box. Befeore each submit of form user has to retype text from picture to input box. This text is validated on server side and if is correct form submission continue otherwise original form is returned.

In Java world exists open source CAPTCHA solution be called Jcaptcha. Jcaptcha will be very easly integrated to any web application and has a lot of prearranged modules e.g. for Struts or Servlet filter. In this article we show how to integrated Jcaptcha with Spring MVC.

What we need?

At first, dynamicly generate CAPTCHA image and second, control user retyped text during form submission. We can use Jcaptcha Base module for this. Base module aims to provide base components to build a specialized module.

The heart of JCaptcha is CaptchaService or more precisely ImageCaptchaService and their specialized implementation. This interface defines two main method getImageChallengeForID and validateResponseForID.

  • getImageChallengeForID(String ID) - method to retrive the image challenge corresponding to the given ticket.
  • validateResponseForID(String ID, Object response) - method to validate a response to the image challenge corresponding to the given ticket.

We get session id as ticket (ID) that will identify the generated captcha. Session id is unique for client and automaticly is sending by browser with each request. We needn`t stress with generating unique number a handling new parameter.

Image genarating will be wrapped to controller from Spring MVC point of view.

public class JCaptchaController implements Controller, InitializingBean{
  private ImageCaptchaService captchaService;

  /**
   @see org.springframework.web.servlet.mvc.Controller#handleRequest(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
   */
  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse responsethrows Exception {
        byte[] captchaChallengeAsJpeg = null;
        // the output stream to render the captcha image as jpeg into
        ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
        
        // get the session id that will identify the generated captcha. 
        //the same id must be used to validate the response, the session id is a good candidate!
        String captchaId = request.getSession().getId();
        
        // call the ImageCaptchaService getChallenge method
        BufferedImage challenge =
                        captchaService.getImageChallengeForID(captchaId,request.getLocale());
        
        // a jpeg encoder
        JPEGImageEncoder jpegEncoder =
                        JPEGCodec.createJPEGEncoder(jpegOutputStream);
        jpegEncoder.encode(challenge);
        

        captchaChallengeAsJpeg = jpegOutputStream.toByteArray();

        // flush it in the response
        response.setHeader("Cache-Control""no-store");
        response.setHeader("Pragma""no-cache");
        response.setDateHeader("Expires"0);
        response.setContentType("image/jpeg");
        ServletOutputStream responseOutputStream =
        response.getOutputStream();
        responseOutputStream.write(captchaChallengeAsJpeg);
        responseOutputStream.flush();
        responseOutputStream.close();
        return null;
  }

  /**
   * Set captcha service
   @param captchaService The captchaService to set.
   */
  public void setCaptchaService(ImageCaptchaService captchaService) {
        this.captchaService = captchaService;        
  }  
  
  /**
   @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
   */
  public void afterPropertiesSet() throws Exception {
        if(captchaService == null){
          throw new RuntimeException("Image captcha service wasn`t set!");
        }
  }
}
Java2html

As you can see, this is classic controller with direct writing to response. It is possible to set any type of ImageCaptchaService implementation through setCaptchaService method. Now we need integrate captcha validation. The good choice is extends SimpleFormController from Spring and override onBindAndValidate method.

This method delegates call to new method validateCaptcha. validateCaptcha is responsible for validation. If is captcha response invalid, new ObjectError is added to validation errors holder and form submission is cancelled.

public class ProtectedFormController extends SimpleFormController {
  /**
   * Default paramter name for CAPTCHA response in <code>{@link HttpServletRequest}</code>
   */
  private static final String DEFAULT_CAPTCHA_RESPONSE_PARAMETER_NAME = "j_captcha_response";
  
  protected ImageCaptchaService captchaService;
  protected String captchaResponseParameterName = DEFAULT_CAPTCHA_RESPONSE_PARAMETER_NAME;
        
  /**
   * Delegates request to CAPTCHA validation, subclasses which overrides this 
   * method must manually call <code>{@link #validateCaptcha(HttpServletRequest, BindException)}</code>
   * or explicitly call super method.
   
   @see #validateCaptcha(HttpServletRequest, BindException)
   @see org.springframework.web.servlet.mvc.BaseCommandController#onBindAndValidate(javax.servlet.http.HttpServletRequest, java.lang.Object, org.springframework.validation.BindException)
   */
  @Override
  protected void onBindAndValidate(HttpServletRequest request, Object command, BindException errorsthrows Exception {        
        validateCaptcha(request, errors);
  }
  
  /**
   * Validate CAPTCHA response, if response isn`t valid creates new error object 
   * and put him to errors holder.
   
   @param request current servlet request
   @param errors errors holder
   */
  protected void validateCaptcha(HttpServletRequest request, BindException errors){
        boolean isResponseCorrect = false;
        
        //remenber that we need an id to validate!
        String captchaId = request.getSession().getId();
        //retrieve the response
        String response = request.getParameter(captchaResponseParameterName);
        //validate response
        try {          
          if(response != null){
                isResponseCorrect =
                  captchaService.validateResponseForID(captchaId, response);
          }
        catch (CaptchaServiceException e) {
                //should not happen, may be thrown if the id is not valid          
        }
        
        if(!isResponseCorrect){
          //prepare object error, captcha response isn`t valid
                  String objectName = "Captcha";
          String[] codes = {"invalid"};
          Object[] arguments = {};
          String defaultMessage = "Wrong cotrol text!";
          ObjectError oe = new ObjectError(objectName, codes, arguments, defaultMessage);
          errors.addError(oe);
        }                 
  }

  /**
   * Set captcha service
   @param captchaService the captchaService to set.
   */
  public void setCaptchaService(ImageCaptchaService captchaService) {
        this.captchaService = captchaService;
  }

  /**
   * Set paramter name for CAPTCHA response in <code>{@link HttpServletRequest}</code>
   @param captchaResponseParameterName the captchaResponseParameterName to set.
   */
  public void setCaptchaResponseParameterName(String captchaResponseParameterName) {
        this.captchaResponseParameterName = captchaResponseParameterName;
  }
}
Java2html

Now we are ready to use Spring and Jcaptcha. I choose sending comment for usage demonstration. Comment is represent as JavaBean and form controller is subclass of ProtectedFormController that has captcha validation.

public class Comment {
  private String email;
  private String subject;
  private String body;
  
  //Getters and Setters
}
Java2html

public class NewCommentForm extends ProtectedFormController {
  
  /**
   @see org.springframework.web.servlet.mvc.SimpleFormController#doSubmitAction(java.lang.Object)
   */
   @Override
    protected void doSubmitAction(Object commandthrows Exception {
        Comment comment = (Commentcommand;
        //do something with new comment for example save to database.
    }   
}
Java2html

Both controller and other stuff are configured in *-servlet.xml.

 
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> 
 <bean id="formController" class="cz.sweb.pichlik.springtutorial.captcha.NewCommentForm">     
     <property name="captchaService"><ref bean="captchaService"/></property>
     <property name="commandClass"><value>cz.sweb.pichlik.springtutorial.captcha.Comment</value></property>
     <property name="formView"><value>form</value></property>
  <property name="successView"><value>submit</value></property>
  <property name="commandName"><value>comment</value></property>
    </bean>   
    
    <!-- This controller generates CAPTCHA image -->
    <bean id="captchaController" class="cz.sweb.pichlik.springtutorial.captcha.JCaptchaController">
     <property name="captchaService"><ref bean="captchaService"/></property>
    </bean> 
       
   <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">    
  <property name="mappings">
   <props>    
    <prop key="/newcomment.htm">formController</prop>
    <prop key="/captcha.htm">captchaController</prop>  
   </props>
  </property>
 </bean>
  
 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
        <property name="prefix"><value>/WEB-INF/jsp/</value></property>
        <property name="suffix"><value>.jsp</value></property>
    </bean>
</beans>
 

Application context configuration contains only captchaService bean. This bean is configured as singleton and as concrete captcha service implementation is selected default Jcaptcha class DefaultManageableImageCaptchaService>.


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> 
 <!-- CAPTCHA SERVICE DEFINITION -->
 <bean id="captchaService" 
  class="com.octo.captcha.service.image.DefaultManageableImageCaptchaService" 
  singleton="true"/>
</beans>

HTML code is placed in form.jsp. You can notice how is JCaptchaController called by URL in src attribute of img element. forEach loop print any errors occured during validation (including captcha invalid response text).


<%@ page session="true" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<html>
 <head>
  <title>New comment</title>
 </head>
 <body>
  <form method="post" action="newcomment.htm">
  <spring:bind path="comment">
   <ul>
    <c:forEach items="${status.errorMessages}" var="errorMessage">
     <li style="color:red"><c:out value="${errorMessage}"/></li>
    </font>   
   </c:forEach>
   </ul>
   <table> 
    <tr>
     <td><label>Email</label></td><td><input type="text" name="email" value="<c:out value="${comment.email}"/>"/></td>
    </tr> 
    <tr>
     <td><label>Subject</label></td><td><input type="text" name="subject" value="<c:out value="${comment.subject}"/>"/></td>
    </tr>
    <tr>
     <td><label>Body</label></td><td><textarea name="body" cols="15" rows="5"><c:out value="${comment.body}"/></textarea></td>
    </tr>
    <tr>
     <td><label>Control text</label></td><td><input type="text" name="j_captcha_response" /></td> 
    </tr>
    <tr>
     <td colspan="2"><img src="captcha.htm" /></td>
    </tr>
    <tr>
     <td colspan="2" align="center"><input type="submit" value="submit" /></td>
    </tr>       
   </table>
   </spring:bind>
  </form>  
 </body>
</html>

New form

Valid submission

Invalid submission

Download source code (80 KB) or source code and libraries (3,2 MB)