JCaptcha in Liferay (multiple instances per page)
Liferay supports simpleCaptcha and reCaptcha by default. The problem is that if you want more instances of a captcha in one page (say, you have a complicated registration form for different kind of users), then none of those captcha engines will satisfy you, because they do not support it.
You can of course try a workaround, like load the captcha with ajax when it is necessary, but in some cases, this is also not a acceptable solution.
The solution described in this post will allow you one instance per portlet, because the string that we use in the session to validate the user input against the captcha image is the session id. You are free to change that and use, for example, the form name, or something else, to have multiple instances in a portlet.
So, here is a solution with another captcha engine, JCaptcha. It is relative easy to use and straight forward.
We assume you have created a liferay portlet plugin project with a MVCPortlet.
Those are the steps you have to do:
- download JCaptcha jar and put it in the lib directory of your portlet project
- create a CaptchaServiceSingleton class
- create the serveResource method in your portlet and implement it to serve the captcha image
- create an action to validate the captcha image
- create a jsp that shows the image in a form were the user can type the letters of the image shown.
More or less, that is all you have to do.
Lets have a closer look at those steps.
Download JCaptcha and copy it in the lib dir
This should be an easy thing to do. The lib directory is in
docroot/WEB-INF/lib
Currently, the file name is "jcaptcha-1.0-all.jar".
There is also one dependency: you have to add the "commons-collections.jar" in the "liferay-plugin-package.properties" file.
Create a CaptchaServiceSingleton class
JCaptcha need a singleton class for its own use. So, we create it. Lets give to this class the name "CaptchaServiceSingleton":
import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.octo.captcha.service.image.DefaultManageableImageCaptchaService; import com.octo.captcha.service.image.ImageCaptchaService; public class CaptchaServiceSingleton { public static ImageCaptchaService getInstance() { return instance; } private static final Log logger = LogFactoryUtil.getLog(CaptchaServiceSingleton.class); private static ImageCaptchaService instance = new DefaultManageableImageCaptchaService(); }
serverResource method
In the portlet class create the following method:
private static final Log logger = LogFactoryUtil.getLog(TestCaptcha.class); @Override public void serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse) throws IOException, PortletException { ThemeDisplay themeDisplay = (ThemeDisplay) resourceRequest.getAttribute(WebKeys.THEME_DISPLAY); 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 = resourceRequest.getPortletSession().getId() + StringPool.UNDERLINE + getPortletName(); logger.info("GENERATING CAPTCHA IMAGE FOR " + captchaId); // call the ImageCaptchaService getChallenge method BufferedImage challenge = CaptchaServiceSingleton.getInstance().getImageChallengeForID(captchaId, themeDisplay.getLocale()); // a jpeg encoder JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(jpegOutputStream); jpegEncoder.encode(challenge); } catch (IllegalArgumentException e) { logger.error(e.getMessage(), e); } catch (CaptchaServiceException e) { logger.error(e.getMessage(), e); } catch (Exception e) { logger.error(e.getMessage(), e); } catch (Throwable e) { logger.error(e.getMessage(), e); } captchaChallengeAsJpeg = jpegOutputStream.toByteArray(); HttpServletResponse response = PortalUtil.getHttpServletResponse(resourceResponse); // flush it in the response response.setHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); response.setContentType(ContentTypes.IMAGE_JPEG); ServletResponseUtil.write(response, captchaChallengeAsJpeg); }
validateCaptcha method
We will create a private method for the captcha validation and an action method that will be called on the submission of the form. The validation method will use the singleton calls we created on the first step.
public void validateCaptcha(ActionRequest request, ActionResponse response) { if (validateCaptcha(request)) { SessionMessages.add(request, "Everything went fine"); } else { SessionErrors.add(request, "Input is wrong. Please try again"); } } public boolean validateCaptcha(PortletRequest request) { Boolean isResponseCorrect = Boolean.FALSE; // remenber that we need an id to validate! String captchaId = request.getPortletSession().getId() + StringPool.UNDERLINE + getPortletName(); // retrieve the response String response = ParamUtil.getString(request, "j_captcha_response"); // Call the Service method logger.info("captchaId:" + captchaId); logger.info("response:" + response); try { isResponseCorrect = CaptchaServiceSingleton.getInstance().validateResponseForID(captchaId, response); } catch (CaptchaServiceException e) { // should not happen, may be thrown if the id is not valid logger.error(e.getMessage(), e); } return isResponseCorrect; }
view.jsp file
So, now, the view... In our "view.jsp" file we put the following code:
  <img src="${serveCaptchaUrl}" alt="" /> <input type="text" name="j_captcha_response" value="" /> <button type="submit">Submit</button>
And that's all.
If you need a refresh link you can add a link that runs a javascript function that removes and add the src of the captcha image. For example, using jQuery, this method could be like this:
$("# refreshCaptcha").click(function(){ $("# captchaImg").attr("src", ""); $("# captchaImg").attr("src", "${serveCaptchaUrl}"); });