Seam offers some basic infrastructure for CAPTCHA creation and validation, so all you have to do if you want to add CAPTCHA validation to a form is add a single form field and show the picture with <h:graphicImage/>. The only built-in implementation we shipped with Seam 1.x and 2.0 was based on JCaptcha, but you could easily extend it and do your own question/answer thing. This is actually what I did and you can see my simplified math question CAPTCHA if you try to post a comment to this entry.
I'm not the only Seam user who had problems with JCaptcha (search the Seam forum if you want to know more, basically: it's over-engineered, needs seconds to startup, sometimes needs seconds to render an image, the default image generators are hard to read but CAPTCHAs are still easy to break, etc). So Gavin wrote a new Captcha implementation we could ship with Seam 2.0.1. This is in Seam CVS now and not released, so ignore this blog entry if you are not a Seam CVS user and come back to it when we release 2.0.1.
The usage scenario is still the same, add an input field and a picture to your form:
<s:validateAll>
<h:inputText size="6" maxlength="6" required="true" id="verifyCaptcha" value="#{captcha.response}"/>
<h:graphicImage value="/seam/resource/captcha"/>
</s:validateAll>
By default Gavins implementation only renders a simple math question as an image with no obfuscation at all. So I extended the built-in classes. This is how my generated CAPTCHA questions look like:
The trick is to tell the user to ignore any circles
- and to never use any characters that look like circles (zero, o, O). I don't think people will have issues deciphering these characters, I've been trying myself a few dozen times during testing and failed only once or twice. I think that this CAPTCHA is quite difficult to break automatically though, the grey shades used for obfuscation and real text are the same and circles really destroy the original shape of the characters. And if it's broken, we can simply add more aggressive circles in small incremental steps or increase the rotation range of the characters.
Another thing that makes CAPTCHAs less painful is to store the data in the HTTP session, so that if a user entered a captcha once on your site, you don't require it to be entered a second time. But that's all built-in with the new Seam Captcha stuff.
Here is my custom code for the image generation:
package org.jboss.seam.wiki.core.captcha;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.captcha.Captcha;
import org.jboss.seam.captcha.CaptchaResponse;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
@Name("org.jboss.seam.captcha.captcha")
@Scope(ScopeType.SESSION)
@Install(precedence = Install.APPLICATION)
public class WikiCaptcha extends Captcha {
Color backgroundColor = new Color(0xf5,0xf5, 0xf5);
Font textFont = new Font("Arial", Font.PLAIN, 25);
int charsToPrint = 6;
int width = 120;
int height = 25;
int circlesToDraw = 4;
float horizMargin = 20.0f;
double rotationRange = 0.2;
String elegibleChars = "ABDEFGHJKLMRSTUVWXYabdefhjkmnrstuvwxy23456789";
char[] chars = elegibleChars.toCharArray();
@Override
@Create
public void init() {
super.init();
StringBuffer finalString = new StringBuffer();
for (int i = 0; i < charsToPrint; i++) {
double randomValue = Math.random();
int randomIndex = (int) Math.round(randomValue * (chars.length - 1));
char characterToShow = chars[randomIndex];
finalString.append(characterToShow);
}
setChallenge(finalString.toString());
setCorrectResponse(finalString.toString());
}
@Override
public BufferedImage renderChallenge() {
// Background
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) bufferedImage.getGraphics();
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);
// Some obfuscation circles
for (int i = 0; i < circlesToDraw; i++) {
int circleColor = 80 + (int)(Math.random() * 70);
float circleLinewidth = 0.3f + (float)(Math.random());
g.setColor(new Color(circleColor, circleColor, circleColor));
g.setStroke(new BasicStroke(circleLinewidth));
int circleRadius = (int) (Math.random() * height / 2.0);
int circleX = (int) (Math.random() * width - circleRadius);
int circleY = (int) (Math.random() * height - circleRadius);
g.drawOval(circleX, circleY, circleRadius * 2, circleRadius * 2);
}
// Text
g.setFont(textFont);
FontMetrics fontMetrics = g.getFontMetrics();
int maxAdvance = fontMetrics.getMaxAdvance();
int fontHeight = fontMetrics.getHeight();
float spaceForLetters = -horizMargin * 2 + width;
float spacePerChar = spaceForLetters / (charsToPrint - 1.0f);
char[] allChars = getChallenge().toCharArray();
for (int i = 0; i < allChars.length; i++ ) {
char charToPrint = allChars[i];
int charWidth = fontMetrics.charWidth(charToPrint);
int charDim = Math.max(maxAdvance, fontHeight);
int halfCharDim = (charDim / 2);
BufferedImage charImage = new BufferedImage(charDim, charDim, BufferedImage.TYPE_INT_ARGB);
Graphics2D charGraphics = charImage.createGraphics();
charGraphics.translate(halfCharDim, halfCharDim);
double angle = (Math.random() - 0.5) * rotationRange;
charGraphics.transform(AffineTransform.getRotateInstance(angle));
charGraphics.translate(-halfCharDim, -halfCharDim);
int charColor = 60 + (int)(Math.random() * 90);
charGraphics.setColor(new Color(charColor, charColor, charColor));
charGraphics.setFont(textFont);
int charX = (int) (0.5 * charDim - 0.5 * charWidth);
charGraphics.drawString("" + charToPrint, charX, ((charDim - fontMetrics.getAscent())/2 + fontMetrics.getAscent()));
float x = horizMargin + spacePerChar * (i) - charDim / 2.0f;
int y = ((height - charDim) / 2);
g.drawImage(charImage, (int) x, y, charDim, charDim, null, null);
charGraphics.dispose();
}
g.dispose();
return bufferedImage;
}
@CaptchaResponse(message = "#{messages['lacewiki.label.VerificationError']}")
public String getResponse() {
return super.getResponse();
}
}
(If some of the code looks familiar, you have maybe seen it here before.)
I still need to finish a few other things before I can upgrade the software running this site to the new Captcha though. So if you want to try it out, get Seam CVS and put this class into your codebase, no other configuration necessary.
You might want to make sure this captcha is as heard to break as you think it is ...
This article at CodingHorror explains that even captchas that seem hard are (quite easily) broken.
Forgot to say that implementing the captcha in your application looks really simple using seam.
I'd love to use seam in production (but not likely to happen at my firm :( )
Christian, something else we should do in the wiki is either hide email addresses behind a CAPTCHA, or else only display them to logged in users. The xxx(AT)hibernate.org stuff is a little bit easy for the bots :-)
See here for an explanation of what I mean.
One of the really killer features of Seam is how easy it is to just extend/override the built-in components by throwing a subclass in the classpath. This is the kind of thing you don't really appreciate until you get into it ;-)
Hm.... i can't read this captcha. I think it will confuse users ;-)
This captcha WILL confuse users. The third image is completely confused. The best captcha that I have seen was from google. Try to do something like that.
Huh? I had zero problem deciphering any of the three images...
So, characters in grey aren't supposed to be entered by the end user? What about CAPTCHA support for disabled users (accessibility)?
Eh, what? All characters need to be entered.
You could implement an audio captcha the same way and use the browser id to detect a screenreader. Please submit to the Seam JIRA.
Is there any way to make the captcha change when the user reloads the page? I tryed changing the ScopeType to EVENT and PAGE, but it didn't work.. Nice work, btw.
Ignore this.. It changes on incorrect input Guess it's getting late :D
Christian, i made use of this code a little over a year ago. however i have a few modifications using image filters that make it significantly more difficult for bots. shoot me an email and i will be happy to let you see what i have and contribute. i would love to see better captcha images in seam. was very disappointed with Jcaptcha and somewhat in awe of the addition bit distributed with 2.0.1. how easy was that to break with a bot? some of the code i have makes use of JAI. (for cropping) but that is easily ported to something smaller.
Great, but
@CaptchaResponse(message = "#{messages['lacewiki.label.VerificationError']}")does not seem to have any effect. I get (the hardwired message from Gavin's code) no matter what lacewiki.label.VerificationError is set,
I 've got asame problem!
It's a hard coded message
it helped to leave the blanks at the '=' sign:
@CaptchaResponse(message="#{messages['lacewiki.label.VerificationError']}")
If you then have your own
@In
private Map<String, String> messages;
and redefine your error message there. I don't know if it's a bug or a feature, but it works that way ;)
br
I have to correct myself: something different must have caused the problem. It's not reproducable and the code seems works as it is... If you also were confused about the blanks-problem as I posted before: just forget it! Sorry!
Are we talking about using SEAM with Hibernate here??
I have created a fine java based katpcha solution that is fast and works well.