Help

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.

19 comments:
 
07. Dec 2007, 01:10 CET | Link
Chistophe Vanfleteren

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.

ReplyQuote
 
07. Dec 2007, 01:15 CET | Link
Christophe Vanfleteren

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 :( )

 
07. Dec 2007, 04:13 CET | Link

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.

 
07. Dec 2007, 04:35 CET | Link
Forgot to say that implementing the captcha in your application looks really simple using seam.

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 ;-)

 
07. Dec 2007, 09:59 CET | Link

Hm.... i can't read this captcha. I think it will confuse users ;-)

 
07. Dec 2007, 17:00 CET | Link

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.

 
07. Dec 2007, 17:12 CET | Link

Huh? I had zero problem deciphering any of the three images...

 
09. Dec 2007, 09:45 CET | Link
HUssein Baghdadi

So, characters in grey aren't supposed to be entered by the end user? What about CAPTCHA support for disabled users (accessibility)?

 
09. Dec 2007, 10:44 CET | Link
So, characters in grey aren't supposed to be entered by the end user?

Eh, what? All characters need to be entered.

What about CAPTCHA support for disabled users (accessibility)?

You could implement an audio captcha the same way and use the browser id to detect a screenreader. Please submit to the Seam JIRA.

 
25. Feb 2008, 00:15 CET | Link

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.

 
25. Feb 2008, 00:20 CET | Link
Robert

Ignore this.. It changes on incorrect input Guess it's getting late :D

 
08. Jul 2008, 10:13 CET | Link

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.

 
05. Aug 2008, 17:06 CET | Link

Great, but

@CaptchaResponse(message = "#{messages['lacewiki.label.VerificationError']}") 

does not seem to have any effect. I get incorrect response (the hardwired message from Gavin's code) no matter what lacewiki.label.VerificationError is set,

 
18. Feb 2009, 09:21 CET | Link
bbb

I 've got asame problem!

It's a hard coded message

 
27. Mar 2009, 11:52 CET | Link
wolfgang wache | er75(AT)gmx.at
Hi!

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
 
27. Mar 2009, 13:55 CET | Link
wolfgang | er75(AT)gmx.at

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!

 
29. Mar 2009, 20:39 CET | Link

Are we talking about using SEAM with Hibernate here??

 
30. Mar 2009, 13:32 CET | Link
Shervin Asgari
Same here.
 
31. Jul 2009, 21:14 CET | Link

I have created a fine java based katpcha solution that is fast and works well.

Post Comment