JCaptcha 簡介

楊文壽發表於2013-01-05

CAPTCHA 全稱 Completely Automated Public Turing Test to Tell Computers and Humans Apart,最早作為卡內基梅隆大學的一個科研專案,用於生成一個人類容易通過而計算機難以通過的測試,目前廣泛應用於網路應用,用於阻止機器人釋出垃圾資訊。JCaptcha 即為 Java 版本的 CAPTCHA 專案,其是一個開源專案,支援生成圖形和聲音版的驗證碼,在生成聲音版的驗證碼時,需要使用到 FreeTTS。目前,JCaptcha 官方網站顯示有 2.0 版本,但二進位制版只有 1.0 版可供下載,本文亦是基於 1.0 版本展開。

一個簡單的圖形驗證碼

JCaptcha 提供了一定的可擴充套件能力,用於開發人員建立出複雜的圖形驗證碼。下面,首先利用 JCaptcha 提供的 API 來快速開發一個簡單示例。本文的示例為 Web 應用,可以執行在 Tomcat 和 WebSphere 上,除了準備 Web 伺服器外,我們還需要準備好 JCaptcha 執行時所必須的 Jar 包。

準備所需的 Jar 包

JCaptcha 專案在實現中,還引用了 commons-collections 和 commons-logging 兩個開源專案,再加上 JCaptcha 本身的實現,我們共需要三個包,具體資訊如下:

  • jcaptcha-1.0-all.jar
  • commons-logging-1.1.1.jar
  • commons-collections-3.2.jar

建立生成圖形驗證碼的 Servlet

在示例 1 中,我們採用 Servlet 生成驗證碼圖片,並將這個圖片寫到 ServletOutputStream 中,同時將 Servlet 的 response 的型別設定為 image/jpeg。在前臺頁面中,我們使用 img 標籤,並將 src 指向這個 Servlet,這樣我們就可以在頁面中顯示這個生成的驗證碼圖片。清單 1 是 Servlet 程式碼片段。


清單 1. 生成圖形驗證碼程式碼片段
				
 // set content type as jpeg 
 httpServletResponse.setHeader("Cache-Control", "no-store"); 
 httpServletResponse.setHeader("Pragma", "no-cache"); 
 httpServletResponse.setDateHeader("Expires", 0); 
 httpServletResponse.setContentType("image/jpeg"); 

 // create the image using session ID 
 logger.fine("tring to get image captcha service"); 
 BufferedImage bufferedImage = service 
        .getImageChallengeForID(httpServletRequest.getSession(true) 
            .getId()); 

 ServletOutputStream servletOutputStream = httpServletResponse 
        .getOutputStream(); 

 // write the image to the servlet output stream 
 logger.fine("tring to output buffered image to servlet output stream"); 
 ImageIO.write(bufferedImage, "jpg", servletOutputStream); 
  
 try { 
  servletOutputStream.flush(); 
 } finally { 
  servletOutputStream.close(); 
 } 

清單 1 中的 service 物件,我們採用了一個 JCaptcha 的預設實現類 DefaultManageableImageCaptchaService,在程式碼中,我們需要通過獲取 service 物件,根據 Session ID 來生成驗證碼圖片。除了使用該預設實現類外,我們也可以通過實現 ImageCaptchaService 介面來生成更為複雜的驗證碼圖片(參見示例 2)。清單 2 為獲取 service 物件的示例程式碼,JCaptcha 建議使用單例來生成 service 物件。


清單 2. 生成 ImageCaptchaService 物件
				
 public class SampleImageCaptchaService  { 

 private static ImageCaptchaService instance; 

 /** 
 * Get default manageable image captcha service 
 * @return ImageCaptchaService 
 */ 
 public static ImageCaptchaService getInstance() { 
 if (instance == null) { 
    instance = new DefaultManageableImageCaptchaService(); 
  } 
    return instance; 
  } 
 } 

示例 1 提供了 2 個簡單的頁面,一個用於顯示圖形驗證碼,另一個則在驗證成功後顯示成功資訊。清單 3 為用於顯示圖形驗證碼的頁面程式碼片段。


清單 3. 顯示圖形驗證碼頁面程式碼片段
				
 <form action="CaptchaValidationServlet" method="post"> 
 <table> 
  <tr> 
    <td colspan="2"><img src="ImageCaptchaServlet" /></td> 
  </tr> 
  <tr> 
    <td> 請輸入您所看到的字元 :</td> 
    <td><input type="text" name="captcha_input" value="" /> 
                <%=request.getAttribute("ERROR") == null ? "" :       
                request.getAttribute("ERROR")%></td> 
  </tr> 
  <tr> 
      
    <td><input type="submit" value="提交" /></td> 
  </tr> 
 </table> 
 </form> 

除了實現圖形驗證碼的生成和展示,我們還需要一個 Servlet 來驗證使用者的輸入。使用者輸入驗證的程式碼的核心,是使用在生成圖片時所使用的 service 物件,呼叫 JCaptcha 的輸入驗證方法(本示例為 validateResponseForID 方法),來判斷使用者輸入是否正確,清單 4 為輸入驗證程式碼片段。


清單 4. 驗證使用者輸入
				
 validated = service.validateResponseForID( 
    request.getSession(true).getId(), userCaptchaResponse).booleanValue(); 

完成上述程式碼工作後,我們還需要在 Web.xml 中設定好上面建立的兩個 Servlet,然後再部署到 Web 伺服器上,圖 1 是部署成功後的示例 1 首頁。


圖 1. 示例 1 的首頁
圖 1. 示例 1 的首頁 

Sample1Index.jpg

至此,我們完成了第一個示例,該示例採用了 JCaptcha 提供的預設實現,下面我們將通過擴充套件使用 JCaptcha,來生成一個更為複雜的圖形驗證碼。

使用 JCaptcha 生成複雜圖形驗證碼

在實際專案中,我們可能需要使用更為複雜的圖形驗證碼,比如,加入帶干擾的圖形背景、更多的干擾點、自定義隨機字型檔及圖形字型等等,下面將基於示例 2 介紹如何使用 JCaptcha 生成更為複雜的圖形驗證碼。

自定義 ImageCaptchaService 的實現

正如介紹示例 1 時所提到的,我們可以通過實現 ImageCaptchaService 介面來建立我們自己所需要的 service 物件,清單 5 為示例 2 對介面 ImageCaptchaService 的實現,我們可以在該實現中,使用自己開發的圖形驗證碼生成引擎(SampleListImageCaptchaEngine)來生成更為複雜的圖形驗證碼。


清單 5. 示例 2 中 ImageCaptchaService 的實現
				
 public class SampleImageCaptchaService extends 
    AbstractManageableImageCaptchaService implements ImageCaptchaService { 

  private static SampleImageCaptchaService instance; 

  public static SampleImageCaptchaService getInstance() { 
    if (instance == null) { 
      //use customized engine 
      ListImageCaptchaEngine engine = new SampleListImageCaptchaEngine(); 
      instance = new SampleImageCaptchaService( 
        new FastHashMapCaptchaStore(), engine, 180, 100000, 75000); 
    } 
    return instance; 
  } 
  
  public SampleImageCaptchaService(CaptchaStore captchaStore, 
      CaptchaEngine captchaEngine, int minGuarantedStorageDelayInSeconds, 
      int maxCaptchaStoreSize, int captchaStoreLoadBeforeGarbageCollection) { 
    super(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds, 
        maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection); 
  } 
 } 

構建生成圖形驗證碼的 ImageCaptchaEngine

清單 5 中的 FastHashMapCaptchaStore 用於存放生成的驗證碼字元,FastHashMapCaptchaStore 基本可以滿足絕大多數專案需求,我們通常不需要建立自己的類來實現它的功能;SampleListImageCaptchaEngine 則是擴充套件的核心,該類是 ImageCaptchaEngine 的一個實現,在該類中,我們可以設定隨機字型檔、生成的字型和大小、字元數、背景圖片以及干擾點等等。清單 6 為 SampleListImageCaptchaEngine 程式碼片段。


清單 6. SampleListImageCaptchaEngine 程式碼片段
				
 //create text parser 
 TextPaster randomPaster = new DecoratedRandomTextPaster(new Integer(8), 
        new Integer(10), new SingleColorGenerator(Color.BLACK), 
 new TextDecorator[] { new BaffleTextDecorator(new Integer(1), Color.WHITE) }); 
 //create image captcha factory 
 ImageCaptchaFactory factory = new GimpyFactory( 
        new RandomWordGenerator("abcdefghijklmnopqrstuvwxyz"), 


    new ComposedWordToImage(new TwistedRandomFontGenerator(new Integer(34), 
            new Integer(40)), new FunkyBackgroundGenerator(new Integer( 
            260), new Integer(70)), randomPaster)); 
    
 ImageCaptchaFactory characterFactory[] = { factory}; 
 this.addFactories(characterFactory); 

在清單 6 中,DecoratedRandomTextPaster 的第一個引數用於設定驗證碼最少字元數,第二個引數為最多的字元數,第三個引數 SingleColorGenerator 為字型顏色,這裡為黑色,TextDecorator 為干擾設定,這裡是一個字元一個干擾點,並且干擾點為白色。

在 ImageCaptchaFactory 中,第一個引數設定了隨機字型檔,這裡為英文字母,在第二個引數中,TwistedRandomFontGenerator 設定了生成的字元字型,最小 34,最大為 40,FunkyBackgroundGenerator 則用於生成干擾背景,除了設定字型大小外,還需要設定生成的圖片大小,示例 2 為 260*70 畫素。圖 2 為示例 2 的首頁截圖。


圖 2. 示例 2 的首頁
圖 2. 示例 2 的首頁 

Sample2Index.jpg

可以說 ImageCaptchaEngine 的不同實現,決定了圖形驗證碼的不同樣式,JCaptcha 目前提供了多種 ImageCaptchaEngine 的實現,這些實現提供了多種不同的圖形驗證碼樣式,當然,我們也可以自己實現 TextPaster 及 TextDecorator 等,進而再繼承實現 ImageCaptchaEngine,從而實現自己所期望的效果。圖 3 為 JCaptcha 官方網站提供的部分 ImageCaptchaEngine 的實現截圖,更多樣式,請參見 官方網站


圖 3. JCaptcha 官方圖形驗證碼示例
圖 3. JCaptcha 官方圖形驗證碼示例 

JCaptchaSample.jpg

開發聲音驗證碼

由於某些專案需要支援盲人使用,而盲人無法看到圖形驗證碼,這時,我們就需要同時提供聲音版的驗證碼。JCaptcha 同時支援聲音版驗證碼,但預設實現並不支援生成的聲音和圖形驗證碼中的字元一致,這裡就需要通過一定的擴充套件定製,才能保證聲音和圖形驗證碼的內容一致,下面首先介紹如何生成聲音驗證碼,然後介紹如何擴充套件定製,才能保證聲音和圖形驗證碼的內容的一致。

FreeTTS

JCaptcha 使用了 FreeTTS 來開發聲音驗證碼,FreeTTS 是一個採用 Java 編寫的語音合成專案,它基於 Flite 專案編寫,Flite 專案是一個由卡內基梅隆大學開發的小型語音合成引擎,其又源於愛丁堡大學的 Festival 語音合成系統和卡內基梅隆大學的 FestVox 專案。本文的示例使用了 FreeTTS 1.2.2,該版本的有如下幾個方面的功能:

  • 一個語音合成引擎
  • 支援多種聲音
    • 1 個 8khz,多音位,男性美國英語發音
    • 1 個 16khz,多音位,男性美國英語發音
    • 1 個 16khz 有限聲音域的男性美國英語發音
  • 支援從 FestVox 匯入聲音(僅支援美國英語)
  • 一定程度上支援從 FestVox 中匯入 CMU ARCTIC 聲音
  • 支援 MBROLA 聲音(需另外下載該功能包)
    • 1 個 16khz 女性美國英語發音
    • 2 個 16khz 男性美國英語發音
  • 部分支援 JSAPI 1.0

為了使用 FreeTTS,JCaptcha 還另外提供了一個 Jar 包用於和 FreeTTS 的整合,所以在執行過程中,除了需要引入 FreeTTS 自帶的 Jar 包,還需要包括 JCaptcha 提供的和 FreeTTS 整合的包,在示例 3 和 4 中,使用瞭如下的包:

  • jcaptcha-extension-sound-freetts-1.0.jar:用於和 FreeTTS 的整合
  • jcaptcha-1.0-all.jar: JCaptcha 核心包
  • freetts.jar:FreeTTS 核心包
  • en_us.jar:用於 FreeTTS
  • commons-logging-1.1.1.jar:用於 JCaptcha
  • commons-collections-3.2.jar:用於 JCaptcha
  • cmutimelex.jar:用於 FreeTTS
  • cmulex.jar:用於 FreeTTS
  • cmudict04.jar:用於 FreeTTS
  • cmu_us_kal.jar:用於 FreeTTS
  • cmu_time_awb.jar:用於 FreeTTS

構建生成聲音的 SoundCaptchaEngine

同 ImageCaptchaEngine 一樣,JCaptcha 同時提供了 SoundCaptchaEngine,所以整個聲音驗證碼的核心是建立 SoundCaptchaEngine 的實現,當然 JCaptcha 也提供了一些預設的實現,比如 DefaultSoundCaptchaEngine 以及 SpellerSoundCaptchaEngine 等等,為了考慮能夠支援生成和圖形驗證碼相同的字元,示例 3 採用繼承抽象類 ListSoundCaptchaEngine 的方式,來建立自己的實現 SampleListSoundCaptchaEngine。清單 7 為 SampleListSoundCaptchaEngine 程式碼片段。


清單 7. SampleListSoundCaptchaEngine 程式碼片段
				
 public class SampleListSoundCaptchaEngine extends ListSoundCaptchaEngine {  

  private static String voiceName = "kevin16"; 

   private static String voicePackage = 
   "com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory"; 

  protected void buildInitialFactories() { 
    //create word generator 
  WordGenerator wordGenerator = new RandomWordGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 
  //create decorator 
 SpellerWordDecorator decorator = new SpellerWordDecorator(", "); 
 //create sound factory 
  SoundCaptchaFactory soundFactory[] = { new SpellerSoundFactory( 
        wordGenerator, 
      new FreeTTSWordToSound(new FreeTTSSoundConfigurator(voiceName, 
          voicePackage, 1.0f, 100, 100), 4, 10), decorator) }; 
    this.setFactories(soundFactory); 
  } 
 } 

建立生成聲音驗證碼的 Servlet

同開發圖形驗證碼一樣,這裡也需要建立 SoundCaptchaService 的實現,實現方式與 ImageCaptchaService 的實現類似,示例 3 的實現類為 SampleSoundCaptchaService,這裡不做敘述,讀者可以參見附帶的示例原始碼。

和 ImageCaptchaService 的實現不同的是,這裡不能採用單例的方式來獲取 SoundCaptchaService 的實現,否則不能多次為同一 Session ID 生成多個聲音驗證碼檔案。另外,在使用者不重新整理頁面,而重複點選聲音驗證碼圖示時,我們需要提供同一段聲音,因此,我們可以將 SoundCaptchaService 的實現物件以及所生成的位元組碼流放入到 session 中,清單 7 為 SoundCaptchaServlet 程式碼片段。


清單 8. SoundCaptchaServlet 程式碼片段
				
 httpServletResponse.setContentType("audio/x-wav"); 
 SoundCaptchaService service = null; 
 if ( httpServletRequest.getSession().getAttribute("soundService") != null) { 
   service = (SoundCaptchaService) httpServletRequest 
      .getSession().getAttribute("soundService"); 
 } else { 
  service = new SampleSoundCaptchaService(); 
 } 
 // get AudioInputStream using session 
 ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();  
 //get from the session if already existed, otherwise, create new one 
 if (httpServletRequest.getSession().getAttribute("stream") == null) { 
      AudioInputStream audioInputStream = service.getSoundChallengeForID( 
  httpServletRequest.getSession().getId(), httpServletRequest.getLocale()); 

      AudioSystem.write(audioInputStream, javax.sound.sampled.AudioFileFormat.Type.WAVE, 
              byteOutputStream); 
 } else { 
    byteOutputStream = 
    (ByteArrayOutputStream)httpServletRequest.getSession().getAttribute("stream"); 
 }    

 // output to servlet 
 ServletOutputStream servletOutputStream = httpServletResponse.getOutputStream(); 
 servletOutputStream.write(byteOutputStream.toByteArray()); 
 // save the service object to session, will use it for validation 
 // purpose 
 httpServletRequest.getSession().setAttribute("soundService", service); 
 httpServletRequest.getSession().setAttribute("stream", byteOutputStream); 
 // output to servlet response stream 
 try { 
  servletOutputStream.flush(); 
 } finally { 
  servletOutputStream.close(); 
 } 

建立 Web 頁面和驗證程式碼

為了方便使用者使用,我們需要在頁面上放置一個圖示,當焦點落在圖示上(考慮到盲人可能使用鍵盤 Tab 鍵來切換焦點),觸發 onFocus 事件來呼叫 JavaScript 方法顯示 embed 標籤,從而呼叫上面生成聲音驗證碼的 Servlet。圖 4 為示例 3 首頁截圖。


圖 4. 示例 3 執行頁面截圖
圖 4. 示例 3 執行頁面截圖 

Sample3Index.jpg

當聲音驗證碼圖示在已經獲取焦點,並朗讀後,使用者可能多次重複讓該圖示獲取焦點以便多次收聽,這時,JavaScript 方法會被多次觸發,如果不做處理,其將多次請求 Servlet,以致生成不同的聲音檔案,本示例採用了前一個小節所介紹的快取 SoundCaptchaService 物件和位元組碼流的方法來解決這個問題,以保證多次能收聽到同一段聲音。當然,也可以採用在 URL 後面加一個隨機 token 的方式,來判斷是否重複提交。清單 8 為示例 3 首頁程式碼片段。

清單 9. 示例 3 index.jsp 程式碼片段

 <% 
      
    request.getSession().removeAttribute("soundService"); 
      request.getSession().removeAttribute("stream"); 
 %> 
 <html> 
 <head> 
 <script type="text/javascript"> 
 
 function playSoundCaptcha() { 
   var wavURL = '<%= request.getContextPath() %>'+'/SoundCaptchaServlet'; 
     var embedAudioPlayer = "<EMBED SRC='" + wavURL + "' HIDDEN='true' AUTOSTART='true'  />";  
|-------10--------20--------30--------40--------50--------60--------70--------80--------9|
|-------- XML error:  The previous line is longer than the max of 90 characters ---------|
   var wavArea = document.getElementById("wavArea"); 
   wavArea.innerHTML = embedAudioPlayer; 
 
   
 } 
 </script> 
 </head> 

 <form action="CaptchaValidationServlet" method="post"> 
 <table> 
  <tr> 
    <td colspan="2"><img src="ImageCaptchaServlet" /></td> 
  </tr> 
  <tr> 
    <td> 請輸入您所看到的字元 :</td> 
    <td><input type="text" name="captcha_input" value="" /> 
    <a href="#"  onFocus="playSoundCaptcha()"> 
  <img src="image/wheelchair.jpg" height="18px" width="18px" 
  alt=""></a><%=request.getAttribute("ERROR") == null ? "" : 
    request  .getAttribute("ERROR")%></td> 
  </tr> 
  <tr> 
    <td><input type="submit" value="提交" /></td> 
  </tr> 
 <div id="wavArea" style="display:none"> 
 </div> 
 </table> 
 </form> 

可以看到,我們採用了不在頁面顯示的 embed 標籤來呼叫 Servlet 以獲取聲音流,然後自動播放。在每次頁面重新整理的時候,還需要將 Session 中的 SoundCaptchaService 的實現物件以及音訊流刪除,以便頁面重新整理時,重新生成新的聲音驗證碼。

後臺用來驗證使用者輸入的邏輯和圖形版的驗證方式相似,這裡不做介紹,具體參見示例原始碼。需要注意的是,由於示例 3 生成的聲音驗證碼中的字元和圖形版的字元並不一致,在後臺驗證的邏輯中,只需要使用者輸入等於聲音驗證碼或圖形驗證碼的字元,即可認為使用者輸入正確。下面將介紹如何才能使聲音版和圖形版中的字元保持一致。

使聲音驗證碼和圖形驗證碼字元一致

JCaptcha 並沒有提供很直觀的方式,來保證聲音驗證碼和圖形驗證碼中的字元一致,這就需要我們瞭解 JCaptcha 的實現,並能夠通過繼承的方式來擴充套件定製 JCaptcha,從而實現聲音和圖形驗證碼字元的一致。

由於圖形驗證碼會先於聲音驗證碼生成,所以,我們第一步就是需要獲取圖形驗證碼所生成的字串,然後是利用所獲取的字串來生成聲音驗證碼。

獲取圖形驗證碼隨機數

在以上示例中,雖然我們在實現 ImageCaptchaEngine 和 SoundCaptchaEngine 的時候,可以設定隨機數生成類,比如 RandomWordGenerator 等,但是即使聲音版和圖形版採用同一個隨機數生成物件,也不能保證會生成同一個字元,因為它們僅僅是設定一個隨機字型檔,而字元則是每次隨機生成。因而,我們並不能通過使用同樣的隨機數生成物件來生成同樣的隨機數,只能通過考慮使用圖形版生成的字元來生成聲音驗證碼,才能保持兩者的一致。

在建立 ImageCaptchaEngine 的實現時,我們需要提供一個 ImageCaptchaFactory,實際上,我們可以通過使用繼承實現 JCaptcha 已有 ImageCaptchaFactory 的實現,來獲取生成的隨機數。清單 9 是示例 4 中繼承了 GimpyFactory 的程式碼片段。


清單 9. SampleGimpyFactory 程式碼片段
				
 public class SampleGimpyFactory extends GimpyFactory { 
  
  ……

  public ImageCaptcha getImageCaptcha(Locale locale) { 
        //length 
        Integer wordLength = getRandomLength(); 

        String word = getWordGenerator().getWord(wordLength, locale); 
        if (this.wordBridge != null) { 
          this.wordBridge.setGeneratedWord(word); 
    } 
        BufferedImage image = null; 
        try { 
            image = getWordToImage().getImage(word); 
        } catch (Throwable e) { 
            throw new CaptchaException(e); 
        } 

        ImageCaptcha captcha = 
        new SampleGimpy(CaptchaQuestionHelper.getQuestion(locale, BUNDLE_QUESTION_KEY), 
                image, word); 
        return captcha; 
  } 

 } 

可以發現,通過使用上面的 SampleGimpyFactory 即可獲取圖形驗證碼的隨機數,所以我們可以將清單 6 中的 GimpyFactory 替換為 SampleGimpyFactory。

在獲取了生成的隨機數後,還需考慮如何將該隨機數傳遞給生成聲音驗證碼的程式碼。考慮到我們在生成驗證碼時,都是基於同一個 Session ID 生成,那麼我們就可以將生成的隨機數放到 map 中,而 key 是 Session ID,那麼,在生成驗證碼字元後,我們就可以用 Session ID 取出該字串,並放到 Session 中,然後,在生成聲音驗證碼的程式碼中,就可以從 Session 中獲取隨機數。但是,並不是所有程式碼都可以很方便的獲取到 ID,所以,我們還需要對示例 2 的程式碼進行改造,以便能夠根據 ID 儲存隨機數。清單 10 為改造後的清單 5 中的類 SampleImageCaptchaService 的程式碼片段。


清單 10. SampleImageCaptchaService 程式碼片段
				
 public class SampleImageCaptchaService extends 
    AbstractManageableImageCaptchaService implements ImageCaptchaService { 

  … .. 

  @Override 
  public BufferedImage getImageChallengeForID(String ID) 
      throws CaptchaServiceException { 
    BufferedImage image=  super.getImageChallengeForID(ID); 
    String generatedWord = ((SampleListImageCaptchaEngine) engine).getWordBridge() 
    .getGeneratedWord(); 
    WordMap.getWordsMap().put(ID, generatedWord); 
    return image; 
  } 

 } 

如清單 10 所示,我們將生成的隨機數放到了一個 map 中,而 key 則是 ID,這裡也就是 SessionID,然後我們就可以在 ImageCaptchaServlet 獲取並將該隨機數放到 Session 中,如清單 11 所示。接下來便是如何利用該字元生成聲音驗證碼。


清單 11. ImageCaptchaServlet 程式碼片段
				
 httpServletRequest.getSession().setAttribute( 
        "generatedWord", 
        WordMap.getWordsMap().get(httpServletRequest.getSession(true) 
            .getId())); 

利用指定字元生成聲音驗證碼

為了能夠使用指定的字串生成驗證碼,我們需要對示例 3 中的程式碼做一定的修改。這裡,示例 4 通過繼承 SpellerSoundFactory 實現了一個擴充套件的 SoundCaptchaFactory,清單 12 為程式碼片段。


清單 12. SampleSpellerSoundFactory 程式碼片段
				
 public class SampleSpellerSoundFactory extends SpellerSoundFactory { 

  private String word; 
  ……

  @Override 
  public SoundCaptcha getSoundCaptcha() { 
    return getSoundCaptcha(Locale.getDefault()); 
  } 

  @Override 
  public SoundCaptcha getSoundCaptcha(Locale locale) { 
    String soundWord = ""; 
    if (this.word != null && !this.word.equals("")) { 
      soundWord = this.word; 
    } else { 
      soundWord = this.wordGenerator.getWord(getRandomLength(), locale); 
    } 

    AudioInputStream sound = this.word2Sound.getSound(wordDecorator 
        .decorateWord(soundWord), locale); 
    SoundCaptcha soundCaptcha = new SpellerSound(getQuestion(locale), 
        sound, word); 
    return soundCaptcha; 
  } 

 } 

根據清單 11 中的程式碼,如果字串能夠正確的傳進來,這個 SampleSpellerSoundFactory 將可以根據該字串生成聲音驗證碼。

相應的,清單 7 中的 SampleListSoundCaptchaEngine 需要做如清單 13 所示的修改。


清單 13. SampleListSoundCaptchaEngine 程式碼
				
 public class SampleListSoundCaptchaEngine extends ListSoundCaptchaEngine { 
  
  private String word;  

  private static String voiceName = "kevin16"; 

  private static String voicePackage = 
  "com.sun.speech.freetts.en.us.cmu_us_kal.KevinVoiceDirectory";

  protected void buildInitialFactories() { 
    WordGenerator wordGenerator = new RandomWordGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ"); 
     SpellerWordDecorator decorator = new SpellerWordDecorator(", "); 
    SoundCaptchaFactory soundFactory[] = { new SampleSpellerSoundFactory( 
        wordGenerator, 
        new FreeTTSWordToSound(new FreeTTSSoundConfigurator(voiceName,
          voicePackage, 1.0f, 100, 100), 4, 10), decorator, word) }; 
    this.setFactories(soundFactory); 
  } 

  public SampleListSoundCaptchaEngine(String word) { 
      this.word = word; 
      buildInitialFactories(); 
  
  } 

 } 

需要注意的是,我們一定要有如清單 13 所示的帶引數的夠找函式,否則初始化時,該類會首先呼叫父類的建構函式,其父類會直接呼叫 buildInitialFactories 函式,而此時字串還沒有傳遞給父類。

接下來需要修改 SampleSoundCaptchaService,在該類中使用 SampleListSoundCaptchaEngine 並傳入隨機數引數。


清單 14. SampleSoundCaptchaService 程式碼片段
				
 public class SampleSoundCaptchaService extends 
    AbstractManageableSoundCaptchaService implements SoundCaptchaService { 
  
  public SampleSoundCaptchaService(String word) { 
    super(new FastHashMapCaptchaStore(), 
        new SampleListSoundCaptchaEngine(word), 180, 100000, 75000); 
  } 
  ……

  
 } 

最後,我們只需要修改 SoundCaptchaServlet,先依據 Session ID 獲取生成的隨機數,然後呼叫清單 14 的 SampleSoundCaptchaService 生成聲音驗證碼,如清單 15 所示。


清單 15. SoundCaptchaServlet 程式碼片段
				
 String word = ""; 
 if (httpServletRequest  .getSession().getAttribute("generatedWord") != null) { 
  word = (String)httpServletRequest.getSession().getAttribute("generatedWord"); 
  logger.info("Get generated word from the session, word=" + word); 
 } 
 service = new SampleSoundCaptchaService(word); 

至於後臺驗證邏輯,可以不做修改,也可以刪除原先用於驗證聲音驗證碼的程式碼。至此,聲音驗證碼的字元將與圖形驗證碼的字元保持一致。

總結

本文先從一個簡單的示例入手,介紹瞭如何採用 JCaptcha 開發一個簡單的圖形驗證碼,然後介紹瞭如何通過擴充套件定製,實現更為複雜的圖形驗證碼,接下來介紹瞭如何採用 JCaptcha 開發聲音驗證碼,並在最後,介紹瞭如何擴充套件 JCaptcha 來保證圖形驗證碼和聲音驗證碼的內容一致。