Dienstag, 1. Februar 2011

Für Symfony einen eigenen Captcha schreiben

Einen eigenen Captcha für das eigene Symfony-Projekt ist relativ einfach, da PHP Funktionen hat um dynamisch Bilder zu erstellen.

Wir haben ein Captcha für unsere Login-Maske erstellt, da Google's reCaptcha zu lange Texte präsentiert und bei zwar seltenen aber doch Gelegenheiten nicht verfügbar war und somit eine Anmeldung auf unsere Webseite unmöglich machte.

Die Lösung besteht aus den folgenden Schritten:
1. Eine kleine Grafik als Hintergrundbild für den Captcha-Text
2. Eine PHP-Klasse, die die Captcha-Grafik produziert
3. Ein Modul "captcha" mit Action und Template
4. Routing für das Captcha-Modul anpassen
5. Eine Formularklasse mit Widget und Validator für das Captcha
6. Ein Validator für das Captcha
7. Das Anzeigen der Captcha-Grafik

1. Hintergrundbild
Oft wir eine etwas "unruhige" Hintergrundgrafik für das Captcha erstellt, um das maschinelle Auslesen des Textes zu erschweren.
Wir haben es hier nicht übertrieben, sondern nur ein par Punkte auf einem weißem Hintergrund platziert.


2. Eine PHP-Klasse für das Erzeugen der Captcha-Grafik
Wir haben uns für eine recht einfache Captche-Grafik entschieden. Es gibt mit PHP auch die Möglichkeit andere Schriftarten zu nutzen sowie den Text zu drehen usw.

Was die einzelne Funktionen in diese Klasse machen können Sie unter http://php.net nachschlagen.

Ein kleiner Hinweis noch. Da später der Aufruf unseres Symfony-Moduls eine Grafik liefern soll und kein HTML, ist es wichtig, dass wir den Header mit

header ("Content-type: image/png");

definieren (siehe unten). Wir sehen hier auch, dass unsere Grafik eine png ist. Es sind aber auch andere Formate möglich. Siehe http://php.net.

class CaptchaImage {

function generate($CaptchaText) {
$fontsize = 5;
header ("Content-type: image/png");

$image = ImageCreateFromPNG($_SERVER['DOCUMENT_ROOT'].'/images/captcha.png'); //Backgroundimage

imagecolorallocate ($image, 0, 0, 0);
$text_color = ImageColorAllocate ($image, 7, 29, 192);

ImageString ($image, $fontsize, 5, 7, $CaptchaText, $text_color);
ImagePNG ($image);
ImageDestroy($img);
}
}

3. Ein Symfony-Modul
Da wir die Captcha-Angelegenheit schön in Symfony-Stil organisiert haben möchten, haben wir ein eigenes Modul dafür erstellt.
Das ist einfach und bringt etwas Struktur in die Anwendung.
Das Modul trägt den Namen "captcha" und die Action "captchagen"

Das Template captchagenSuccess.php ist leer, da die Action das Captcha-Bild liefern soll.

Die "captchagen" Action ist nicht sehr umfangreich und besteht hauptsächlich aus dem Generieren des Captche-Textes und das
Erzeugen des Bildes. In der Action wir der Captcha-Text in der Session aufgehängt, damit wir den Später gegen Benutzereingabe in unserem Validator prüfen können. Mehr dazu später.

class captchaActions extends sfActions
{

public function executeCaptchagen($request) {
//Captche-Text generieren
$captchatext = '';
$pool1 = 'ABDEFGHIJKLMNPRSTUVWXYZ';
for ($i = 1; $i <= 3; $i++) {
$captchatext .= substr($pool1, rand(0, 22), 1);
}
$captchatext .= " ";
$pool2 = '123456789';
for ($i = 1; $i <= 3; $i++) {
$captchatext .= substr($pool2, rand(0, 8), 1);
}

//Captche-Text in der Session aufhängen
$_SESSION['captchatext'] = $captchatext;

//Captcha-Text mit Hilfe unsere Klasse "CaptchaImage" erstellen $tmpCaptchaImage = new CaptchaImage(); $tmpCaptchaImage->generate($captchatext);
}

}

4. Routing anpassen
Damit wir unser Somfony-Modul "captcha" rufen können, passen wir das Routing an.
Das machen wir in der Datei apps/frontend/config/routing.yml

captcha:
  url: /captcha
  param: { module: captcha, action: captchagen }


5. Widget und Validator für das Captcha
Wir benutzen bereits das sfDoctrineGuardPlugin und haben daher die darin enthaltene Form-Klasse "sfGuardFormSignin" um unser
Captcha erweitert. Selbstverständlich können Sie Ihre eigene Form-Klasse für die (Anmelde-)Maske entsprechend erweitern.

So sieht die erweiterte Klasse aus:

class sfGuardFormSignin extends BasesfGuardFormSignin
{
public function configure()
{
//Widgets erstellen und konfigurieren
$this->widgetSchema['username'] = new sfWidgetFormInputText();
$this->widgetSchema['password'] = new sfWidgetFormInputPassword(array('type' => 'password'));
$this->widgetSchema['remember'] = new sfWidgetFormInputCheckbox();
$this->widgetSchema['captcha'] = new sfWidgetFormInputText();

//Validators erstellen und konfigurieren
$this->validatorSchema['username'] = new sfValidatorString();
$this->validatorSchema['password'] = new sfValidatorString();
$this->validatorSchema['remember'] = new sfValidatorBoolean();
$this->validatorSchema['captcha'] = new sfValidatorCaptcha();

$this->validatorSchema->setPostValidator(new sfGuardValidatorUser());

$this->widgetSchema->setLabels(array(
'username' => 'Benutzername:',
'password' => 'Passwort:',
'remember' => 'Bitte heute eingeloggt bleiben:',
'captcha' => 'Sicherheitscode:'
));
}
}

Wir haben in diese Klasse folgende Erweiterungen gemacht:
Ein Captcha-Widget hinzugefüht:
$this->widgetSchema['captcha'] = new sfWidgetFormInputText();
Das ist einfach ein normales Textfeld, wo der Anwender den Captcha-Text eintragen kann.

Ein Validator für den Captcha hinzugfügt:
$this->validatorSchema['captcha'] = new sfValidatorCaptcha();
Dieser Validator, den wir selber geschrieben haben, wird unten erläutert.

6. Validator
Um den, vom Anwender eingegebener, Captcha-Text zu prüfen, schreiben wir in Symfony-Manier ein Validator.
Wir haben den "sfValidatorCaptcha" genannt und in der Datei sfValidatorCaptcha.class.php unter lib/form/ abgelegt.

class sfValidatorCaptcha extends sfValidatorString
{
protected function configure($options = array(), $messages = array())
{
parent::configure($options, $messages);

$this->setMessage('invalid', 'Der Sicherheitscode stimmt nicht mit dem im Bild überein');
}


protected function doClean($value)
{
parent::doClean($value);
$clean = (string) $value;

//Captcha-Text aus der Sesson holen
$captchatext = $_SESSION['captchatext'];

if($captchatext != $clean) {
throw new sfValidatorError($this, 'invalid', array('value' => $value));
}

return $clean;
}
}

In der Methode configure, definieren wir den Hinweis für den Anwender, sollte er den Captcha-Text in der Captcha-Grafik falsch abtippen.

Die Methode doClean vergleicht den vom Anwender eingegebenen Text, mit dem in der Action captchagen generierten Text (den wir aus der Session holen). Rekapitulation: Der Captcha-Text hatten wir ja in der Action "captchagen", wo der Text generiert wird, in der Session aufgehängt, damit der uns später zwecks Validierung zur Verfügung steht.

7. Anzeigen der Captcha-Grafik
Für das Anzeigen der Captcha-Grafik, erweitern wir das ensprechende Template des sfDoctrineGuardPlugins.
Da wir in dem Template nicht das Symfony-Rendering nutzen, sondern ein eigenes Layout mit eine Tabelle erstellt haben, müssen wir hier nur diese Zeilen aufnehmen:

<tr>
<td colspan="2">
<?php echo $form['captcha']->renderError() ?>
</td>
</tr>
<tr>
<td>
Sicherheitscode:
</td>
<td>
<?php echo image_tag(url_for('@captcha'), array('raw_name' => true)) ?>
</td>
</tr>
<tr>
<td>
<?php echo $form['captcha']->renderLabel() ?>
</td>
<td>
<?php echo $form['captcha']->render() ?>
</td>
</tr>

Das interessanteste hier ist diese Zeile:

<?php echo image_tag(url_for('@captcha'), array('raw_name' => true)) ?>

Die Symfony-Hilfsroutine "image_tag" generiert ein img-Tag.
In unserem Fall sieht das Ergebnis so aus:

<img src="/captcha" />

Der Hilfsroutine-Parameter "raw_name" verhindert, dass Symfony ein ".png" hinten dran an "/captcha" hängt.
Auch wenn der Header ein Bild in png-Format vorgibt, wäre das ja nicht korrekt, da hier die Bildquelle (src) eine Symfony-Action ist.
Vergleiche hier den url_for-Parameter "@captcha" mit unser Routing-Definition unter Punkt 4 oben. Das hier verwendete "@captcha" ist der Routing-Name "captcha:".

Das fertige Ergebnis sieht so aus:



Ich denke das war's jetzt :-)
Viel Spaß beim Ausprobieren!

PS. Wenn Jemandem veraten könnte, wie man hier Quellcode formatieren kann, wäre das sehr nett ;-)

Keine Kommentare:

Kommentar veröffentlichen