You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tapestry.apache.org by Howard <hl...@gmail.com> on 2009/12/04 03:08:00 UTC

[Tapestry Central] Tapestry and Kaptcha

Another bit of interesting work I did, for another client, was to
implement a CAPTCHA system. I chose the library Kaptcha and built
services and components around it.
If you follow the documentation for Katpcha, you'll see that you're
supposed to configure it inside web.xml and add a servlet. That's not
the Tapestry way, especially for something that will likely be split
off into its own library at some point and there's no reason that all
the necessary plumbing can't occur within the context of Tapestry's
APIs.
The essence of a CAPTCHA is two fold: first, a secret string is
generated on the server side. On the client-side, an image and a text
field are displayed. The image is a distorted version of the secret
text. The user must type the text ... humans being better able to pull
meaning out of the distortion than any typical program.
Back on the server side, we compare what the user entered against the
secret string.
I broke the implementation up into three pieces:
- A Tapestry service to handle generating the secret string and the
image
- A Tapestry component to display the image
- A second component to handle the text field
In practice, all it takes to use this is the following:
<t:kaptchaimage t:id="kaptcha"/> <br/> <t:kaptchafield image="kaptcha"/>

The two components work together to select the secret word, display the
image, and validate that the user has entered the expected value.
Let's look at how this all comes together. KaptchaProducer Service
Kaptcha includes an interface, Producer, that has most of what I want:
package com.google.code.kaptcha; import
java.awt.image.BufferedImage; /** * Responsible for creating captcha
image with a text drawn on it. */ public interface Producer { /** *
Create an image which will have written a distorted text. * * @param
text * the distorted characters * @return image with the text */
BufferedImage createImage(String text); /** * @return the text to be
drawn */ String createText(); }

I extended this to add methods for determining the width and height of
the captcha image:
package com.myclient.services.kaptcha; import
com.google.code.kaptcha.Producer; /** * Extension of KatpchaProducer
that exposes the images width and height (in * pixels). * */ public
interface KaptchaProducer extends Producer { int getWidth(); int
getHeight(); }

My implementation is largely a wrapper around Kaptcha's default
implementation:
package com.myclient.services.kaptcha; import
java.awt.image.BufferedImage; import java.util.Map; import
java.util.Properties; import
com.google.code.kaptcha.impl.DefaultKaptcha; import
com.google.code.kaptcha.util.Config; public class KaptchaProducerImpl
implements KaptchaProducer { private final DefaultKaptcha producer;
private final int height; private final int width; public
KaptchaProducerImpl(Map<String, String> configuration) { producer = new
DefaultKaptcha(); Config config = new
Config(toProperties(configuration)); producer.setConfig(config); height
= config.getHeight(); width = config.getWidth(); } public int
getHeight() { return height; } public int getWidth() { return width; }
public BufferedImage createImage(String text) { return
producer.createImage(text); } public String createText() { return
producer.createText(); } private static Properties
toProperties(Map<String, String> map) { Properties result = new
Properties(); for (String key : map.keySet()) { result.put(key,
map.get(key)); } return result; } }

What's all the business with the Map<String, String> configuration?
That's a Tapestry IoC mapped configuration, that allows us to extend
the configuration of the Kaptcha Producer ... say, to change the width
or height or color scheme.
Note that this way my choice, to have a centralized text and image
producer, so that all CAPTCHAs in the application would have a uniform
look and feel. Another alterntiave would have been to have the
KaptchaImage component (described shortly) have its own instance of
DefaultKaptcha, with parameters to control its configuration.
KaptchaImage Component
So with this service in place, how do we generate the image? This is
done in three steps:
- Selecting a secret word and storing it persistently in the session
- Rendering an <img> element, including a src attribute
- Providing an image byte stream when asked by the browser
package com.myclient.components; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.OutputStream; import
javax.imageio.ImageIO; import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.Link; import
org.apache.tapestry5.MarkupWriter; import
org.apache.tapestry5.annotations.Persist; import
org.apache.tapestry5.annotations.SupportsInformalParameters; import
org.apache.tapestry5.ioc.annotations.Inject; import
org.apache.tapestry5.services.Response; import
com.myclient.services.kaptcha.KaptchaProducer; /** * Part of a Captcha
based authentication scheme; a KaptchaImage generates a new * text
image whenever it renders and can provide the previously * rendred text
subsequently (it is stored persistently in the session). * * The
component renders an <img> tag, including width and height *
attributes. Other attributes come from informal
parameters. */ @SupportsInformalParameters public class KaptchaImage
{ @Persist private String captchaText; @Inject private KaptchaProducer
producer; @Inject private ComponentResources resources; @Inject private
Response response; public String getCaptchaText() { return
captchaText; } void setupRender() { captchaText =
producer.createText(); } boolean beginRender(MarkupWriter writer) {
Link link = resources.createEventLink("image");
writer.element("img", "src", link.toAbsoluteURI(), "width",
producer.getWidth(), "height", producer.getHeight());
resources.renderInformalParameters(writer); writer.end(); return
false; } void onImage() throws IOException { BufferedImage image =
producer.createImage(captchaText); response.setDateHeader("Expires",
0); response.setHeader("Cache-Control", "no-store, no-cache,
must-revalidate"); response.setHeader("Cache-Control", "post-check=0,
pre-check=0"); response.setHeader("Pragma", "no-cache"); OutputStream
stream = response.getOutputStream("image/jpeg");
ImageIO.write(image, "jpg", stream); stream.flush(); stream.close(); } }

This component (which has no template) has two render phase methods. In
setupRender() we choose the secret word; since the captchaText field
has the @Persist annotation, it's value will be stored in the session.
Inside beginRender() is where we render the image. We also generate a
callback link for an event named "image". The URL Tapestry generates
will identify the page and component within the page, as well as this
event name.
Notice how we use the getWidth() and getHeight() extensions on the
service interface to set these attributes of the <img> tag.
Later, the browser will send a request for the event, and the onImage()
event handler method will be invoked. This is where we get the image
bytestream from the service and pump it down to the client. As you can
see, we set a bunch of header values to ensure that the browser won't
cache the image. KaptchaField Component
The last part of the overall puzzle is the text field. Again, there are
two main responsibilities:
- Rendering out the text field (when rendering)
- Validating that the user entered the correct secret text (when the
form is submitted)
package com.myclient.components; import
org.apache.tapestry5.BindingConstants; import
org.apache.tapestry5.ComponentResources; import
org.apache.tapestry5.FieldValidator; import
org.apache.tapestry5.MarkupWriter; import
org.apache.tapestry5.ValidationTracker; import
org.apache.tapestry5.annotations.BeginRender; import
org.apache.tapestry5.annotations.Environmental; import
org.apache.tapestry5.annotations.Parameter; import
org.apache.tapestry5.annotations.SupportsInformalParameters; import
org.apache.tapestry5.corelib.base.AbstractField; import
org.apache.tapestry5.ioc.Messages; import
org.apache.tapestry5.ioc.annotations.Inject; import
org.apache.tapestry5.services.FieldValidatorSource; import
org.apache.tapestry5.services.Request; /** * Field paired with a {@link
KaptchaImage} to ensure that the user has provided * the correct
value. * */ @SupportsInformalParameters public class KaptchaField
extends AbstractField { /** * The image output for this field. The
image will display a distorted text * string. The user must decode the
distorted text and enter the same value. */ @Parameter(required = true,
defaultPrefix = BindingConstants.COMPONENT) private KaptchaImage
image; @Inject private Request request; @Inject private Messages
messages; @Inject private ComponentResources resources; @Environmental
private ValidationTracker validationTracker; @Inject private
FieldValidatorSource fieldValidatorSource; @Override public boolean
isRequired() { return true; } @BeginRender boolean
renderTextField(MarkupWriter writer) {
writer.element("input", "type", "password", "id",
getClientId(), "name", getControlName(), "value", "");
resources.renderInformalParameters(writer); FieldValidator
fieldValidator = fieldValidatorSource.createValidator(
this, "required", null); fieldValidator.render(writer); writer.end();
return false; } @Override protected void processSubmission(String
elementName) { String userValue = request.getParameter(elementName); if
(image.getCaptchaText().equals(userValue)) return;
validationTracker.recordError(this,
messages.get("incorrect-captcha")); } }

The renderTextField() method is largely straight forward: by the time
this is invoked, the unique clientId and controlName will already have
been set for the field. The only trick here is to create some
client-side validation to enforce that the field is required.
Later the form will be submitted by the user and the
processSubmission() method is invoked. It asks the KaptchaImage for the
stored text and compares it to the user's input. If invalid, then an
error message is recorded, associated with the field. The actual error
text is stored in the component's message catalog. Conclusion
Tapestry's approach is quite often about integration: integration of
component code with other resources (such as templates or message
catalogs), integration of components with other components, and
integration of components with services. Here we get to see how a
singleton service can be used by any number of components, how two
components can be connected together, and how easy it is to provide
logic both when rendering a page and on related requests from the
client.

--
Posted By Howard to Tapestry Central at 12/03/2009 06:08:00 PM