2012年10月17日 星期三

Get Random Word in the JCaptcha

這裡的JCaptcha指的是整合在Jasig CAS,相關設定可參考另一篇 JCaptcha integration with CAS 這裡的測試也會延續這一篇的設定。因有追查在某些裝置上驗證碼會無效的需求,需要 log 產生在登入頁的驗證碼及 User 輸入的驗證碼,以查出問題之所在,查詢 JCaptcha API 後發現沒有 interface 可取得已產出的驗證碼,同樣求神問卜後找到這一篇使用 JCaptcha 開發圖形和聲音驗證碼,因同時使用圖形及聲音驗證所以需要驗證碼一致,透過 override API 的方式來取得產出的驗證碼,主要是參考這一篇文章將取得驗證碼整合至 Jasig CAS 及 JCaptcha 以達到目的。

使用 JCaptcha 開發圖形和聲音驗證碼有提供 SourceCode ,會直接下載來整合進 Jasig CAS 和 JCaptcha,如果只是修改 package 則不再列出,build cas-server-core 也請參考另一篇的說明。

準備執行環境

Maven是為 build JCaptcha integration with CAS 做準備
  • JDK (Java Development Kit) version 1.6+
  • Apache Maven 2.2.1+
  • Apache Tomcat 6+
  • JA-SIG CAS 3.4.2.1+
  • JCaptcha 1.0

下載 Sample



Build & Running JCaptcha Sample integration with CAS

  1. 將 Sample 整合至 CAS 可參考以下二張圖的對照,因不想 CAS 的相關設定改變太多,所以選擇修改 Sample 的 package 架構,不然依測試的經驗此 Sample 不用修改即可運作;保留另一篇 JCaptcha integration with CAS 文章中的CaptchaServiceSingleton.javaImageCaptchaServlet.java,因此 Sample 複製到 CAS 會有以下的對應修改,另外只處理圖形驗證碼所以聲音驗證的相關 Java file 不用複製。
  2. com\sample\jcaptcha\service\SampleImageCaptchaService.java --> org\jasig\cas\captcha\CaptchaServiceSingleton.java
    
    com\sample\jcaptcha\servlet\ImageCaptchaServlet.java --> org\jasig\cas\captcha\ImageCaptchaServlet.java
    



  3. cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\factory\SampleGimpyFactory.java line-37 在產生驗證碼的圖檔之前先取得驗證碼的文字。
  4. package org.jasig.cas.captcha.factory;
    
    import java.awt.image.BufferedImage;
    import java.util.Locale;
    
    import com.octo.captcha.CaptchaException;
    import com.octo.captcha.CaptchaQuestionHelper;
    import com.octo.captcha.component.image.wordtoimage.WordToImage;
    import com.octo.captcha.component.word.wordgenerator.WordGenerator;
    import com.octo.captcha.image.ImageCaptcha;
    import com.octo.captcha.image.gimpy.GimpyFactory;
    import org.jasig.cas.captcha.service.WordBridge;
    
    /**
     * Sample Gimpy Factory
     * @author guangqingzhong
     *
     */
    public class SampleGimpyFactory extends GimpyFactory {
     
        private WordBridge wordBridge = new WordBridge();
    
     
        public SampleGimpyFactory(WordGenerator generator, WordToImage word2image) {
            super(generator, word2image);
        }
      
    
        public SampleGimpyFactory(WordGenerator generator, WordToImage word2image, WordBridge wordBridge) {
            super(generator, word2image);
        }
    
        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;
        }
    
        public WordBridge getWordBridge() {
            return this.wordBridge ;
        }
    }
    
    

  5. cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\engin\SampleListImageCaptchaEngine.java 有修改成亂數取背景圖的方式,不是完全照 Sample 處理,line-57 可看到 Create Image 是使用 SampleGimpyFactory object,再透過 WordBridge object 取得驗證碼的文字,line-65 則是之前產生驗證碼圖片的方式。
  6. package org.jasig.cas.captcha.engine;
    
    import java.awt.Color;
    import java.awt.Font;
    
    import com.octo.captcha.component.image.backgroundgenerator.FunkyBackgroundGenerator;
    import com.octo.captcha.component.image.backgroundgenerator.FileReaderRandomBackgroundGenerator;
    import com.octo.captcha.component.image.color.SingleColorGenerator;
    import com.octo.captcha.component.image.fontgenerator.TwistedRandomFontGenerator;
    import com.octo.captcha.component.image.fontgenerator.FontGenerator;
    import com.octo.captcha.component.image.fontgenerator.RandomFontGenerator;
    import com.octo.captcha.component.image.textpaster.DecoratedRandomTextPaster;
    import com.octo.captcha.component.image.textpaster.TextPaster;
    import com.octo.captcha.component.image.textpaster.RandomTextPaster;
    import com.octo.captcha.component.image.textpaster.textdecorator.BaffleTextDecorator;
    import com.octo.captcha.component.image.textpaster.textdecorator.TextDecorator;
    import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
    import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
    import com.octo.captcha.engine.image.ListImageCaptchaEngine;
    import com.octo.captcha.image.ImageCaptchaFactory;
    import org.jasig.cas.captcha.factory.SampleGimpyFactory;
    import org.jasig.cas.captcha.service.WordBridge;
    
    /**
     * Sample Image Engine
     * 
     * @author guangqingzhong
     * 
     */
    public class SampleListImageCaptchaEngine extends ListImageCaptchaEngine {
        private WordBridge wordBridge;
     
    
        public SampleListImageCaptchaEngine() {
            super();
        }
    
    
        protected void buildInitialFactories() {
    
            FileReaderRandomBackgroundGenerator background = new FileReaderRandomBackgroundGenerator(new Integer(180), new Integer(50),"../webapps/cas/images/backgrounds/");
    
    
            Font[] fontsList = new Font[] {
                new Font("Arial", 0, 10),
                new Font("Tahoma", 0, 10),
                new Font("Verdana", 0, 10),
            };
    
            FontGenerator fontGenerator = new RandomFontGenerator(new Integer(30), new Integer(30), fontsList);
    
            //create text parser
            TextPaster randomPaster = new RandomTextPaster(new Integer(6),
                new Integer(6), new SingleColorGenerator(Color.BLACK), true);
    
            //create image captcha factory
            ImageCaptchaFactory factory = new SampleGimpyFactory(
                new RandomWordGenerator("abcdefghijklmnopqrstuvwxyz0123456789"),
                new ComposedWordToImage(fontGenerator, background, randomPaster));
    
            wordBridge = ((SampleGimpyFactory)factory).getWordBridge();
      
            ImageCaptchaFactory characterFactory[] = { factory};
            this.addFactories(characterFactory);
            //this.addFactory(new GimpyFactory(wgen, wordToImage));
        }
    
        public WordBridge getWordBridge() {
            return wordBridge;
        }
    }
    

  7. cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\CaptchaServiceSingleton.java 前一篇文章是將這一支程式同時實作 ImageCaptchaServiceImageCaptchaEngine 的功能,現在則採用 Sample 的方式各自實作,這裡取得 Image Buffer 後會以 sessionID 為 Key 將驗證碼文字放到 WordMap object (line - 47),另外為了 CAS 判斷驗證碼的方便性,將 CaptchaServiceSingleton 改為 ImageCaptchaService  line - 20、25。
  8. package org.jasig.cas.captcha;
    
    import java.awt.image.BufferedImage;
    
    import com.octo.captcha.engine.CaptchaEngine;
    import com.octo.captcha.engine.image.ListImageCaptchaEngine;
    import com.octo.captcha.service.CaptchaServiceException;
    import com.octo.captcha.service.captchastore.CaptchaStore;
    import com.octo.captcha.service.captchastore.FastHashMapCaptchaStore;
    import com.octo.captcha.service.image.AbstractManageableImageCaptchaService;
    import com.octo.captcha.service.image.ImageCaptchaService;
    import org.jasig.cas.captcha.engine.SampleListImageCaptchaEngine;
    import org.jasig.cas.captcha.service.WordMap;
    import org.jasig.cas.captcha.service.WordBridge;
    
    public class CaptchaServiceSingleton extends
      AbstractManageableImageCaptchaService implements ImageCaptchaService {
    
        //private static CaptchaServiceSingleton instance;
        private static ImageCaptchaService instance;
    
        private static ListImageCaptchaEngine engine;
    
        //public static CaptchaServiceSingleton getInstance() {
        public static ImageCaptchaService getInstance() {
            if (instance == null) {
                engine = new SampleListImageCaptchaEngine();
                instance = new CaptchaServiceSingleton(
                            new FastHashMapCaptchaStore(), engine, 180, 1000, 750);
            }
            return instance;
        }
    
        public CaptchaServiceSingleton(CaptchaStore captchaStore,
                CaptchaEngine captchaEngine, int minGuarantedStorageDelayInSeconds,
                int maxCaptchaStoreSize, int captchaStoreLoadBeforeGarbageCollection) {
            super(captchaStore, captchaEngine, minGuarantedStorageDelayInSeconds,
                maxCaptchaStoreSize, captchaStoreLoadBeforeGarbageCollection);
        }
    
        @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;
        }
    }
    

  9. 修改cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\web\flow\AuthenticationViaFormAction.java,於驗證錯誤時將登入前後的驗證碼文字 print 出來,參考 line-30 ~ 32
  10. import com.octo.captcha.service.CaptchaServiceException;
    import org.jasig.cas.captcha.CaptchaServiceSingleton;
    import org.jasig.cas.captcha.service.WordMap;
    ....
    
        public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) throws Exception {
            final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
            final Service service = WebUtils.getService(context);
    
            // for captcha check input and expect value begin
            final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
            Boolean isResponseCorrect = Boolean.FALSE;
            String enterCaptchaId = request.getParameter( "j_captcha_response" );
            String captchaId = request.getSession().getId();
    
    ....
    
            try {
                //WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
                //putWarnCookieIfRequestParameterPresent(context);
                //return "success";
    
                // for captcha
                if( Boolean.TRUE.equals(isResponseCorrect) ) { 
                    WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
                    putWarnCookieIfRequestParameterPresent(context);
                    return "success";
                } else {
                    // for captcha, captcha validate failed
                    System.out.println("Session ID=" + captchaId);
                    System.out.println("generated word=" + WordMap.getWordsMap().get(captchaId));
                    System.out.println("j_captcha_response=" + enterCaptchaId);
    
                    messageContext.addMessage( new MessageBuilder().error().code( "captcha.input.error" ).defaultText( "captcha.input.error" ).build() );
                    return "error"; 
                }// for captcha 
    
            } catch (final TicketException e) {
                populateErrorsInstance(e, messageContext);
                return "error";
            }
        }
    

  11. build cas-server-core-3.4.3.jar,切換到 cas-server-3.4.3\cas-server-core 路徑執行下列指令,將產出的cas-server-core-3.4.3.jar複製到cas\WEB-INF\lib此路徑下。
  12. mvn clean package -Dmaven.test.skip=true

  13. 啟動 Tomcat ,連結至CAS Login Page,輸入錯誤的驗證碼,可看到以下的 log,驗證碼為e4ct0a第一次出現時是進入登入頁時,第二次出現時是按登入驗證後,123456是故意輸錯的驗證碼。

  14. 測試驗證碼一直出現錯誤的裝置,可看到以下的 log,於登入頁顯示的驗證碼為691152,但 Server 已經產生第二組驗證碼053171了,造成驗證碼不符無法登入成功,經高人指點是因為 Browser 的行為造成的,可看相關參考的連結,說明的相當清楚就不再重覆了。


相關設定可參考:
使用 JCaptcha 開發圖形和聲音驗證碼
Response Download file for Android Browser

沒有留言:

張貼留言