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    <filter>
02         <filter-name>Acegi Filter Chain Proxy</filter-name>             
03 <filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
04         <init-param>
05             <param-name>targetClass</param-name>
06             <param-value>org.acegisecurity.util.FilterChainProxy</param-value>
07         </init-param>
08    </filter>
09 
10   <filter>
11     <filter-name>Acegi Channel Processing Filter</filter-name>    
12 <filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
13     <init-param>
14       <param-name>targetClass</param-name>
15       <param-value>org.acegisecurity.securechannel.ChannelProcessingFilter</param-value>
16     </init-param>
17   </filter>
18   
19     <filter-mapping>
20       <filter-name>Acegi Filter Chain Proxy</filter-name>
21       <url-pattern>/*</url-pattern>
22     </filter-mapping>
23 
24 <filter-mapping>
25   <filter-name>Acegi Channel Processing Filter</filter-name>
26   <url-pattern>/*</url-pattern>
27 </filter-mapping>

Then add following beans to your Spring xml context file:

01 <bean id="channelProcessingFilter" class="org.acegisecurity.securechannel.ChannelProcessingFilter">
02   <property name="channelDecisionManager"><ref local="channelDecisionManager"/></property>
03   <property name="filterInvocationDefinitionSource">
04   <value>
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   </value>
12   </property>
13 </bean>
14 
15 <bean id="channelDecisionManager" class="org.acegisecurity.securechannel.ChannelDecisionManagerImpl">
16   <property name="channelProcessors">
17   <list>
18   <ref local="testOnceAfterMaxRequestsCaptchaChannelProcessor"/>
19   <ref local="alwaysTestAfterTimeInMillisCaptchaChannelProcessor"/>
20   <ref local="alwaysTestAfterMaxRequestsCaptchaChannelProcessor"/>
21   <ref local="alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor"/>
22   </list>
23   </property>
24 </bean>
25 
26 <bean id="testOnceAfterMaxRequestsCaptchaChannelProcessor" class="org.acegisecurity.captcha.TestOnceAfterMaxRequestsCaptchaChannelProcessor">
27   <property name="thresold">
28   <value>4</value>
29   </property>
30   <property name="entryPoint">
31   <ref bean="captchaEntryPoint" />
32   </property>
33 </bean>
34 
35 <bean id="alwaysTestAfterTimeInMillisCaptchaChannelProcessor" 
36 class="org.acegisecurity.captcha.AlwaysTestAfterTimeInMillisCaptchaChannelProcessor">
37   <property name="thresold">
38   <value>5000</value>
39   </property>
40   <property name="entryPoint">
41   <ref bean="captchaEntryPoint" />
42   </property>
43 </bean>
44 
45 <bean id="alwaysTestAfterMaxRequestsCaptchaChannelProcessor" 
46 class="org.acegisecurity.captcha.AlwaysTestAfterMaxRequestsCaptchaChannelProcessor">
47   <property name="thresold">
48   <value>5</value>
49   </property>
50   <property name="entryPoint">
51   <ref bean="captchaEntryPoint" />
52   </property>
53 </bean>
54 
55 <bean id="alwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor" 
56 class="org.acegisecurity.captcha.AlwaysTestBelowAverageTimeInMillisBetweenRequestsChannelProcessor">
57   <property name="thresold">
58   <value>20000</value>
59   </property>
60   <property name="entryPoint">
61   <ref bean="captchaEntryPoint" />
62   </property>
63 </bean>
64 
65 <bean id="captchaEntryPoint" class="org.acegisecurity.captcha.CaptchaEntryPoint">
66   <property name="captchaFormUrl">
67     <value>/captcha.html</value>
68   </property>
69 </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_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 <bean id="captchaValidationProcessingFilter" class="org.acegisecurity.captcha.CaptchaValidationProcessingFilter">
02   <property name="captchaService">
03     <ref bean="captchaService" />
04   </property>
05   <property name="captchaValidationParameter">
06     <value>j_captcha_response</value>
07   </property>
08 </bean>
09     
10 <bean id="captchaService" class="my.package.JCaptchaServiceProxyImpl" >
11   <property name="jcaptchaService" ref="jcaptchaService" />
12 </bean>

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

01 <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
02       <property name="filterInvocationDefinitionSource">
03          <value>
04      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
05     PATTERN_TYPE_APACHE_ANT        
06 
07 /**=httpSessionContextIntegrationFilter,captchaValidationProcessingFilter,channelProcessingFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
08          </value>
09       </property>
10     </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:

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    <bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
2       <property name="context"><value>my.package.FixedCaptchaSecurityContextImpl</value></property>
3    </bean>

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 <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">   
02         <property name="mappings">
03             <props> 
04                <prop key="/captcha-image.html">captchaImageCreateController</prop>     
05                <prop key="/captcha.html">captchaFormController</prop>                   
06             </props>
07         </property> 
08 </bean> 
09 
10 <bean id="jstlViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
11           <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
12           <property name="prefix" value="/WEB-INF/jsp/"/>     
13           <property name="suffix" value=".jsp"/>
14 </bean>  
15 
16 <bean id="captchaImageCreateController" 
17         class="cz.morosystems.sportportal.controllers.CaptchaImageCreateController" >
18         <property name="jcaptchaService" ref="jcaptchaService"/>
19 </bean>
20 
21     <bean id="captchaFormController" 
22         class="cz.morosystems.sportportal.controllers.CaptchaFormController" >
23         <property name="formView" value="captcha"/>
24         <property name="sessionForm" value="false"/>
25     </bean>
26 
27 <!-- jcaptchaService is injected into captchaImageCreateController as well as to captchaService beans -->
28 <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:

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   <form action="" method="post">
02     <table>
03       <tr><td></td><td>Vložte text z obrázku</td></tr>
04       <tr>
05         <td><img src="captcha-image.html" /></td>
06         <td><input type="text" name="j_captcha_response" value=""/></td>
07       </tr>
08       <tr>
09         <td></td>
10         <td><input type="submit" value="Odeslat" /></td>
11                   </tr>
12           </table>      
13   </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:

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.

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值