TUTORIAL: Cum sa-ti faci propriul CAPTCHA

Average: 4.7 (13 votes)

CAPTCHA - un termen familiar pentru unii dintre noi, mai putin cunoscut altora, dar cu siguranta ceva cu care toti ne-am intalnit la un moment dat.
Generalizat, CAPTCHA (acronim pentru Completely Automated Public Turing test to tell Computers and Humans Apart) reprezinta o modalitate oarecare de a face distinctia intre un utilizator uman si o masina (computer), dupa cum spune si numele. Acest mecanism a aparut din nevoia de securitate, fiind pentru prima oara implementat in sistemul de inregistrare de la Yahoo, si consta in prezentarea unei imagini ce continea un cuvant distorsionat, cerandu-i-se utilizatorului sa recunoasca cuvantul din imagine, lucru foarte greu de realizat, daca nu chiar imposibil, de catre o masina. Cu timpul, conceptul s-a raspandit si s-a dezvoltat, ajungand la forme foarte diverse care impuneau utilizatorilor una din urmatoarele:

  • recunoasterea unor coduri aleatorii
  • recunoasterea unui numar minim de cuvinte cu un grad foarte ridicat de distorsionare dintr-o imagine data
  • recunoasterea obiectelor dintr-o imagine data
  • efectuarea de operatii matematice simple
  • dezlegarea unor ghicitori simple
  • reproducerea in scris a unor cuvinte/propozitii redate prin sunete (aceasta metoda este recomandata ca metoda alternativa, pentru a nu discrimina persoanele ce sufera de diverse deficiente care le impiedica sa citeasca informatia de pe monitor)

Unde devine folositor acest sistem? CAPTCHA este util intr-unul din urmatoarele cazuri, fara a se limita insa la acestea:

  • protejarea unui formular de inregistrare de inregistrarile automate
  • prevenirea postarii comentariilor publicitare sau de orice fel de catre roboti pe bloguri sau alte aplicatii asemanatoare
  • protejarea sistemelor de vot impotriva tentativelor de fraudare a rezultatelor prin folosirea robotilor care sa voteze automat
  • protejarea formularelor de contact de pe site-uri

In clipa de fata exista pe internet o multime de astfel de solutii, gratuite sau comerciale, dar tocmai gradul lor de raspandire le face vulnerabile. Unele folosesc cuvinte din dictionar, usor de recunoscut de catre roboti, altele folosesc imagini comune, care pot fi indexate anterior, altele folosesc text nedistorsionat si lista punctelor slabe ar putea continua.
In cele ce urmeaza, vom realiza un sistem simplu, care va genera un cod aleatoriu, a carei recunoastere vom incerca s-o ingreunam cat mai mult cu putinta.

ATENTIE!
Acesta este un tutorial. Scopul sau este de a invata pe cei care-l citesc un anumit lucru. Scopul sau NU este acela de a oferi pur si simplu o solutie gratuita, de aceea va trebui sa cititi cu atentie tot ceea ce urmeaza si sa efectuati unele modificari in functie de aplicatia dumneavoastra.

  • Cerinte minime de sistem (server-ul pe care va rula script-ul):
    • PHP 4.1.0 sau mai recent (PHP 5.x.x pentru blur)
    • libraria FreeType
    • GD 2.0.1 sau mai recent
  • Configurare:
    In fisierul de configurari sunt definite caile catre fonturile ce vor fi folosite si imaginile de fundal. Acestea trebuie inlocuite cu fonturile si imaginile voastre sau puteti downloada arhivele cu cele folosite de mine. Toate fonturile din arhiva sunt fonturi GRATUITE!
    Fonturile folosite in tutorial
    Imaginile de fundal folosite in tutorial

In primul rand vom crea un fisier de configurari ( Ex:config.php) in care vor fi definiti parametrii dupa care va fi generat codul.

  1. Codul generat de catre fisier trebuie facut disponibil si pentru scriptul care prelucreaza input-ul utilizatorului. Aceasta se poate face cu ajutorul sesiunilor, a unei baze de date, a unor fisiere temporare, etc. Pentru exemplu nostru am ales sa folosesc sesiunile, pentru ca este cea mai simpla dintre variante si cea mai usor de implementat, independent de mediul in care ruleaza scriptul (este posibil sa nu avem acces la o baza de date sau sa nu avem drepturi de scriere pe hard din oarecare motive, insa sesiunile functioneaza in toate cazurile). Astfel, primul lucru va fi sa definim numele variabilei in care vom pastra codul:
    $sessionVar = 'myCaptcha';

  2. Urmatorul pas va fi definirea caracterelor permise.
    $allowedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
    De remarcat ar fi faptul ca nu am introdus cifra 0 (zero), pentru a evita confuziile intre cifra 0 (zero) si litera O (o mare).

  3. O noua piedica in calea recunoasterii codului ar fi ca lungimea acestuia sa fie aleatorie. Pentru aceasta, vom avea nevoie de o lungime minima si una maxima:
    $minCodeLength = 4;
    $maxCodeLength = 6;

  4. Se stie ca un CAPTCHA este mai usor de spart daca dimensiunea caracterelor este egala, astfel ca noi vom avea dimensiuni aleatorii pentru fiecare caracter in parte, deci va trebui sa definim un minim si un maxim (valorile reprezinta dimensiunea in pixeli):
    $minFontSize = 24;
    $maxFontSize = 32;

  5. Caracterele cu spatiere uniforma sunt mai usor de izolat, deci mai usor de recunoscut, astfel ca vom defini un spatiu minim si un maxim intre caracterele ce vor forma codul:
    $minLetterSpacing = 4;
    $maxLetterSpacing = 10;

  6. Un alt factor ce influenteaza usurinta recunoasterii caracterelor este liniaritatea acestora, astfel ca vom evita plasarea uniforma a acestora pe o linie orizontala prin definirea unei deplasari verticale maxime (o distanta maxima masurata in pixeli la care va fi plasat caracterul fata de linia mediana orizontala a imaginii):
    $maxVerticalDisplacement = 15;

  7. Continuand cu alterarea codului, vom roti caracterele cu un numar aleatoriu de grade, la stanga sau la dreapta:
    $maxRotationLeft  = 15;
    $maxRotationRight = 15;

  8. $textPadding = 5;
    Asa cum spune si numele variabilei, acesta va fi padding-ul (distanta de la cod pana la marginile imaginii, o zona neutra)

  9. Hai sa ne jucam putin si cu gama de culori! Sa incepem cu fundalul:
    $bgColor = array(255, 255, 255);


  10. In continuare, vom defini un array in care vom stoca culorile permise pentru caracterele ce vor forma codul, atat in format zecimal cat si in format hexazecimal:
    $textColor = array(	
    	'200, 0, 0',
    	'30, 200, 0',
    	'150, 0, 200',
    	'150, 150, 0',
    	'100, 100, 100',
    	'ff0099',
    	'99ff00'
    	);


  11. Hai sa vedem acum ce ar mai ingreuna munca robotilor? Ah! Fonturi diferite pentru fiecare caracter in parte! ? In continuare vom defini directorul in care sunt stocate fonturile si fisierele in format TTF pe care le vom folosi. Fonturi in format TTF gratuite se pot gasi pe site-ul DaFont.
    ATENTIE! Formatul TTF necesita libraria FreeType pentru a putea fi folosit in functiile GD!
    $fontsFolder = 'my_fonts/';
    $fontFiles   = array(	
    	'elektra.ttf',
    	'episode1.ttf',
    	'lcd2bold.ttf',
    	'optimusprinceps.ttf',
    	'sams_town.ttf',
    	'steinerlight.ttf',
    	'turn_table.ttf'
    	);


  12. Ultimul lucru ce ne-a ramas de facut este definirea unor fisiere imagine pentru a putea fi folosite pe fundal:
    $backgroundsFolder = 'my_backgrounds/';
    $backgrounds = array(
    	'bg_1.png',
    	'bg_2.png',
    	'bg_3.png',
    	'bg_4.png'
    	);



NU UITATI SA INLOCUITI CAILE SI NUMELE FISIERELOR DE MAI SUS CU CELE FOLOSITE DE DUMNEAVOASTRA


De-ajuns cu configurarile si sa dam drumul la actiune!
Vom avea nevoie de un fisier PHP (sa-i zicem show_code.php) pe care il vom apela asa cum apelam o imagine oarecare, si anume:

<img src="show_code.php" alt="myCaptcha" />

  1. Sa incepem prin a initializa sesiunea si a include fisierul cu setarile:
    session_start();
    include_once('config.php');
    $_SESSION[$sessionVar] = '';


  2. Pe parcursul codului, vor aparea 2 operatiuni pe care mi s-a parut mai natural sa le pun in 2 functii, pentru a pastra mai clari pasii pe care i-am urmat in vederea generarii imaginii si pe care nici nu le voi explica amanuntit.
    In primul rand, vom avea nevoie de o functie care va returna spatiul ocupat de un anumit caracter:
    function getCharSize($fontFile, $char, $size, $angle=0){
    	if( is_file($fontFile) && strlen($char) == 1 && $size > 0){
    		$corners = @imagettfbbox( $size, $angle, $fontFile, $char );
    		$left    = ($corners[0]>$corners[6])?$corners[6]:$corners[0];
    		$right   = ($corners[2]>$corners[4])?$corners[2]:$corners[4];
    		$top     = ($corners[5]>$corners[7])?$corners[7]:$corners[5];
    		$bottom  = ($corners[1]>$corners[3])?$corners[1]:$corners[3];
    		$width   = $right - $left + 4;
    		$height  = abs($top - $bottom) + 4;
    		$return  = array(
    				'width'  =>$width, 
    				'height' =>$height,
    				'bottom' =>$bottom,
    				'right'  =>$right,
    				'top'    =>$top,
    				'left'   =>$left
    				);
    	}else{
    		$return = false;
    	}
    	return $return;
    }
    Pentru a intelege mai bine cum functioneaza codul de mai sus, cititi documentatia functiei imagettfbbox


    Apoi, vom avea nevoie de o functie care ne va returna valori intr-un format unic, indiferent de modul (hexazecimal sau zecimal) in care au fost specificate culorile pe care le poate avea un caracter:
    function hex2decColor($color){
    	if(substr_count($color, ',') == 2){
    		list($red, $green, $blue) = array_map('trim', explode(',', $color));
    	}else{
    		$red   = hexdec($color[0].$color[1]);
    		$green = hexdec($color[2].$color[3]);
    		$blue  = hexdec($color[4].$color[5]);
    	}
    	return array('red'=>$red, 'green'=>$green, 'blue'=>$blue);
    }


  3. Initializam cu 0 dimensiunile imaginii si generam lungimea codului, in functie de lungimea minima si lungimea maxima stabilite:
    $imgWidth = 0;
    $imgHeight = 0;
     
    $codeLength = rand($minCodeLength, $maxCodeLength);


  4. In continuare, vom genera de $codeLength ori cate un caracter si proprietatile sale:
    for($c = 1; $c <= $codeLength; $c++){
    	$char[$c]['char']   = $allowedChars[rand(0, strlen($allowedChars)-1)];
    	$char[$c]['color']  = hex2decColor($textColor[rand(0, count($textColor)-1 )]);
    	$char[$c]['displacement'] = rand(0, $maxVerticalDisplacement*2) - $maxVerticalDisplacement;
    	$char[$c]['font']   = $fontsFolder.$fontFiles[rand(0, count($fontFiles)-1 )];
    	$char[$c]['size']   = rand($minFontSize, $maxFontSize);
    	$char[$c]['angle']  = rand(0, $maxRotationLeft + $maxRotationRight) - $maxRotationRight;
    	$char[$c]['space']  = rand($minLetterSpacing, $maxLetterSpacing);

    Aflam dimensiunile caracterului curent, folosind una din functiile definite mai devreme:
    	$properties = getCharSize($char[$c]['font'], $char[$c]['char'], $char[$c]['size'], $char[$c]['angle']);
    	$width  = $properties['width'];
    	$height = $properties['height'];

    Dupa ce am aflat latimea caracterului, o adaugam la latimea totala a imaginii, impreuna cu distanta pana la urmatorul caracter:
    	$imgWidth += $width;
    	if($c != $codeLength)
    		$imgWidth += $char[$c]['space'];

    Daca inaltimea imaginii este mai mica decat inaltimea caracterului curent, atunci ii atribuim valoarea inaltimii caracterului:
    	if( ($height + abs($char[$c]['displacement'])) > $imgHeight )
    		$imgHeight = $height + abs($char[$c]['displacement']);

    Adaugam caracterul curent in sirul ce formeaza codul nostru:
    	$_SESSION[$sessionVar] .= $char[$c]['char'];
    }

    Adaugam la dimensiunile finale ale imaginii si padding-ul stabilit * 2 (fata/spate pe orizontala, sus/jos pe verticala):
    $imgWidth  += $textPadding * 2;
    $imgHeight += $textPadding * 2 + $maxVerticalDisplacement;


  5. Acum, ca am stabilit dimensiunile, hai sa si creem imaginea si sa calculam pozitia imaginii de fundal. Imaginile de fundal pot fi diferite texturi sau peisaje, la rezolutie mai mare decat imaginea noastra, asa ca nu vom avea nevoie decat de o portiune definita aleatoriu din acestea:
    $img = imagecreatetruecolor($imgWidth, $imgHeight);

    Alegem o imagine aleatorie si-i citim dimensiunile:
    $bg = rand(0, count($backgrounds)-1);
    list($bgWidth, $bgHeight) = getimagesize($backgroundsFolder.$backgrounds[$bg]);

    Stabilim pozitia de unde incepe portiunea de imagine ce va fi folosita ca fundal si o copiem in imaginea noastra:
    $bgX = rand(0, $bgWidth-$imgWidth);
    $bgY = rand(0, $bgHeight-$imgHeight);
     
    $bgImg = imagecreatefrompng($backgroundsFolder.$backgrounds[$bg]); 
    imagecopy($img, $bgImg, 0, 0, $bgX, $bgY, $imgWidth, $imgHeight);
    imagedestroy($bgImg);


  6. Urmeaza scrierea codului in imagine, asa ca incepem prin a pozitiona cursorul nostru imaginar la capat de rand:
    $cursor = $textPadding;

    Alocam culoarea generata anterior pentru caracterul curent:
    for($c = 1; $c <= $codeLength; $c++){
    	$color = imagecolorallocate($img, $char[$c]['color']['red'], $char[$c]['color']['green'], $char[$c]['color']['blue']);

    Calculam pozitia pe verticala a caracterului, in functie de dimensiunile sale si de valoarea deplasarii fata de centru generata pentru el:
    	$properties = getCharSize($char[$c]['font'], $char[$c]['char'], $char[$c]['size'], $char[$c]['angle']);
    	$width  = $properties['width'];
    	$height = $properties['height'];
    	$bottom = $properties['bottom'];
     
    	$y = $char[$c]['displacement'] + $maxVerticalDisplacement + $height - $bottom;

    Scriem caracterul curent si deplasam cursorul la dreapta:
    	imagettftext($img, $char[$c]['size'], $char[$c]['angle'], $cursor, $y, $color, $char[$c]['font'], $char[$c]['char']);
    	$cursor += $width + $char[$c]['space'];
    }

    Nu ne-a mai ramas decat sa aplicam filtrul Gaussian Blur, daca sistemul ne permite:
    if(function_exists('imagefilter')){
    	imagefilter($img, IMG_FILTER_GAUSSIAN_BLUR);
    }


  7. In momentul acesta, imaginea noastra este gata si nu ne mai ramane decat sa o servim browserului, specificandu-i acestuia cu ajutorul header-elor ca imaginea nu trebuie pastrata in cache si ca formatul acesteia este PNG (poate fi la fel de bine GIF, JPEG sau orice alt format suportat de libraria GD), dupa care eliberam resursele pe server:
    header( 'Cache-Control: no-store, no-cache, must-revalidate' );
    header( 'Cache-Control: post-check=0, pre-check=0', false );
    header( 'Pragma: no-cache' );
    header( 'Content-type: image/png' );
    imagepng($img);
    imagedestroy($img);


THE END




Eh! Verificarea! :P Era sa uit... Hai ca-i simplu!

In fisierul in care se verifica datele trimise pe POST va trebui sa deschidem sesiunea folosind session_start();, dupa care va trebui sa verificam ca datele primite de la formular coincid cu cele salvate in sesiune, adica, in cazul nostru, ceva de genul:

if($_SERVER['REQUEST_METHOD'] == 'POST'){
	if($_POST['myCaptcha'] == $_SESSION['myCaptcha']){
		// se proceseaza restul datelor
	}else{
		// Wrong Code! Try Again or Go Away!
	}
}



Tips&Triks

Pentru a oferi vizitatorilor posibilitatea de a genera un alt cod fara a fi nevoie sa reincarce toata pagina, afisati imaginea folosind urmatorul cod:
<img src="show_code.php" alt="myCaptcha" onclick="javascript:this.src += '?';" />

astfel, codul va fi regenerat pur si simplu facand click pe imagine! ;)



Tags: