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:
- Implementierung eines eigenen Servlets, welches mit der Auslieferung der generierten Bilder beauftragt wird.
- Eintrag des Servlets und des Mappings in die web.xml
- Implementierung eines Singleton, welches den CaptchaService zur Verfügung stellt.
- 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.

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
Ich würde mir das Beispiel gerne anschauen. Leider scheint die Beispiel URL nicht lade zu wollen. Ich versuch es später noch mal.
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
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
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.
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.
@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?