Spring framework + Acegi Security captcha layer + JCaptcha integration

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.

autor: Petr Matulík | kategorie: Spring framework | publikováno: 2006-4-7 19:21:45 | komentáře (5)
RSS komentářů: RSS 0.91, RSS 1.0, RSS 2.0

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 errorsthrows 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 requestthrows Exception {
16     return new Object();
17   }
18 }

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

01   "" method="post">
02     
03       
04       
05         
06         
07       
08       
09         
10         
11                   
12           
Vložte text z obrázku
"captcha-image.html" />"text" name="j_captcha_response" value=""/>
"submit" value="Odeslat" />
      
13   

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 responsethrows 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.

 

你可能感兴趣的:(技术学习)