I’ve just integrated captcha functionality to my Spring + Acegi powered web application and due to the lack of first level documentation on this topic in Acegi Security documentation or wherever I decided to create this brief step by step manual. Hope this help others.

I’m using Acegi 1.0.0 RC2, Spring framework 2.0 M3 and JCaptcha 1.0 RC2.0.1, that is cutting edge versions, but only restriction for you should be at least 0.9 Acegi Security version (has captcha support) used.

Acegi captcha support layer has been contributed to Acegi by Marc Antoine Garrigue, one of the lead developers of JCaptcha project, so it should be worth to try it.

First of all you need to set channel processing filter in your web.xml if you haven’t yet. So your acegi filter settings in web.xml should look like this:



<filter>
</filter><filter -name>Acegi Filter Chain Proxy</filter>
<filter -class>org.acegisecurity.util.FilterToBeanProxy</filter>
<init -param>
<param -name>targetClass</param>
<param -value>org.acegisecurity.util.FilterChainProxy</param>
</init>

<filter>
</filter><filter -name>Acegi Channel Processing Filter</filter>
<filter -class>org.acegisecurity.util.FilterToBeanProxy</filter>
<init -param>
<param -name>targetClass</param>
<param -value>org.acegisecurity.securechannel.ChannelProcessingFilter</param>
</init>

<filter -mapping>
</filter><filter -name>Acegi Filter Chain Proxy</filter>
<url -pattern>/*</url>

<filter -mapping>
</filter><filter -name>Acegi Channel Processing Filter</filter>
<url -pattern>/*</url>


Then add following beans to your Spring xml context file:


<bean id="channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter">
<property name="channelDecisionManager"><ref local="channelDecisionManager"/></property>
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/registration.html=REQUIRES_CAPTCHA_ONCE_ABOVE_THRESOLD_REQUESTS
/whatever/**/*.html=REQUIRES_CAPTCHA_BELOW_AVERAGE_TIME_IN_MILLIS_REQUESTS
/anything/**/*.html=REQUIRES_CAPTCHA_AFTER_THRESOLD_IN_MILLIS
/something/else/**/*.html=REQUIRES_CAPTCHA_ABOVE_THRESOLD_REQUESTS
</value>
</property>
</bean>

<bean id="channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
<property name="channelProcessors">
<list>
<ref local="testOnceAfterMaxRequestsCaptchaChannelProcessor"/>
<ref local="alwaysTestAfterTimeInMillisCaptchaChannelProcessor"/>
<ref local="alwaysTestAfterMaxRequestsCaptchaChannelProcessor"/>
<ref local="alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor"/>
</list>
</property>
</bean>

<bean id="testOnceAfterMaxRequestsCaptchaChannelProcessor" class="org.acegisecurity.captcha.TestOnceAfterMaxRequestsCaptchaChannelProcessor">
<property name="thresold">
<value>4</value>
</property>
<property name="entryPoint">
<ref bean="captchaEntryPoint" />
</property>
</bean>

<bean id="alwaysTestAfterTimeInMillisCaptchaChannelProcessor"
class="org.acegisecurity.captcha.AlwaysTestAfterTimeInMillisCaptchaChannelProcessor">
<property name="thresold">
<value>5000</value>
</property>
<property name="entryPoint">
<ref bean="captchaEntryPoint" />
</property>
</bean>

<bean id="alwaysTestAfterMaxRequestsCaptchaChannelProcessor"
class="org.acegisecurity.captcha.AlwaysTestAfterMaxRequestsCaptchaChannelProcessor">
<property name="thresold">
<value>5</value>
</property>
<property name="entryPoint">
<ref bean="captchaEntryPoint" />
</property>
</bean>

<bean id="alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor"
class="org.acegisecurity.captcha.AlwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor">
<property name="thresold">
<value>20000</value>
</property>
<property name="entryPoint">
<ref bean="captchaEntryPoint" />
</property>
</bean>

<bean id="captchaEntryPoint" class="org.acegisecurity.captcha.CaptchaEntryPoint">
<property name="captchaFormUrl">
<value>/captcha.html</value>
</property>
</bean>


In filterInvocationDefinitionSource of channelProcessingFilter bean you should specify which urls will be intercepted with verification-of-users-humanity process. With declared attribute you specify which type of four supported types of humanity check you wish to use.

Urls with REQUIRES_CAPTCHA_ONCE_ABOVE_THRESOLD_REQUESTS attribute will be processed by TestOnceAfterMaxRequestsCaptchaChannelProcessor declared in channelDecisionManager bean. If declared URL will be accessed more times than specified thresold, user will be redirected to captcha entryPoint and captcha validation process will be started. Other attributes map similarly:

  • REQUIRES_CAPTCHA_BELOW_AVERAGE_TIME_IN_MILLIS_REQUESTSAlwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor (contrary to api doc)
  • REQUIRES_CAPTCHA_AFTER_THRESOLD_IN_MILLISAlwaysTestAfterTimeInMillisCaptchaChannelProcessor
  • REQUIRES_CAPTCHA_ABOVE_THRESOLD_REQUESTSAlwaysTestAfterMaxRequestsCaptchaChannelProcessor

Meaning of these attributes is specified in api doc appropriate to channel processor implementation.

Next step is to add filter bean which intercepts all http request and if finds parameter with specified name (j_captcha_response in this case), it calls CaptchaServiceProxy implementation and validate the captcha response value against session id.


<bean id="captchaValidationProcessingFilter" class="org.acegisecurity.captcha.CaptchaValidationProcessingFilter">
<property name="captchaService">
<ref bean="captchaService" />
</property>
<property name="captchaValidationParameter">
<value>j_captcha_response</value>
</property>
</bean>

<bean id="captchaService" class="my.package.JCaptchaServiceProxyImpl" >
<property name="jcaptchaService" ref="jcaptchaService" />
</bean>

Next you need to add newly defined filters to your Acegi filter chain like this:


<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,captchaValidationProcessingFilter,channelProcessingFilter, authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

Note that httpSessionContextIntegrationFilter must be located at first place in the chain and then follow captcha filter, channel processing filter, etc.

Last thing you have to do to finalize acegi captcha support structure setup process is to switch implementation class of used SecurityContext in your httpSessionContextIntegrationFilter bean. And here we come to interesting point of setup, because there is a bug in org.acegisecurity.captcha.CaptchaSecurityContextImpl which cause that TestOnceAfterMaxRequestsCaptchaChannelProcessor and AlwaysTestAfterMaxRequestsCaptchaChannelProcessor channel processors doesn’t work. This issue should be fixed in Acegi 1.0 final.

My little bit provisory and temporary solution to this problem is to implement own CaptchaSecurityContext simple implementation which looks like this:


public class FixedCaptchaSecurityContextImpl extends CaptchaSecurityContextImpl {

public int hashCode() {

if (getAuthentication() == null) {
return (int)System.currentTimeMillis();
} else {
return this.getAuthentication().hashCode();
}
}
}

Therefore the httpSessionIntegrationFilter bean looks as follows:


public class JCaptchaServiceProxyImpl implements CaptchaServiceProxy {

private ImageCaptchaService jcaptchaService;

public boolean validateReponseForId(String id, Object response) {

try {
return jcaptchaService.validateResponseForID(id, response);

} catch (CaptchaServiceException cse) {
//fixes known bug in JCaptcha
return false;
}
}

public void setJcaptchaService(ImageCaptchaService jcaptchaService) {
this.jcaptchaService = jcaptchaService;
}
}

In captchaEntryPoint bean we have specified /captcha.html as entry point url. Hence our servlet-context.xml could contain following beans:


<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/captcha-image.html">captchaImageCreateController</prop>
<prop key="/captcha.html">captchaFormController</prop>
</props>
</property>
</bean>

<bean id="jstlViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

<bean id="captchaImageCreateController"
class="cz.morosystems.sportportal.controllers.CaptchaImageCreateController" >
<property name="jcaptchaService" ref="jcaptchaService"/>
</bean>

<bean id="captchaFormController"
class="cz.morosystems.sportportal.controllers.CaptchaFormController" >
<property name="formView" value="captcha"/>
<property name="sessionForm" value="false"/>
</bean>

<!-- jcaptchaService is injected into captchaImageCreateController as well as to captchaService beans -->
<bean id="jcaptchaService" class="com.octo.captcha.service.image.DefaultManageableImageCaptchaService" />

This spring settings is abbreviated. There should be mapping for url mentioned in channelProcessingFilter bean and appropriate controllers, but that’s not necessary for our purpose.

CaptchaFormController in its simplest form:


public class CaptchaFormController extends SimpleFormController {

protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors) throws Exception {

String originalRequestMethod = request.getParameter("original_request_method");
String originalRequestUrl = request.getParameter("original_requestUrl");
String originalRequestParameters = request.getParameter("original_request_parameters");

String redirectUrl = originalRequestUrl;

return new ModelAndView("redirect:" + redirectUrl);
}

protected Object formBackingObject(HttpServletRequest request) throws Exception {
return new Object();
}
}

The formView of this form controller is captcha.jsp which contains following code:


<form action="" method="post">
<table>
<tr><td></td><td>Vložte text z obrázku</td></tr>
<tr>
<td><img src="captcha-image.html" /></td>
<td><input type="text" name="j_captcha_response" value=""/></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="Odeslat" /></td>
</tr>
</table>
</form>

Note the name of input field (j_captcha_response) and value of src attribute of img tag. Here I was strongly inspirated by Spring MVC + JCaptcha integration solution suggested by Roman Pichlik (I stole it actually … I’m sorry Roman). As you can see in urlMapping bean, captcha image is generated thru Roman’s CaptchaImageCreateController:


public class CaptchaImageCreateController implements Controller, InitializingBean {

private ImageCaptchaService jcaptchaService;

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
byte[] captchaChallengeAsJpeg = null;

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();

BufferedImage challenge =
jcaptchaService.getImageChallengeForID(captchaId, request.getLocale());

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;
}

public void setJcaptchaService(ImageCaptchaService jcaptchaService) {
this.jcaptchaService = jcaptchaService;
}

public void afterPropertiesSet() throws Exception {
if(jcaptchaService == null){
throw new RuntimeException("Image captcha service wasn`t set!");
}
}
}

And that’s all. Now when some of humanity-required urls is requested by not authenticated user, than appropriate channel processor is activated and humanity check processed accordingly. That means user is provided with JCaptcha image and after successful response he is redirected to originally requested url.

My view inside this topic is quiet flat and some information provided here can be little bit inaccurate. On the other hand this solution works for me and I hope it will help others with similar needs. Any suggestions would be appreciated anyway.

Share on Facebook0Share on Google+0Tweet about this on TwitterShare on LinkedIn0Email this to someone