2012年10月17日 星期三

JCaptcha integration with CAS

說明Jasig CAS登入頁增加圖形驗證碼的功能,圖形驗證碼採用JCaptcha來實做,這部份參與的不多只有做一些小修改,因此大部份是同事的努力,這邊只是單純的詳實記錄下來,如同事有blogger也會提供相關連結以利參考。

CAS Server的相關設定請參考文章末的參考連結,這裡只著重在Jasig CAS跟JCaptcha的整合。

準備執行環境

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

下載JA-SIG CAS Server
  • 下載CAS Server版本為3.4.2.1,並將下載檔案cas-server-3.4.3-release.zip解壓縮。



Build & Running JCaptcha integration with CAS

  1. 修改pom.xml,可在cas-server-3.4.3-release.zip解壓縮後的路徑cas-server-3.4.3\cas-server-core\pom.xml下找到,新增下列幾行
  2.         <!-- jCaptcha -->
            <dependency>
                <groupId>com.octo.captcha</groupId>
                <artifactId>jcaptcha-all</artifactId>
                <version>1.0-RC6</version>
            </dependency>
    

  3. 新增cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\CaptchaServiceSingleton.java 檔案,以產生圖形驗證的圖檔。
  4. package org.jasig.cas.captcha;
    
    import com.octo.captcha.service.image.ImageCaptchaService;
    import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
    import com.octo.captcha.engine.image.ListImageCaptchaEngine;
    import com.octo.captcha.component.word.wordgenerator.WordGenerator;
    import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
    import com.octo.captcha.component.image.color.RandomRangeColorGenerator;
    import com.octo.captcha.component.image.textpaster.TextPaster;
    import com.octo.captcha.component.image.textpaster.RandomTextPaster;
    import com.octo.captcha.component.image.backgroundgenerator.BackgroundGenerator;
    import com.octo.captcha.component.image.backgroundgenerator.UniColorBackgroundGenerator;
    import com.octo.captcha.component.image.fontgenerator.FontGenerator;
    import com.octo.captcha.component.image.fontgenerator.RandomFontGenerator;
    import com.octo.captcha.component.image.wordtoimage.WordToImage;
    import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
    import com.octo.captcha.image.gimpy.GimpyFactory;
    import java.awt.Font;
    import com.octo.captcha.service.captchastore.FastHashMapCaptchaStore;
    import java.awt.Color;
    
    public class CaptchaServiceSingleton {
     private static ImageCaptchaService instance =
        new DefaultManageableImageCaptchaService(new FastHashMapCaptchaStore(),
                      new MyImageCaptchaEngine(),
                      180,
                      1000,
                      750);
    
     public static ImageCaptchaService getInstance() {
      return instance;
     }
    
     private static class MyImageCaptchaEngine extends ListImageCaptchaEngine {
      protected void buildInitialFactories() {
       WordGenerator wgen = new RandomWordGenerator("abcdefghijklmnopqrstuvwxyz0123456789");
       RandomRangeColorGenerator cgen = new RandomRangeColorGenerator(
         new int[] {0, 100},
         new int[] {0, 100},
         new int[] {0, 100});
       TextPaster textPaster = new RandomTextPaster(new Integer(7), new Integer(7), cgen, true);
    
       BackgroundGenerator backgroundGenerator = new UniColorBackgroundGenerator(new Integer(200), new Integer(100), new Color(204, 204, 204));
    
       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(20), new Integer(35), fontsList);
    
       WordToImage wordToImage = new ComposedWordToImage(fontGenerator, backgroundGenerator, textPaster);
       this.addFactory(new GimpyFactory(wgen, wordToImage));
      }
     }
    }
    

  5. 新增cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\ImageCaptchaServlet.java 檔案,以利CAS Server Login Page Request。
  6. package org.jasig.cas.captcha;
    
    import com.octo.captcha.service.CaptchaServiceException;
    import com.sun.image.codec.jpeg.JPEGCodec;
    import com.sun.image.codec.jpeg.JPEGImageEncoder;
    
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.awt.image.BufferedImage;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    
    
    public class ImageCaptchaServlet extends HttpServlet {
    
     public void init(ServletConfig servletConfig) throws ServletException {
      super.init(servletConfig);
     }
    
     protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
    
      byte[] captchaChallengeAsJpeg = null;
      // the output stream to render the captcha image as jpeg into
      ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
      try {
       // get the session id that will identify the generated captcha.
       //the same id must be used to validate the response, the session id is a good candidate!
       String captchaId = httpServletRequest.getSession().getId();
       // call the ImageCaptchaService getChallenge method
       BufferedImage challenge =
       CaptchaServiceSingleton.getInstance().getImageChallengeForID(captchaId,
       httpServletRequest.getLocale());
    
       // a jpeg encoder
       JPEGImageEncoder jpegEncoder =
       JPEGCodec.createJPEGEncoder(jpegOutputStream);
       jpegEncoder.encode(challenge);
      } catch (IllegalArgumentException e) {
       httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
      } catch (CaptchaServiceException e) {
       httpServletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       return;
      }
    
      captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
    
      // flush it in the response
      httpServletResponse.setHeader("Cache-Control", "no-store");
      httpServletResponse.setHeader("Pragma", "no-cache");
      httpServletResponse.setDateHeader("Expires", 0);
      httpServletResponse.setContentType("image/jpeg");
      ServletOutputStream responseOutputStream =
        httpServletResponse.getOutputStream();
      responseOutputStream.write(captchaChallengeAsJpeg);
      responseOutputStream.flush();
      responseOutputStream.close();
     }
    }
    

  7. 於 Login Page submit 時增加圖形驗證碼的判斷,接著找出 submit 時執行的程式,cas\WEB-INF\login-webflow.xml片段如下,登入成功與否的控制在authenticationViaFormAction.submit() method。
  8.  
        <view-state id="viewLoginForm" view="casLoginView" model="credentials">
            <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
            <binder>
                <binding property="username" />
                <binding property="password" />
            </binder>
            <on-entry>
                <set name="viewScope.commandName" value="'credentials'" />
            </on-entry>
            <transition on="submit" bind="true" validate="true" to="realSubmit">
                <set name="flowScope.credentials" value="credentials" />
                <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
            </transition>
        </view-state>
        
    
        <action-state id="realSubmit">
            <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
            <transition on="warn" to="warn" />
            <transition on="success" to="sendTicketGrantingTicket" />
            <transition on="error" to="viewLoginForm" />
        </action-state>
    

    接著從cas\WEB-INF\cas-servlet.xml(如下)可看到authenticationViaFormAction的路徑,相對路徑為cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\web\flow
         <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
            p:centralAuthenticationService-ref="centralAuthenticationService"
            p:warnCookieGenerator-ref="warnCookieGenerator" />
    

    修改cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\web\flow\AuthenticationViaFormAction.java,加上圖形驗證碼的判斷,僅列出修改的submit() method,新增 line-8 ~ 20 取得 User 輸入的驗證碼並比對是否輸入正確,將 line-42 ~ 43 置換成 line-46 ~ 55 加上驗證碼輸入正確與否的流程判斷,。
    import com.octo.captcha.service.CaptchaServiceException;
    import org.jasig.cas.captcha.CaptchaServiceSingleton;
    ....
    
        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 {
                isResponseCorrect = CaptchaServiceSingleton.getInstance().validateResponseForID(captchaId, enterCaptchaId);
            } catch (CaptchaServiceException e) {
                //should not happen, may be thrown if the id is not valid
                logger.error( "Captcha Code Validation Failed", e );
            }
            // for captcah input and expect value end
    
            if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) {
    
                try {
                    final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials);
                    WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
                    putWarnCookieIfRequestParameterPresent(context);
                    return "warn";
                } catch (final TicketException e) {
                    if (e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass())) {
                        populateErrorsInstance(e, messageContext);
                        return "error";
                    }
                    this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);
                    }
                }
            }
    
            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
                    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";
            }
        }
    

  9. 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此路徑下。
  10. mvn clean package -Dmaven.test.skip=true

  11. 下載jcaptcha-1.0-bin.zip,解壓縮後將 jcaptcha-1.0-all.jar 複製到cas\WEB-INF\lib此路徑下。

  12. 修改 cas\WEB-INF\web.xml 加上 JCaptcha Servlet mapping。
  13.     <!-- jCaptcha Servlet -->
        <servlet>
            <servlet-name>jcaptcha</servlet-name>
            <servlet-class>org.jasig.cas.captcha.ImageCaptchaServlet</servlet-class>
            <load-on-startup>0</load-on-startup>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>jcaptcha</servlet-name>
            <url-pattern>/jcaptcha</url-pattern>
        </servlet-mapping>
    

  14. 修改登入頁 cas\WEB-INF\view\jsp\default\ui\casLoginView.jsp 加上圖形驗證碼的顯示。
  15.     <div class="row fl-controls-left">
            <img src="jcaptcha" />
            <input type="text" name="j_captcha_response" cssClass="required" cssErrorClass="error" size="25" />
        </div>
    

  16. 啟動 Tomcat ,連結至CAS Login Page,可看到如下畫面,經測試須正確輸入圖形驗證碼才可登入成功。

下一節依需求修改驗證碼的干擾方式,JCaptcha 官網有提供很多種方式以下所述只是其中一種,單看需求而訂。

Modify JCaptcha Background

  • cas-server-3.4.3\cas-server-core\src\main\java\org\jasig\cas\captcha\CaptchaServiceSingleton.java 修改如下,照之前的步驟 build & copy cas-server-core-3.4.3.jar
  • package org.jasig.cas.captcha;
    
    import com.octo.captcha.service.image.ImageCaptchaService;
    import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
    import com.octo.captcha.engine.image.ListImageCaptchaEngine;
    import com.octo.captcha.component.word.wordgenerator.WordGenerator;
    import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
    import com.octo.captcha.component.image.color.RandomRangeColorGenerator;
    import com.octo.captcha.component.image.color.SingleColorGenerator;
    import com.octo.captcha.component.image.textpaster.TextPaster;
    import com.octo.captcha.component.image.textpaster.RandomTextPaster;
    import com.octo.captcha.component.image.backgroundgenerator.FileReaderRandomBackgroundGenerator;
    import com.octo.captcha.component.image.fontgenerator.FontGenerator;
    import com.octo.captcha.component.image.fontgenerator.RandomFontGenerator;
    import com.octo.captcha.component.image.wordtoimage.WordToImage;
    import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
    import com.octo.captcha.image.gimpy.GimpyFactory;
    
    import java.awt.Font;
    import com.octo.captcha.service.captchastore.FastHashMapCaptchaStore;
    import java.awt.Color;
    
    
    public class CaptchaServiceSingleton {
     private static ImageCaptchaService instance =
           new DefaultManageableImageCaptchaService(new FastHashMapCaptchaStore(),
                                                    new MyImageCaptchaEngine(), 
                                                    180,
                                                    1000,
                                                    750);
    
     public static ImageCaptchaService getInstance() {
      return instance;
     }
        
     private static class MyImageCaptchaEngine extends ListImageCaptchaEngine {
      protected void buildInitialFactories() {
       WordGenerator wgen = new RandomWordGenerator("abcdefghijklmnopqrstuvwxyz0123456789");
       RandomRangeColorGenerator cgen = new RandomRangeColorGenerator(
       new int[] {0, 100},
       new int[] {0, 100},
       new int[] {0, 100});
       SingleColorGenerator wordColor = new SingleColorGenerator(Color.BLACK);
       TextPaster textPaster = new RandomTextPaster(new Integer(6), new Integer(6), wordColor, true);
    
       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);
    
       WordToImage wordToImage = new ComposedWordToImage(fontGenerator, background, textPaster);
       this.addFactory(new GimpyFactory(wgen, wordToImage));
     
      }
     }
    }
    
  • 產出圖形驗證碼的背景圖(jpg),放置於CaptchaServiceSingleton.java所設定的路徑%TOMCAT_HOME%/webapps/cas/images/backgrounds/,此 sample 只產出二張圖,一張黑色直條紋,一張紫色橫條紋

  • 啟動 Tomcat ,連結至CAS Login Page,可看到如下畫面,背景圖可自行放置數量不限,會依亂數顯示,如有放置新的圖 Tomcat 須重啟才能顯示新的背景圖。



相關設定可參考:
JA-SIG CAS Single-Sign On - 1
SSO demonstration for a quick start with CAS
CAS on Windows Quick Setup Guide
JA-SIG CAS - 3-1: 增加Captcha功能
JCAPTCHA integration with CAS

沒有留言:

張貼留言