1

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!

guilhebl
  • 6,652
  • 8
  • 40
  • 62

2 Answers2

3

The browser is caching the response. Your attempt to avoid this is incomplete and incorrect:

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

Please refer How to control web page caching, across all browsers? for the proper set:

response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
response.setDateHeader("Expires", 0); // Proxies.

Further, to make it more robust, add a query string with the current timestamp in millis to the image URL. Here's an example provided that you've a java.util.Date instance as managed bean with the name now:

<h:graphicImage id="capImg" value="#{request.contextPath}/../captcha.jpg?#{now.time}" />

(please note that I also simplified the way to get the request context path, I only don't understand how it's useful if you go to domain root by ../ anyway)

Community
  • 1
  • 1
BalusC
  • 992,635
  • 352
  • 3,478
  • 3,452
  • 1
    OP is basing his work on [this blog post](http://javahunter.wordpress.com/2010/09/25/integrating-captcha-in-jsf-2-0/) from 2010, which, apart from having all caching problems you stated, as well abuses session as a holder of captcha value. – skuntsel Mar 27 '13 at 15:49
  • 1
    @skuntsel: I see. Session scope isn't exactly abused, it should be stored in the session scope anyway, but it is indeed used the wrong way. E.g. if you open 2 browser tabs with the page with the captcha and go back to the 1st tab after having opened the 2nd tab, then the submit will fail because it expects the value of the captcha in 2nd tab. The solution is to use an autogenerated key which is stored in a hidden input field (or in the view scope, in JSF terms), or just to use an existing and sane JSF component instead :) PrimeFaces has one. – BalusC Mar 27 '13 at 15:58
  • Thanks for the input, BalusC I tried your 1st suggestion and it still happens to render twice, as the graphicImage value invokes twice the doGet() method of CaptchaServlet, overwriting the CAPTCHA_KEY session object but not re-rendering the img in screen. I'm trying to avoid having to include PrimeFaces just to use this single component, I'll try to build one based on a different approach. – guilhebl Mar 27 '13 at 20:43
  • I tried using JCaptcha as per example of https://jcaptcha.atlassian.net/wiki/display/general/5+minutes+application+integration+tutorial - Still happens the same issue, the doGet() method is called twice when I navigate to the page, thus rendering 2 different Catpcha Response Keys, overwriting the 1st one, but not re-rendering the image in the page for the new generated captcha. Strange that only in Chrome it works fine, I'm still resistant on using PrimeFaces as it uses reCaptcha and works only online. – guilhebl Mar 28 '13 at 18:06
  • I found a workaround for this using a HashMap to store (id, captchaImage) and service them in case it's the same request and flushing the HashMap whenever a person hit's a button that leads to a page containing a Captcha, I know this isn't the desired solution but for the meanwhile I'll stick with this. – guilhebl Mar 28 '13 at 21:36
0

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.

guilhebl
  • 6,652
  • 8
  • 40
  • 62
  • What are the drawbacks of this approach? – guneykayim Nov 07 '14 at 17:35
  • I can't remember exactly what I was thinking on the time in terms of drawbacks I guess regarding ajax or back button handling support, not sure, but in overall it fitted well my required scenario. – guilhebl Nov 08 '14 at 01:04