Snipsnap Developing: Integrating JCaptcha With SnipSnap 
Like I said a few days ago I wanted to open once again the user registration on the blog, although I also wanted - or at least try - to block spam bots that crawled on this blog last October.
My idea was to somehow implement a "write what's in the image" system, which are better know as
CAPTCHA, a fancy acronym for Completely Automated Public Turing test to tell Computers and Humans Apart. I was pretty sure such kind of system already existed so I googled for it and found a java implementation called
JCAPTCHA (why didn't I just tried that on the 1st place?).
I must say that the guys from JCAPTCHA did an excellent job on their documentation, the
5 minute application integration tutorial is a simple and comprehensive guide and definitely was due to it that I implemented JCAPTCHA so easily with SnipSnap.
Keep in mind that my implementation is not the only one and probably not perfect. Discussions about other ways or why I've done it this way are always welcomed.
So shall we get our hands dirty with some code?
Before doing anything else you'll have to download jcaptcha
library and put it in the snipsnap lib directory!
The second thing is to create a new java package called ora.snipsnap.JCaptcha.
In that directory you'll create two different classes:
- JCatpchaSingleton
- JCatpchaServlet
I know it's discussable to have a servlet here instead of org.snipsnap.net package or create a org.snipsnap.net.jcaptcha package, well there were only two classes and I thought there would be no problem at all if they were together.
JCaptchaSingleton, is as the name says a
singleton and it's the Snipsnap entry point for JCaptcha engine core, the code is as simple as the following:
import com.octo.captcha.service.image.ImageCaptchaService;
import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
public class CaptchaServiceSingleton {
private static ImageCaptchaService instance =
null;
private CaptchaServiceSingleton();
public static ImageCaptchaService getInstance(){
if(instance==
null) {
instance =
new DefaultManageableImageCaptchaService();
}
return instance;
}
}
This is mainly the code given at the JCAPTCHA tutorial with few modifications - the constructor became private so that anyone actually tries to instantiate the class, and the logic of getInstance method.
The
servlet is also pretty simple, here is the code:
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;
ByteArrayOutputStream jpegOutputStream =
new ByteArrayOutputStream();
try {
String captchaId = httpServletRequest.getSession().getId();
BufferedImage challenge =
JCaptchaSingleton.getInstance().getImageChallengeForID(captchaId,
httpServletRequest.getLocale());
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();
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();
}
}
The servlet has no code changes from the tutorial, so don't credit me in any way for it's code.
Now we have to change the registration process. Such process is in the
NewUserServlet class, located in the org.snipsnap.net package.
First we had a new error string, regarding a mismatch in the word entered and image.
private final static String ERR_WRONG_IMAGE = "login.wrong.image";
Now add a private method within the class which will be responsible to tell if the written word is the same as the image or not:
private boolean isResponseCorrect(HttpServletRequest request) {
String captchaId = request.getSession().getId();
String response = request.getParameter(
"j_captcha_response");
boolean isResponseCorrect =
false;
try {
isResponseCorrect = JCaptchaSingleton.getInstance().validateResponseForID(captchaId,
response);
}
catch (CaptchaServiceException e) {
e.printStackTrace();
}
return isResponseCorrect;
}
Finally in the
doPost method find the following code,
/* … */
if (!config.deny(Configuration.APP_PERM_REGISTER)) {
String login = request.getParameter(
"login");
String email = request.getParameter(
"email");
String password = request.getParameter(
"password");
String password2 = request.getParameter(
"password2");
login = login !=
null ? login :
"";
email = email !=
null ? email :
"";
if (request.getParameter(
"cancel") ==
null) {
/* … */
Becomes,
/* … */
if (!config.deny(Configuration.APP_PERM_REGISTER)) {
String login = request.getParameter(
"login");
String email = request.getParameter(
"email");
String password = request.getParameter(
"password");
String password2 = request.getParameter(
"password2");
login = login !=
null ? login :
"";
email = email !=
null ? email :
"";
if (request.getParameter(
"cancel") ==
null) {
if(!isResponseCorrect(request)) {
errors.put(
"login",ERR_WRONG_IMAGE);
sendError(session, errors, request, response);
return;
}
/* … */
Now that you have the two classes done and edited the servlet that handles registration you
only need to take care of the mappings, the jsp, the localization and the build process. It sounds much, but it's not!
MappingsYou have to edit the file web.xml-tmpl in the directory
src/apps/default/WEB-INF in that file you have to add the following lines:
<servlet>
<servlet-name>jcaptcha</servlet-name>
<servlet-class>org.snipsnap.jcaptcha.JCaptchaServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jcaptcha</servlet-name>
<url-pattern>/jcaptcha</url-pattern>
</servlet-mapping>
JSPYou have to edit the register.jsp, which is in the
src/apps/default/ directory and add the following table line to the existing table:
<tr <c:if test="${errors'letters' != null}">class="error-position"</c:if>>
<td>
<fmt:message key="login.letters"/>
</td>
<td>
<input type='text' name='j_captcha_response' value=''></td>
<td><img src="jcaptcha"></td>
</td>
</tr>
LocalizationAccess the internationalization directory at
src/apps/default/WEB-INF/classes/i18n and edit at least the message resource file that you using, most probably
messages_en.properties and enter the value pair
login.letters=Please enter the letters<br/> you see in the imageBuild ProcessYou have to add the jcaptcha library in the following places of the
build.xml:
- Add it to server.classpath
- Add it to app.classpath
- In the jar-server task, include it in the fileset that will generate the jar.
- In the snipsnap-war task, add it to the file list that are being copied into WEB-INF/lib library.
FinishingNow you only need to recompile snipsnap and redeploy it and you'll see a image coming up on the registration interface. And that will only allow you to register
if the letters that have been typed are the same in the image.
You can see a fine example of integration here in this blog, just check my
login page.
Questions and suggestions are always welcomed!
Happy coding!
I've uploaded snipsnap jar, although keep in mind that other modifications in the source code are made. All the modifications I've done are documented in the blog and since you've already read my posts I know you're aware of them.