Home > Java JEE, Software Entwicklung > captcha tutorial – Bots aussperren

captcha tutorial – Bots aussperren

Vergangene Woche habe ich für einen aktuellen Kunden eine Funktion implementiert, die es dem Nutzer einer Webseite erlaubt, anderen diese Seite per E-Mail zu empfehlen. Dies öffnete Spammern potentiell Tür und Tor. Dem Kunden war besonders daran gelegen, sogenannte Bots auszusperren, die diese Funktion in sehr hoher Frequenz ausführen können.

[ad#vert-banner]

Als Mittel der Wahl hat sich seit Jahren das sogenannte CAPTCHA etabliert. Dabei werden Bilder erzeugt, die auf unebendem Hintergrund verzerrte Zeichen darstellen. Der Benutzer muss diese sogenannte Challenge lösen, in dem er die Zeichenfolge in ein Formularfeld eingibt. Stimmen die eingegebenen Zeichen und die Zeichen auf dem Bild übereinstimmen. Hier macht man sich zu nutze, dass der Mensch besser abstrahieren kann, und die Buchstaben trotz widriger Umstände erkennen kann, wo eine Maschine scheitert.

Da ich das Rad nicht neu erfinden will, habe ich mich einer Bibliothek bedient, die diese Funktionalität zur Verfügung stellt, und sehr einfach in das aktuelle Projekt einzubinden ist. Meine Wahl fiel auf JCaptcha. Wie der Name vermuten läßt ist JCaptcha eine Java basierte Implementierung. Sie läßt sich in Java basierte Webprojekte sehr einfach integrieren.

Folgende Schritte sind notwendig:

  1. Implementierung eines eigenen Servlets, welches mit der Auslieferung der generierten Bilder beauftragt wird.
  2. Eintrag des Servlets und des Mappings in die web.xml
  3. Implementierung eines Singleton, welches den CaptchaService zur Verfügung stellt.
  4. Erstellen einer validierungsmethode zum Überprüfen der Benutzereingabe.

Der generelle Ablauf ist recht einfach. Das in der JSP mit Hilfe des <img src=”img.captcha”> referenzierte Bild wird von unserem Servlet bedient. Dieses erzeugt mit Hilfe des CaptchaService ein neues Bild und gibt es als jpg zurück. Der CaptchaService speichert in einer internen Map unter dem Schlüssel der jeweiligen session id das captcha response. Die Methode validateResponseForID des CaptchaService prüft, ob der eingegebene Text mit dem Bild übereinstimmt.

Das Servlet wird wie folgt implementiert:

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;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 com.octo.captcha.service.CaptchaServiceException;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
/**
 * @author Christoph Bünte
 *
 * @version $Revision: 1.5 $
 *
 * @created 11.10.2007
 *
 */
public class CaptchaServlet extends HttpServlet {

/**
  * The serial id for this class
  */
 private static final long serialVersionUID = -5555311841435084305L;

@Override
 public void init(ServletConfig servletConfig) throws ServletException {

super.init(servletConfig);
}

/**
  * Generiert das Captcha image in form eines bytearrays
  *
  * @param token
  *            SessionId
  * @param loc
  *            Locale Object für die Sprache des Benutzers
  * @return
  */
 private synchronized byte[] getCaptchaChallenge(String token, Locale loc) {

byte[] captchaChallenge = null;

try {
 		// create the captcha challenge
 		BufferedImage challenge = CaptchaServiceSingleton.getInstance()
 				.getImageChallengeForID(token, loc);

		// transform image data into jpeg byte array
 		ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();

		JPEGImageEncoder jpegEncoder = JPEGCodec
 				.createJPEGEncoder(jpegOutputStream);

		jpegEncoder.encode(challenge);

		captchaChallenge = jpegOutputStream.toByteArray();

 	} catch (IOException e) {
 		//TODO: Fehlerbehandlung
 	} catch (CaptchaServiceException e) {
 		//TODO: Fehlerbehandlung
 	}

	return captchaChallenge;

 }

@Override
 protected void doGet(HttpServletRequest request,

 		HttpServletResponse response) throws ServletException, IOException {

			// Hier benutzen wir die session id und das Locale Objekt des requests
			String token = request.getSession().getId();
 			Locale loc = request.getLocale();

			// hole das CAPTCHA challenge als JPEG
			byte[] captchaAsJpeg = getCaptchaChallenge(token, loc);

			// falls nicht verfügbar, sende http code 404 (resource not available)
			if (captchaAsJpeg == null) {
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
			}

			// gibt ein response Objekt mit dem jpeg bild zurück
			// (für den browser: benutze keinen cache und verwirf den inhalt sofort)
			// set header

			response.setHeader("Cache-Control", "no-store");
			response.setHeader("Pragma", "no-cache");
			response.setDateHeader("Expires", 0);
			response.setContentType("image/jpeg");

			// sende bilddaten
			ServletOutputStream responseOutputStream = response.getOutputStream();
			responseOutputStream.write(captchaAsJpeg);
			responseOutputStream.flush();
			responseOutputStream.close();
 		}

}

Der Eintrag in die web.xml sieht so aus:

<servlet>
	<servlet-name>jcaptcha</servlet-name>
	<display-name>JCaptcha</display-name>
	<servlet-class>your.package.path.CaptchaServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>jcaptcha</servlet-name>
	<url-pattern>*.captcha</url-pattern>
</servlet-mapping>

Das CaptchaService als Singleton implementiert:

import com.octo.captcha.engine.image.ListImageCaptchaEngine;
import com.octo.captcha.service.captchastore.CaptchaStore;
import com.octo.captcha.service.captchastore.FastHashMapCaptchaStore;
import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
import com.octo.captcha.service.image.ImageCaptchaService;
/**
 *
 * @author Christoph Bünte
 *
 * @version $Revision: 1.6 $
 *
 * @created 11.10.2007
 *
 */
public class CaptchaServiceSingleton {

	private static ImageCaptchaService instance = new DefaultManageableImageCaptchaService();

	/**
	  * Gibt die einzige Instanz einnes CaptchaService zurück.
	  *
	  * @return
	  */
	 public static ImageCaptchaService getInstance() {

		return instance;
	 }
}

Im Formular für die Eingabe der Captcha Response, wird das Bild wie folgt eingebunden:

<img src="img.captcha"  class="captcha">

Je nachdem, welches Webframework man verwendet, differiert die Implementierung einer Validierungsmethode. Letztlich könnte sie wie folgt aussehen. Es ist ein Beispiel, wie es im stripes framework verwendet wird.

/**
 * Validating capture input
 *
 * @param errors
 */
@ValidationMethod(on = { "sendRecommendation" }, when = ValidationState.ALWAYS)

public void validateCaptcha(ValidationErrors errors) {
 try {
 	String sessionId = context.getRequest().getSession().getId();
 	if (!CaptchaServiceSingleton.getInstance().validateResponseForID(
 			sessionId, captchaResponse)) {
 		errors.add("captchaResponse", new LocalizableError(
 				"captchaResponse.invalidValue"));
 	}
 } catch (CaptchaServiceException e) {
 	// ungültige Session id, Fehlerbehandlung
 }
}

Das Ergebnis ist schon ganz ansehnlich. Im Formular wird mit der Default Engine ein mehr oder weniger schönes Captcha Bild erzeugt. Jedoch fällt Ästheten sofort auf, dass das Bild überhaupt nicht in das Design der Seite passt. Um solche Anforderungen zu erfüllen, implementiert man am besten eine CaptchaEngine. Dazu müssen wir die Singleton Klasse noch etwas modifizieren, doch das schnell erledigt. Wird sind doch agil, oder?

Die unten gezeigte Engine erzeugt ein Captcha Bild mit hellblauem Hintergrund und grauer und schwarzer Schrift. Für die Schrift werden nur Großbuchstaben verwendet. Man hätte als Alternative auch die Informationen zum Rendern des Bildes auch direkt in der Singleton Klasse zu einer vorhandenen Engine hinzufügen können. Unangenehmer Nebeneffekt ist aber, dass es immer eine Default engine gibt. Beim Rendern der Bilder wird dann eine verfügbare Engine zufällig ausgewählt, so dass in unregelmässigen Abständen eine Bild mit grün-rot-gelb gesprenkeltem Hintergrund erscheint. Dieses, für mich anfänglich, merkwürde Verhalten hat mich zwei Stunden aufgehalten. Wer unterschiedliche Captcha Bilder erzeugen möchte implenentiert entsprechend mehr Klassen. Hier ein Beispiel:

import java.awt.Color;import com.octo.captcha.component.image.backgroundgenerator.BackgroundGenerator;
import com.octo.captcha.component.image.backgroundgenerator.UniColorBackgroundGenerator;
import com.octo.captcha.component.image.fontgenerator.DeformedRandomFontGenerator;
import com.octo.captcha.component.image.fontgenerator.FontGenerator;
import com.octo.captcha.component.image.textpaster.RandomTextPaster;
import com.octo.captcha.component.image.textpaster.TextPaster;
import com.octo.captcha.component.image.wordtoimage.ComposedWordToImage;
import com.octo.captcha.component.image.wordtoimage.WordToImage;
import com.octo.captcha.component.word.wordgenerator.RandomWordGenerator;
import com.octo.captcha.component.word.wordgenerator.WordGenerator;
import com.octo.captcha.engine.CaptchaEngine;
import com.octo.captcha.engine.image.ListImageCaptchaEngine;
import com.octo.captcha.image.ImageCaptchaFactory;
import com.octo.captcha.image.gimpy.GimpyFactory;

/**
 * Das ist unsere spezielle {@link CaptchaEngine} Implementierung.
 * @author Christoph Bünte
 *
 * @version $Revision: 1.2 $
 *
 * @created 12.10.2007
 *
 */
final public class CustomCaptchaEngine extends ListImageCaptchaEngine {

@Override
 protected void buildInitialFactories() {

// RGB: #4C4E42
 	Color textColor = new Color(0x4c, 0x4e, 0x42);
 	Color textColor2 = Color.BLACK;
 	Color[] colors = new Color[2];
 	colors[0]=textColor;
 	colors[1]=textColor2;

// RGB: #F4F8FB
 	Color backgroundColor = new Color(0xf4, 0xf8, 0xfb);

// Einheitlicher Hintergrund mit der gegeben Breite, Höhe und Farbe
 	BackgroundGenerator bgGenerator = new UniColorBackgroundGenerator(250,
 			70, backgroundColor);

// Der Text hat die Min-und Maxlänge mit den gegeben Farben
 	TextPaster paster = new RandomTextPaster(6, 6, colors);

// Schriftgrößen 10 bis 30
 	FontGenerator fontGenerator = new DeformedRandomFontGenerator(
 			10, 30);

// Benutze einen einfachen Mechanismus zum Mischen von Hintergrund und Text
 	WordToImage wordToImage = new ComposedWordToImage(fontGenerator,
 			bgGenerator, paster);

// Benutze zufällige Worte aus den gegebenen Buchstaben
 	WordGenerator wordGenerator = new RandomWordGenerator(
 			"ABCDEFGHIJKLMNOPQRSTUVWXYZ");

ImageCaptchaFactory factory = new GimpyFactory(wordGenerator,
 			wordToImage);

 	this.addFactory(factory);
 }
}

Und hier die neue Singleton Implementierung:

import com.octo.captcha.engine.image.ListImageCaptchaEngine;
import com.octo.captcha.service.captchastore.CaptchaStore;
import com.octo.captcha.service.captchastore.FastHashMapCaptchaStore;
import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
import com.octo.captcha.service.image.ImageCaptchaService;

/**
 *
 * @author Christoph Bünte
 *
 * @version $Revision: 1.6 $
 *
 * @created 11.10.2007
 *
 */

public class CaptchaServiceSingleton {

	private static ImageCaptchaService instance = initializeService();

	/**
	  * Gibt die einzige Instanz einnes CaptchaService zurück.
	  *
	  * @return
	  */
	 public static ImageCaptchaService getInstance() {
	 	return instance;
	 }

	/**
	  * Initialisiert den {@link ImageCaptchaService}
	  *
	  * @return
	  */
	 private static ImageCaptchaService initializeService() {

	// Wir brauchen eine Instanz unser eigenen Engine
	 	ListImageCaptchaEngine engine = new CustomCaptchaEngine();

		CaptchaStore captchaStore = new FastHashMapCaptchaStore();
 		captchaStore.empty();

		ImageCaptchaService service = new DefaultManageableImageCaptchaService(
 			captchaStore, engine, 180, 100000, 75000);

		return service;

 	}

}

Kommentare und Anregungen sind wie immer willkommen. Beispiele für die kreativsten Bilder nehme ich auch gerne entgegen. Viel Spass beim ausprobieren.

  1. Jonas
    23. Oktober 2007, 09:59 | #1

    Hmm… wenn ich das so sehe, wie aufwändig das mit Stripes ist, möchte ich doch mal das simple Captcha Beispiel inkl. Source in Wicket zur Lektüre empfehlen. – Aber ich möchte hier auch keinen Framework- Flamewar vom Zaune brechen ;)

  2. 23. Oktober 2007, 10:11 | #2

    Ich würde mir das Beispiel gerne anschauen. Leider scheint die Beispiel URL nicht lade zu wollen. Ich versuch es später noch mal.

  3. 23. Oktober 2007, 12:42 | #3

    So, die Seite geht wieder. Ich bin nicht so richtig “wicked”, deswegen fällt es mir jetzt schwer. Scheint aber ein einfache Ansatz zu sein. Hilft aber nichts, wenn ein anderen Framework verwedent wird. Da ist JCaptcha schon deutlich universeller einsetzbar.

    Und Flamewars gibts hier nich ;)

  4. 7. Dezember 2007, 11:20 | #4

    Wer sich zu dem Thema noch etwas genauer informieren möchte, darf sich auch den Podcast von //SEIBERT/MEDIA anhören. Am besten natürlich online auf podcast.de! http://www.podcast.de/podcast/8409/Seibert_Media_Podcast

  5. Cpt. Cook
    12. Juli 2008, 08:17 | #5

    Von Captchas sollte man die Finger lassen. Sie sind für Menschen schlecht lesbar und für Maschinen (also Bots) kein Problem.

    Viel einfacher ist es, mit einer Vorschau vor dem Versenden-Funktion zu arbeiten. DAS können Spambots nicht und gibt 99,9% Spamschutz.

  6. 12. Juli 2008, 23:15 | #6

    Die Frage über das Für und Wider von Captchas wollte ich nicht in diesem Artikel abhandeln. Manchmal kommt man als Entwickler einfach nicht drum herum. Wider besseren Wissens ist man dann gezwungen das ein oder andere Feature zu programmieren, weil der Auftraggeber es gerne haben möchte und dafür bezahlt. Anyways, ich wollte mich eigentlich auch nicht rechtfertigen, sondern lediglich denjenigen, die Captchas einsetzen (wollen), einen kleinen Leitfaden an die Hand geben.

  7. 12. September 2008, 08:58 | #7

    @Cpt. Cook
    Manchmal sicherlich problematisch zu erkennen (deshalb sollte man sich ja ein neues erstellen lassen können), aber ob dem Nutzer in allen Fällen eine Vorschau besser gefällt?

  1. 26. November 2007, 07:46 | #1