JCaptcha – Generación de Captchas en Java

2
19446

JCaptcha – Generación de Captchas en Java

Índice de contenidos.

1. Introducción

En este tutorial veremos la librería JCaptcha. Se trata de una librería opensource para generación de captchas en Java. En todo formulario web que pida al usuario introducir datos que posteriormente serán procesados deberíamos introducir una imagen
de tipo Captcha que nos asegure que quién está enviando el formulario es un humano y no una máquina. Si no añadimos esta característica no podríamos evitar que un robot o programa
automatizado pudiera darse de alta repetidas veces en nuestra web, participar en encuestas, etc.

JCaptcha nos aporta las herramientas necesarias para generar las imágenes que utilizaremos para validar nuestros formularios proporcionándonos además la personalización
de la imagen permitiéndonos cambiar su tamaño, apariencia, fuente, colores, etc.

Otra de las grandes ventajas de esta librería es que se encuentra en los repositorios públicos de Maven por lo que con añadir la dependencia en nuestro pom.xml bastaría y por
otro lado se integra con Spring por lo que su configuración es muy sencilla.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Asus G50Vseries (Core Duo P8600 2.4GHz, 4GB RAM, 320 GB HD).
  • Sistema operativo: Windows 7
  • ICEFaces 1.8.2
  • JCaptcha 1.0
  • Maven 2.2.1
  • Spring 2.5.6
  • Framework wuija 1.9

3. pom.xml

Para proyectos con Maven basta con añadir la dependencia en el pom.xml. Suponemos que en el pom.xml ya tenemos configuradas las dependencias a Spring, JSF, ICEFaces, etc.


    com.octo.captcha
    jcaptcha
    1.0

4. Configuración básica de JCaptcha con Spring

Existen varias formas de configurar JCaptcha en un proyecto web. Por un lado podemos desplegar un Servlet (SimpleImageCaptchaServlet) que atiende las peticiones del captcha,
ver documentación aquí. Por otro lado podemos configurar
los servicios necesarios para generación y validación del captcha a través de Spring. En este tutorial seguiremos esta configuración.

En el fichero de configuración de Spring applicationContext.xml añadimos el servicio de generación de las imágenes:


Con esto ya tenemos disponible este servicio para la generación de la imagen del captcha, pero eso sí con su configuración por defecto que en muchos casos nos puede ser más que
suficiente. En nuestra aplicación debemos crear la lógica de negocio, utilizando el servicio levantado anteriormente, para crear una imagen de captcha, validar la palabra ‘oculta’
del captcha, etc. Para ello crearemos un Managed Bean de JSF gestionado por String mediante la anotación @Controller y una clase Captcha que se encargará de todo lo relacionado
con las operaciones del captcha.

La clase Captcha queda así:

package com.autentia.tutoriales.jsf;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
import com.octo.captcha.service.image.ImageCaptchaService;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;

public class Captcha {

	private final Log log = LogFactory.getLog(Captcha.class);

	private final DefaultManageableImageCaptchaService imageCaptchaService;

	private String captchaId;

	private String captchaValue;

	private byte[] captchaImage;

  /**
   * Recibe el servicio de generación de captcha y genera una nueva imagen
   */     
	public Captcha(ImageCaptchaService imageCaptchaService) {
		this.imageCaptchaService = (DefaultManageableImageCaptchaService)imageCaptchaService;

		try {
			generateCaptchaImage();
		} catch (IOException e) {
			log.error("Error generando captcha: " + e.getMessage());
		}
	}

	/**
	 * Devuelve una imagen de captcha
	 * 
	 * @return
	 * @throws IOException
	 */
	public void generateCaptchaImage() throws IOException {
		final HttpServletRequest httpServletRequest = (HttpServletRequest)FacesContext.getCurrentInstance()
				.getExternalContext().getRequest();

		// Stream de salida para la imagen del captcha
		final ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();

		// Guarda el identificador de sesión del usuario para validar el captcha
		captchaId = httpServletRequest.getSession().getId();

		// Creación de la imagen de captchacall the ImageCaptchaService getChallenge method
		final BufferedImage challenge = imageCaptchaService.getImageChallengeForID(captchaId);

		// Codificamos la imagen en JPEG
		final JPEGImageEncoder jpegEncoder = JPEGCodec.createJPEGEncoder(jpegOutputStream);
		jpegEncoder.encode(challenge);

		// Guardamos la imagen como un flujo de bytes para pintarla en pantalla
		captchaImage = jpegOutputStream.toByteArray();
	}

	/**
	 * Validación del captcha. Se recoge el identificador de la sesión del usuario y el texto introducido para validar que ha
	 * reconocido el texto del captcha.
	 * 
	 * @return
	 */
	public boolean isValidCaptcha() {
		boolean validate = false;

		try {
			validate = imageCaptchaService.validateResponseForID(captchaId, captchaValue);
		} catch (Exception e) {
			log.error("Error durante la validación del captcha: " + e.getMessage());
		}

		return validate;
	}

	public String getCaptchaValue() {
		return captchaValue;
	}

	public void setCaptchaValue(String captchaValue) {
		this.captchaValue = captchaValue;
	}

	public byte[] getCaptchaImage() {
		return captchaImage;
	}
}

El controlador de la vista UserCtrl se encargará de recuperar los datos del formulario y validar el captcha.

package com.autentia.tutoriales.jsf;

import java.io.IOException;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.context.FacesContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.stereotype.Controller;

import com.octo.captcha.service.image.ImageCaptchaService;

@Controller
@Scope("request")
public class UserCtrl {

	private static final Log log = LogFactory.getLog(UserCtrl.class);

	private String userName;

	private String welcomeMessage;

	private String welcome;

	private Captcha captcha;

	@Resource
	private MessageSourceAccessor messageSourceAccessor;

	@Resource
	private ImageCaptchaService imageCaptchaService;

	@SuppressWarnings("unused")
	@PostConstruct
	private void init() {
		welcome = messageSourceAccessor.getMessage("home.welcome");
		captcha = new Captcha(imageCaptchaService);
	}

	/**
	 * Antes de aceptar los datos del formulario se debe pasar la validación del Captcha
	 * 
	 * @return
	 */
	public String accept() {
		if (!captcha.isValidCaptcha()) {
			final String message = messageSourceAccessor.getMessage("message.captcha.error");
			addMessage(null, FacesMessage.SEVERITY_ERROR, message, message);

			newCaptcha();
			return "";
		}

		welcomeMessage = welcome + " " + userName;

		return "home";
	}

	/**
	 * Genera una nueva imagen de Captcha
	 * 
	 * @return
	 */
	public String newCaptcha() {
		try {
			welcomeMessage = null;
			captcha.generateCaptchaImage();
		} catch (IOException e) {
			log.error("Error generando captcha: " + e.getMessage());
		}
		return "home";
	}

	public static void addMessage(String clientId, Severity severity, String summary, String detail) {
		final FacesContext context = FacesContext.getCurrentInstance();

		if (context != null) {
			final FacesMessage message = new FacesMessage(severity, summary, detail);
			context.addMessage(clientId, message);
		}
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getWelcomeMessage() {
		return welcomeMessage;
	}

	public void setWelcomeMessage(String welcomeMessage) {
		this.welcomeMessage = welcomeMessage;
	}

	public Captcha getCaptcha() {
		return captcha;
	}
}

Por último la página home.jspx para sacar el formulario queda así:

< ?xml version="1.0" encoding="UTF-8"? >

< jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:c="http://java.sun.com/jstl/core"
	xmlns:ice="http://www.icesoft.com/icefaces/component">

	< ui:composition template="/WEB-INF/facelets/template/defaultLayout.jspx" >
		< ui:define name="content">	
			

#{msg['home.userData']} < ice:panelGrid columns="3"> < ice:outputLabel for="userName" value="#{msg['home.userNameLabel']}: "/> < ice:inputText id="userName" value="#{userCtrl.userName}" required="true"/> < ice:message for="userName" errorClass="error"/> < ice:outputLabel for="captcha" value="#{msg['home.captcha']}: "/> < ice:inputText id="captchaValue" value="#{userCtrl.captcha.captchaValue}" required="true"/> < ice:message for="captchaValue" errorClass="error"/> < /ice:panelGrid> < ice:graphicImage id="captcha" value="#{userCtrl.captcha.captchaImage}" />

< ice:commandButton action="#{userCtrl.newCaptcha}" value="#{msg['button.newCaptcha']}" immediate="true" /> < ice:commandButton value="#{msg['button.accept']}" action="#{userCtrl.accept}"/>

< ice:messages globalOnly="true" style="color:red;"/>

< ice:panelGroup rendered="#{not empty userCtrl.welcomeMessage}">

El resultado del formulario queda así:

Si metemos mal la palabra del captcha no se registrarán los datos del formulario por lo que impedimos acceder a la lógica de negocio:

Es importante regenerar el captcha cada vez que el usuario falla ya que de lo contrario podrían romper nuestra seguridad con un ataque de diccionario. También se le suele dar al usuario la posibilidad de generar un nuevo captcha debido a que algunas veces
resulta complicado averiguar la palabra que se esconde en el captcha.

Una vez que el usuario introduce la palabra correctamente pasamos la validación y llamaríamos a la lógica de negocio, en nuestro caso recogemos el nombre del usuario y le
damos la bienvenida.

5. Configuración avanzada de JCaptcha

Por defecto JCaptcha utiliza un diccionario de palabras en inglés pero podemos configurar esta opción añadiendo nuestro propio diccionario asociado al locale del usuario.

Como dijimos en la introducción JCaptcha nos permite personalizar completamente la generación de la imagen a través de los servicios correspondientes en cada caso. Para
insertar nuestro propio fichero de palabras debemos crear un toodlist_es (para el locale en español). También nos permite configurar los colores del texto, el tamaño de la fuente,
el fondo de la imagen así como los filtros para deformar la imagen. Un ejemplo de esta configuración sería el siguiente:

  < bean id="imageCaptchaService" class="com.octo.captcha.service.image.DefaultManageableImageCaptchaService">
		< property name="captchaEngine" ref="captchaEngine"/>
	< /bean>
	
	< bean id="captchaEngine" class="com.octo.captcha.engine.GenericCaptchaEngine">
		< constructor-arg index="0">
			< list>
				< ref bean="CaptchaFactory"/>
			< /list>
		< /constructor-arg>
	< /bean>
	
	< bean id="CaptchaFactory" class="com.octo.captcha.image.gimpy.GimpyFactory" >
		< constructor-arg>
		< constructor-arg>
	< /bean>
	
	< bean id="wordgen" class= "com.octo.captcha.component.word.wordgenerator.DictionaryWordGenerator" >
		< constructor-arg>
	< /bean>	
	
	< bean id="filedict" class="com.octo.captcha.component.word.FileDictionary" >
		< constructor-arg index="0">toddlist
	< /bean>
	
	< bean id="wordtoimage" class="com.octo.captcha.component.image.wordtoimage.FilteredComposedWordToImage" >
		< constructor-arg index="0">
		< constructor-arg index="1">
		< constructor-arg index="2">
		< constructor-arg index="3">
		< constructor-arg index="4">
		< constructor-arg index="5">
	< /bean>
		
	< bean id="fontGenRandom" class="com.octo.captcha.component.image.fontgenerator.RandomFontGenerator" >
		< constructor-arg index="0">100
		< constructor-arg index="1">100
		< constructor-arg index="2">
			< list>
				< ref bean="fontArial"/>
			< /list>
		< /constructor-arg>
	< /bean>
		
	< bean id="fontArial" class="java.awt.Font" >
		< constructor-arg index="0">Arial
		< constructor-arg index="1">0
		< constructor-arg index="2">8
	< /bean>
		
	< bean id="backGenUni" class="com.octo.captcha.component.image.backgroundgenerator.UniColorBackgroundGenerator" >
		< constructor-arg index="0">250
		< constructor-arg index="1">100
	< /bean>
	
	< bean id="simpleWhitePaster" class="com.octo.captcha.component.image.textpaster.SimpleTextPaster" >
		< constructor-arg type="java.lang.Integer" index="0">3
		< constructor-arg type="java.lang.Integer" index="1">5
		< constructor-arg type="java.awt.Color" index="2">
	< /bean>
	
	< bean id="color" class="java.awt.Color" >
		< constructor-arg type="int" index="0">100
		< constructor-arg type="int" index="1">200
		< constructor-arg type="int" index="2">56
	< /bean>
	
	< bean id="sphere" class="com.jhlabs.image.SphereFilter" >
		< property name="refractionIndex">1
	< /bean>

	< bean id="rippleBack" class="com.jhlabs.image.RippleFilter" >
		< property name="waveType">3
		< property name="XAmplitude">10
		< property name="YAmplitude">3
		< property name="XWavelength">20
		< property name="YWavelength">10
		< property name="edgeAction">1
	< /bean>
	
	< bean id="ripple3" class="com.jhlabs.image.RippleFilter" >
		< property name="waveType">5
		< property name="XAmplitude">5
		< property name="YAmplitude">5
		< property name="XWavelength">10
		< property name="YWavelength">10
		< property name="edgeAction">1
	< /bean>
	
	< bean id="emboss" class="com.jhlabs.image.EmbossFilter" >
		< property name="bumpHeight">1.0
	< /bean>
	
	< bean id="smear" class="com.jhlabs.image.SmearFilter" >
		< property name="shape">0
		< property name="distance">15
		< property name="density">0.4
		< property name="scatter">0.5
		< property name="angle">0.0
		< property name="mix">0.6
		< property name="fadeout">0
	< /bean>

	< bean id="ripple" class="com.jhlabs.image.RippleFilter" >
		< property name="waveType">1
		< property name="XAmplitude">2
		< property name="YAmplitude">2
		< property name="XWavelength">10
		< property name="YWavelength">10
		< property name="edgeAction">1
	< /bean>

	< bean id="ripple2" class="com.jhlabs.image.RippleFilter" >
		< property name="waveType">2
		< property name="XAmplitude">2
		< property name="YAmplitude">2
		< property name="XWavelength">10
		< property name="YWavelength">10
		< property name="edgeAction">1
	< /bean>

	 < bean id="twirl" class="com.jhlabs.image.TwirlFilter" >
		< property name="angle">0.8
	< /bean>

	< bean id="water" class="com.jhlabs.image.WaterFilter" >
		< property name="amplitude">2
		< property name="antialias">true
		< property name="wavelength">20
	< /bean>

	< bean id="weaves" class="com.jhlabs.image.WeaveFilter" >
		< property name="useImageColors">true
		< property name="XGap">2
		< property name="XWidth">10
		< property name="YWidth">16
		< property name="YGap">6
	< /bean>

	< bean id="crystal" class="com.jhlabs.image.CrystalizeFilter" >
		< property name="scale">0.5
		< property name="gridType">1
		< property name="fadeEdges">false
		< property name="edgeThickness">0.4
		< property name="randomness">1.0
	< /bean>

El resultado del captcha con esta configuración cambia totalmente:

6. Conclusiones

El resultado de la utilización de esta herramienta no puede ser más satisfactorio ya que permite de forma muy sencilla configurarla y adecuarla a nuestras necesidades gracias
al soporte de Spring. Siempre es mucho mejor utilizar librerías de terceros para nuestros proyectos que tener que hacer todo nosotros. Imaginaos el esfuerzo que supondría crear algo equivalente a
JCaptcha si lo necesitáramos para nuestros proyectos.

Un saludo. Juan.

2 COMENTARIOS

  1. Hola, muchas gracias por el post. Me ha sido de gran utilidad.

    Quería comentar que para aplicar vuestro ejemplo en mi código, he tenido modificar el fichero de configuración \\\’faces-beans.xml\\\’, en concreto el bean que hace uso del captcha, y modificar la configuración de jCaptcha en application-context.xml ya que las palabras de mi diccionario son todas de cuatro caracteres.

    La modificación en faces-beans.xml es:

    imageCaptchaService
    #{imageCaptchaService}

    (y en el bean implementar get y set de la propiedad imageCaptchaService).

    La modificación de application-context.xml es en el bean \\\’simpleWhitePaster\\\’, cambiar los argumentos de tipo Integer y darles valor 4.

    Saludos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad