JCaptcha+Memcache的验证码集群实现

一、问题背景

      为了防止垃圾信息发布机器人的自动提交攻击,采用CAPTCHA验证码来保护该模块,提高攻击者的成本。

 

二、验证码简介

       全自动区分计算机和人类的图灵测试(Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA)俗称验证码,是一种区分用户是计算机和人的公共全自动程序。在CAPTCHA测试中,作为服务器的计算机会自动生成一个问题由用户来解答。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。

       验证码作为一种辅助安全手段在Web安全中有着特殊的地位,验证码安全和web应用中的众多漏洞相比似乎微不足道,但是千里之堤毁于蚁穴,有些时候如果能绕过验证码,则可以把手动变为自动,对于Web安全检测有很大的帮助。大部分验证码的设计者都不知道为什么要用到验证码,或者对于如何检验验证码的强度没有任何概念。大多数验证码在实现的时候只是把文字印到背景稍微复杂点的图片上就完事了,程序员没有从根本上了解验证码的设计理念。

        JCaptcha即为Java版本的CAPTCHA项目,其是一个开源项目,支持生成图形和声音版的验证码,声音版的验证码需要使用到FreeTTS。
其原理是:服务器端首先随机产生一个字符串,生成一个图片,在后台存储一份。在做验证的时候,通过在后台传进去request参数获取到后台存储的值与输入的值进行比对。基于Servlet的使用方式可以参考官网的tutorial( https://jcaptcha.atlassian.net/wiki/display/general/5+minutes+application+integration+tutorial)

 

JCaptcha的架构图如下所示:
 

三、问题分析

       JCaptcha默认的实现是基于单机模式(MapCaptchaStore存储信息单机HashMap中),为了适应集群环境可以把验证信息存储在session中,但是要求Web服务器配置session stick或者Session复制。为了实现负载均衡且避免session复制带来的性能损失,集群部署方案是完全分布式的,既不是session stick也不进行session复制。进行验证时,由A节点到B节点进行验证,B节点CaptchaStore中store中得不到当前验证码,无法进行验证。  

        由上可知,如果把验证码统一存储在一个地方,问题将迎刃而解,故考虑自定义CaptchaStore采用memcache来存储,如下图所示:
        

 

四、具体实施

   1)在pom.xml加入依赖:       

< dependency >
     < groupId >com.octo.captcha</ groupId >
     < artifactId >jcaptcha-all</ artifactId >
     < version >1.0-RC6</ version >
</ dependency >

 
     2)JCaptcha与Spring集成配置
     applicationContext.xml:

< bean  id = "imageCaptchaService"  class = "com.octo.captcha.service.image.DefaultManageableImageCaptchaService" >
     < constructor-arg  type = "com.octo.captcha.service.captchastore.CaptchaStore"  index = "0" >
         < ref  bean = "myCaptchaStore" />
     </ constructor-arg >
     <!--which captcha Engine you use-->
     < constructor-arg  type = "com.octo.captcha.engine.CaptchaEngine"  index = "1" >
         < ref  bean = "myCaptchaEngine" />
     </ constructor-arg >
 
     < constructor-arg  index = "2" >
         < value >180</ value >
     </ constructor-arg >
 
     < constructor-arg  index = "3" >
         < value >100000</ value >
     </ constructor-arg >
 
     < constructor-arg  index = "4" >
         < value >75000</ value >
     </ constructor-arg >
</ bean >
 
< bean  id = "myCaptchaStore"  class = "com.xxx.util.MyCaptchaStore" />
 
<!--you can define more than one captcha engine here -->
< bean  id = "myCaptchaEngine"  class = "com.xxx.util.MyCaptchaEngine" />

 

        3)定制CaptchaStore

/**
  * 定制CaptchaStore
  * 线上集群环境,前端可能从A服务器取得验证码,而验证是到B服务器
  * 默认的hashmap store是保存在单个Jvm内存中的,这样验证就会有问题
  *
  */
public  class  MyCaptchaStore  implements  CaptchaStore {
    //CacheService负责封装memcache访问功能
     @Resource
     private  CacheService cacheService;
  
     @Override
     public  boolean  hasCaptcha(String id) {
         CaptchaAndLocale captcha = cacheService.getCaptcha(id);
         return  captcha ==  null  false  true ;
     }
  
     @Override
     public  void  storeCaptcha(String id, Captcha captcha)  throws  CaptchaServiceException {
         try  {
             cacheService.setCaptcha(id, new  CaptchaAndLocale(captcha));
         catch  (Exception e) {
             throw  new  CaptchaServiceException(e);
         }
     }
  
     @Override
     public  void  storeCaptcha(String id, Captcha captcha, Locale locale)  throws  CaptchaServiceException {
         try  {
            c acheService.setCaptcha(id, new  CaptchaAndLocale(captcha,locale));
         catch  (Exception e) {
             throw  new  CaptchaServiceException(e);
         }
     }
  
     @Override
     public  boolean  removeCaptcha(String id) {
         return c acheService.removeCaptcha(id);
     }
  
     @Override
     public  Captcha getCaptcha(String id)  throws  CaptchaServiceException {
         CaptchaAndLocale captchaAndLocale = cacheService.getCaptcha(id);
         return  captchaAndLocale !=  null  ? (captchaAndLocale.getCaptcha()) :  null ;
     }
  
     @Override
     public  Locale getLocale(String id)  throws  CaptchaServiceException {
         CaptchaAndLocale captchaAndLocale = cacheService.getCaptcha(id);
         return  captchaAndLocale !=  null  ? (captchaAndLocale.getLocale()) :  null ;
     }
  
     @Override
     public  int  getSize() {
         return  0 ;
     }
  
     @Override
     public  Collection getKeys() {
         return  null ;
     }
  
     @Override
     public  void  empty() {
     }
  
     @Override
     public  void  initAndStart() {
     }
  
     @Override
     public  void  cleanAndShutdown() {
     }
}

 

       4)定制验证码Engine

/**
  * 验证码Engine
  *
  */
public  class  MyCaptchaEngine  extends  ListImageCaptchaEngine {
  
     protected  void  buildInitialFactories() {
         int  minWordLength =  4 ;
         int  maxWordLength =  4 ;
         int  fontSize =  20 ;
         int  imageWidth =  100 ;
         int  imageHeight =  30 ;
  
         WordGenerator wordGenerator =  new  RandomWordGenerator(
                 "0123456789abcdefghijklmnopqrstuvwxyz" );
  
         TextPaster randomPaster =  new  DecoratedRandomTextPaster(minWordLength,
                 maxWordLength,  new  RandomListColorGenerator( new  Color[] {
                 new  Color( 23 170 27 ),  new  Color( 220 34 11 ),
                 new  Color( 23 67 172 ) }),  new  TextDecorator[] {});
        
         BackgroundGenerator background =  new  UniColorBackgroundGenerator(
                 imageWidth, imageHeight, Color.LIGHT_GRAY);
         FontGenerator font =  new  RandomFontGenerator(fontSize, fontSize,
                 new  Font[] {  new  Font( "nyala" , Font.BOLD, fontSize),
                         new  Font( "Bell MT" , Font.PLAIN, fontSize),
                         new  Font( "Credit valley" , Font.BOLD, fontSize) });
  
         ImageDeformation postDef =  new  ImageDeformationByFilters(
                 new  ImageFilter[] {});
         ImageDeformation backDef =  new  ImageDeformationByFilters(
                 new  ImageFilter[] {});
         ImageDeformation textDef =  new  ImageDeformationByFilters(
                 new  ImageFilter[] {});
  
         WordToImage word2image =  new  DeformedComposedWordToImage(font,
                 background, randomPaster, backDef, textDef, postDef);
         addFactory( new  GimpyFactory(wordGenerator, word2image));
  
     }
}


         5)生成验证码图片

/**
  * 验证码
  */
@Controller
public  class  CaptchaController {
     private  static  final  Logger LOGGER = LoggerFactory.getLogger(CaptchaController. class );
  
     @Resource
     private  ImageCaptchaService imageCaptchaService;
  
     @RequestMapping (value =  "/jcaptcha" )
     public  void  ImageCaptcha(HttpServletRequest request , HttpServletResponse response)  throws  IOException {
         String captchaId = UUID.randomUUID().toString();
  
         BufferedImage image = imageCaptchaService.getImageChallengeForID(captchaId, request.getLocale());
         response.setHeader( "Cache-Control" "no-store" );
         response.setHeader( "Pragma" "no-cache" );
         response.setDateHeader( "Expires" 0 );
         response.setContentType( "image/jpeg" );
  
         Cookie cookie =  new  Cookie(Constants.CAPTCHA_COOKIE_NAME,captchaId);
         cookie.setMaxAge( 30 * 60 );
         response.addCookie(cookie);
  
         ServletOutputStream responseOutputStream = response.getOutputStream();
         ImageIO.write(image,  "jpg" , responseOutputStream);
  
         try  {
             responseOutputStream.flush();
         finally  {
             responseOutputStream.close();
         }
  
     }
  
}

 

 
        6)验证过程

 

@RequestMapping (value =  "/test" ,method = RequestMethod.POST)
@ResponseBody
public  JsonResponse test( @RequestParam (value =  "content" ) String content,
@RequestParam (value =  "authCode" ) String authCode,
HttpServletRequest request) {
     String captchaId =  null ;
     Cookie[] cookies = request.getCookies();
     for  (Cookie cookie : cookies) {
         if  (cookie.getName().equals(Constants.CAPTCHA_COOKIE_NAME)) {
             captchaId = cookie.getValue();
             break ;
         }
     }
 
     if  (StringUtil.isBlank(captchaId)) {
         return  new  JsonResponse( 40401 , "验证码错误" );
     }
 
     Boolean flag =  false ;
     try  {
         flag = imageCaptchaService.validateResponseForID(captchaId, authCode);
     catch  (CaptchaServiceException cse) {
 
     }
     if  (!flag) {
         return  new  JsonResponse( 40401 , "验证码错误" );
     }
 
    doSomething(); //业务任务
 
     return  new  JsonResponse( 200 , "success" );
}
 
 

五、参考资料

猜你喜欢

转载自zhanshenny.iteye.com/blog/2098779