Search code examples
jsf-2java-ee-6graphics2dservlet-3.0prettyfaces

JSF 2 Captcha using <h:graphicImage rendering twice for Servlet generated image value working only for Chrome


I have an issue in my application using where I have a Captcha component built as a JSF Custom Tag:

in my JavaEE 6 webapp I use: JSF 2.1 + Jboss Richfaces 4.2.3 + EJB 3.1 + JPA 2.0 + PrettyFaces 3.3.3

I have a JSF2 custom tag that is:

<tag>
    <tag-name>captcha</tag-name>
    <source>tags/captcha.xhtml</source>
</tag>  

in my XHTML page called accountEdit.xhtml I have the captcha being displayed:

                <ui:fragment rendered="#{customerMB.screenComponent.pageName eq 'create'}">
                    <div class="form_row">
                            <label class="contact"><strong>#{msg.captcha}:</strong>
                            </label>
                            <atl:captcha></atl:captcha>                     
                    </div>                                                          
                </ui:fragment>

in captcha.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:a4j="http://richfaces.org/a4j"
    xmlns:rich="http://richfaces.org/rich">

    <table border="0">
        <tr>
            <td>
            <h:graphicImage id="capImg" value="#{facesContext.externalContext.requestContextPath}/../captcha.jpg" />
            </td>
            <td><a4j:commandButton id="resetCaptcha" value="#{msg.changeImage}" immediate="true" action="#{userMB.resetCaptcha}" >
                <a4j:ajax render="capImg" execute="@this" />                
            </a4j:commandButton></td>
        </tr>
        <tr>
            <td><h:inputText value="#{userMB.captchaComponent.captchaInputText}" /></td>            
        </tr>
    </table>

</ui:composition>

in my web.xml I have configured a CaptchaServlet that handles the request for generating a captcha during runtime:

<servlet>   
    <servlet-name>CaptchaServlet</servlet-name>
    <servlet-class>com.myapp.web.common.servlet.CaptchaServlet</servlet-class>      
    <init-param>
        <description>passing height</description>
        <param-name>height</param-name>
        <param-value>30</param-value>
    </init-param>
    <init-param>
        <description>passing width</description>
        <param-name>width</param-name>
        <param-value>120</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>


<servlet-mapping>
    <servlet-name>CaptchaServlet</servlet-name>
    <url-pattern>/captcha.jpg</url-pattern>
</servlet-mapping>

My CaptchaServlet implementation:

public class CaptchaServlet extends HttpServlet {

    /**
     * 
     */
    private static final long serialVersionUID = 6105436133454099605L;

    private int height = 0;
    private int width = 0;
    public static final String CAPTCHA_KEY = "captcha_key_name";

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        height = Integer
                .parseInt(getServletConfig().getInitParameter("height"));
        width = Integer.parseInt(getServletConfig().getInitParameter("width"));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse response)
            throws IOException, ServletException {

        // Expire response
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Max-Age", 0);

        BufferedImage image = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = image.createGraphics();
        Hashtable<TextAttribute, Object> map = new Hashtable<TextAttribute, Object>();
        Random r = new Random();
        String token = Long.toString(Math.abs(r.nextLong()), 36);
        String ch = token.substring(0, 6);
        Color c = new Color(0.6662f, 0.4569f, 0.3232f);
        GradientPaint gp = new GradientPaint(30, 30, c, 15, 25, Color.white,
                true);
        graphics2D.setPaint(gp);
        Font font = new Font("Verdana", Font.CENTER_BASELINE, 26);
        graphics2D.setFont(font);
        graphics2D.drawString(ch, 2, 20);
        graphics2D.dispose();
        HttpSession session = req.getSession(true);
        session.setAttribute(CAPTCHA_KEY, ch);

        OutputStream outputStream = response.getOutputStream();
        ImageIO.write(image, "jpeg", outputStream);
        outputStream.close();
    }
}

When I run this app on Glassfish 3.1.1 when the Servlet's doGet() method is called while rendering

for the HttpServlet doGet() method that renders:

<h:graphicImage id="capImg" value="#{facesContext.externalContext.requestContextPath}/../captcha.jpg" />

doGet() renders only once for Google Chrome, thus rendering correctly.

For Firefox and IE doGet() renders twice updating the Captcha Key but not updating the painted Captcha Image on the page.

If anyone might know what could be a fix for this and why it has this behavior for Chrome different from other browsers please let me.

Thanks in advance!


Solution

  • I found a solution for this, is not the optimal solution but it works, here it goes:

    captcha.xhtml

    <table border="0">
        <tr>
            <td>
                <h:graphicImage url="#{request.contextPath}/../jcaptcha"/>
            </td>
            <td>
                <input type='text' name='j_captcha_response' value='' />
            </td>
        </tr>
    </table>
    

    CaptchaServlet doGet method:

        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.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-cache, no-store, must-revalidate"); // HTTP 1.1.
            httpServletResponse.setHeader("Pragma", "no-cache");
            httpServletResponse.setDateHeader("Expires", 0);
            httpServletResponse.setContentType("image/jpeg");
            ServletOutputStream responseOutputStream =
                    httpServletResponse.getOutputStream();
            responseOutputStream.write(captchaChallengeAsJpeg);
            responseOutputStream.flush();
            responseOutputStream.close();
        }
    

    created CaptchaServiceRequestSingleton.java

        package com.myapp.web.common.listener;
    
        import java.awt.image.BufferedImage;
        import java.util.HashMap;
        import java.util.Locale;
    
        import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
        import com.octo.captcha.service.image.ImageCaptchaService;
    
    public class CaptchaServiceSingleton {
    
        private static ImageCaptchaService instance = new DefaultManageableImageCaptchaService();
        private static final int MAX_CACHE_SIZE = 200;
        private static HashMap<String, BufferedImage> captchaImgCache = new HashMap<String, BufferedImage>();
    
        public static ImageCaptchaService getInstance(){
            return instance;
        }
    
        public static BufferedImage getImageChallengeForID(String id, Locale locale) {
            if (captchaImgCache.containsKey(id)) {
                return captchaImgCache.get(id);
            } else {
                BufferedImage bImage = instance.getImageChallengeForID(id, locale);
    
                // if limit reached reset captcha cache
                if (captchaImgCache.size() > MAX_CACHE_SIZE) {
                    captchaImgCache = new HashMap<String, BufferedImage>();
                }
    
                captchaImgCache.put(id, bImage);
                return bImage;
            }
        }
    
        public static void resetImageChallengeForID(String id) {        
            if (captchaImgCache.containsKey(id)) {      
                captchaImgCache.remove(id);
            }               
        }
    
    }
    

    when clicking on "Create Account" button Captcha is reset:

    CustomerMB.openCreateCustomerAccount():

    public String openCreateCustomerAccount() {
        customerAccountEditVO = new CustomerAccountVO();
        screenComponent.setPageName(NameConstants.CREATE);
        getUserMB().resetCaptcha();
        return null;
    }
    

    in UserMB.resetCaptcha():

    public String resetCaptcha() {
        CaptchaServiceSingleton.resetImageChallengeForID(JSFUtil.getRequest().getRequestedSessionId());     
        return null;
    }
    

    Perhaps it's not the perfect solution but at least it's working for all Browsers.