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:
01 02 Acegi Filter Chain Proxy 03 class>org.acegisecurity.util.FilterToBeanProxyclass> 04 05 targetClass 06 org.acegisecurity.util.FilterChainProxy 07 08 09 10 11 Acegi Channel Processing Filter 12 class>org.acegisecurity.util.FilterToBeanProxyclass> 13 14 targetClass 15 org.acegisecurity.securechannel.ChannelProcessingFilter 16 17 18 19 20 Acegi Filter Chain Proxy 21 /* 22 23 24 25 Acegi Channel Processing Filter 26 /* 27 |
Then add following beans to your Spring xml context file:
01 "channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter"> 02 "channelDecisionManager">["channelDecisionManager"/>] 03 "filterInvocationDefinitionSource"> 04 05 CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 06 PATTERN_TYPE_APACHE_ANT 07 /registration.html=REQUIRES_CAPTCHA_ONCE_ABOVE_THRESOLD_REQUESTS 08 /whatever/**/*.html=REQUIRES_CAPTCHA_BELOW_AVERAGE_TIME_IN_MILLIS_REQUESTS 09 /anything/**/*.html=REQUIRES_CAPTCHA_AFTER_THRESOLD_IN_MILLIS 10 /something/else/**/*.html=REQUIRES_CAPTCHA_ABOVE_THRESOLD_REQUESTS 11 12 13 14 15 "channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl"> 16 "channelProcessors"> 17
18 ["testOnceAfterMaxRequestsCaptchaChannelProcessor"/>] 19 ["alwaysTestAfterTimeInMillisCaptchaChannelProcessor"/>] 20 ["alwaysTestAfterMaxRequestsCaptchaChannelProcessor"/>] 21 ["alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor"/>] 22 23 24 25 26 "testOnceAfterMaxRequestsCaptchaChannelProcessor" class="org.acegisecurity.captcha.TestOnceAfterMaxRequestsCaptchaChannelProcessor"> 27 "thresold"> 28 4 29 30 "entryPoint"> 31 ["captchaEntryPoint" />] 32 33 34 35 "alwaysTestAfterTimeInMillisCaptchaChannelProcessor" 36 class="org.acegisecurity.captcha.AlwaysTestAfterTimeInMillisCaptchaChannelProcessor"> 37 "thresold"> 38 5000 39 40 "entryPoint"> 41 ["captchaEntryPoint" />] 42 43 44 45 "alwaysTestAfterMaxRequestsCaptchaChannelProcessor" 46 class="org.acegisecurity.captcha.AlwaysTestAfterMaxRequestsCaptchaChannelProcessor"> 47 "thresold"> 48 5 49 50 "entryPoint"> 51 ["captchaEntryPoint" />] 52 53 54 55 "alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor" 56 class="org.acegisecurity.captcha.AlwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor"> 57 "thresold"> 58 20000 59 60 "entryPoint"> 61 ["captchaEntryPoint" />] 62 63 64 65 "captchaEntryPoint" class="org.acegisecurity.captcha.CaptchaEntryPoint"> 66 "captchaFormUrl"> 67 /captcha.html 68 69 |
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_REQUESTS ... AlwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor (contrary to api doc)
- REQUIRES_CAPTCHA_AFTER_THRESOLD_IN_MILLIS ... AlwaysTestAfterTimeInMillisCaptchaChannelProcessor
- REQUIRES_CAPTCHA_ABOVE_THRESOLD_REQUESTS ... AlwaysTestAfterMaxRequestsCaptchaChannelProcessor
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.
01 "captchaValidationProcessingFilter" class="org.acegisecurity.captcha.CaptchaValidationProcessingFilter"> 02 "captchaService"> 03 ["captchaService" />] 04 05 "captchaValidationParameter"> 06 j_captcha_response 07 08 09 10 "captchaService" class="my.package.JCaptchaServiceProxyImpl" > 11 "jcaptchaService" ref="jcaptchaService" /> 12 |
Next you need to add newly defined filters to your Acegi filter chain like this:
01 "filterChainProxy" class="org.acegisecurity.util.FilterChainProxy"> 02 "filterInvocationDefinitionSource"> 03 04 CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 05 PATTERN_TYPE_APACHE_ANT 06 07 /**=httpSessionContextIntegrationFilter,captchaValidationProcessingFilter,channelProcessingFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor 08 09 10
|
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:
01 public class FixedCaptchaSecurityContextImpl extends CaptchaSecurityContextImpl { 02 03 public int hashCode() { 04 05 if (getAuthentication() == null) { 06 return (int)System.currentTimeMillis(); 07 } else { 08 return this.getAuthentication().hashCode(); 09 } 10 } 11 } |
Therefore the httpSessionIntegrationFilter bean looks as follows:
1 "httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"> 2 "context">my.package.FixedCaptchaSecurityContextImpl 3 |
Now we should look at actual integration with JCaptcha. That is where previously mentioned my.package.JCaptchaServiceProxyImpl comes to light. It is also very simple:
01 public class JCaptchaServiceProxyImpl implements CaptchaServiceProxy { 02 03 private ImageCaptchaService jcaptchaService; 04 05 public boolean validateReponseForId(String id, Object response) { 06 07 try { 08 return jcaptchaService.validateResponseForID(id, response); 09 10 } catch (CaptchaServiceException cse) { 11 //fixes known bug in JCaptcha 12 return false; 13 } 14 } 15 16 public void setJcaptchaService(ImageCaptchaService jcaptchaService) { 17 this.jcaptchaService = jcaptchaService; 18 } 19 } |
In captchaEntryPoint bean we have specified /captcha.html as entry point url. Hence our servlet-context.xml could contain following beans:
01 "urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> 02 "mappings"> 03 04 "/captcha-image.html">captchaImageCreateController 05 "/captcha.html">captchaFormController 06 07 08 09 10 "jstlViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 11 "viewClass" value="org.springframework.web.servlet.view.JstlView"/> 12 "prefix" value="/WEB-INF/jsp/"/> 13 "suffix" value=".jsp"/> 14 15 16 "captchaImageCreateController" 17 class="cz.morosystems.sportportal.controllers.CaptchaImageCreateController" > 18 "jcaptchaService" ref="jcaptchaService"/> 19 20 21 "captchaFormController" 22 class="cz.morosystems.sportportal.controllers.CaptchaFormController" > 23 "formView" value="captcha"/> 24 "sessionForm" value="false"/> 25 26 27 28 "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:
01 public class CaptchaFormController extends SimpleFormController { 02 03 protected ModelAndView onSubmit(HttpServletRequest request, 04 HttpServletResponse response, Object command, BindException errors) throws Exception { 05 06 String originalRequestMethod = request.getParameter("original_request_method"); 07 String originalRequestUrl = request.getParameter("original_requestUrl"); 08 String originalRequestParameters = request.getParameter("original_request_parameters"); 09 10 String redirectUrl = originalRequestUrl; 11 12 return new ModelAndView("redirect:" + redirectUrl); 13 } 14 15 protected Object formBackingObject(HttpServletRequest request) throws Exception { 16 return new Object(); 17 } 18 } |
The formView of this form controller is captcha.jsp which contains following code:
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:
01 public class CaptchaImageCreateController implements Controller, InitializingBean { 02 03 private ImageCaptchaService jcaptchaService; 04 05 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { 06 byte[] captchaChallengeAsJpeg = null; 07 08 ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream(); 09 10 // get the session id that will identify the generated captcha. 11 //the same id must be used to validate the response, the session id is a good candidate! 12 String captchaId = request.getSession().getId(); 13 14 BufferedImage challenge = 15 jcaptchaService.getImageChallengeForID(captchaId,request.getLocale()); 16 17 JPEGImageEncoder jpegEncoder = 18 JPEGCodec.createJPEGEncoder(jpegOutputStream); 19 jpegEncoder.encode(challenge); 20 21 captchaChallengeAsJpeg = jpegOutputStream.toByteArray(); 22 23 // flush it in the response 24 response.setHeader("Cache-Control", "no-store"); 25 response.setHeader("Pragma", "no-cache"); 26 response.setDateHeader("Expires", 0); 27 response.setContentType("image/jpeg"); 28 ServletOutputStream responseOutputStream = 29 response.getOutputStream(); 30 responseOutputStream.write(captchaChallengeAsJpeg); 31 responseOutputStream.flush(); 32 responseOutputStream.close(); 33 return null; 34 } 35 36 public void setJcaptchaService(ImageCaptchaService jcaptchaService) { 37 this.jcaptchaService = jcaptchaService; 38 } 39 40 public void afterPropertiesSet() throws Exception { 41 if(jcaptchaService == null){ 42 throw new RuntimeException("Image captcha service wasn`t set!"); 43 } 44 } 45 } |
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.