[PHP] Wprowadź numer widoczny na obrazku

Autor: Tomasz Jędrzejewski
Data publikacji: 19.08.2005, 09:48 | Ostatnia modyfikacja: 19.11.2006, 17:47

ArtykuÅ‚ opisuje, jak zabezpieczyć formularz przed dziaÅ‚aniem robotów WWW, poprzez konieczność wprowadzenia z generowanego losowo obrazka tzw. kodu bezpieczeÅ„stwa.



Internet nie jest uczciwym miejscem. Znajduje siÄ™ w nim mnóstwo ludzi, których jedynÄ… rozrywkÄ… jest utrudnianie życia wÅ‚aÅ›cicielom i twórcom stron internetowych poprzez wpisywanie gÅ‚upot do formularzy lub pisanie rozmaitych robotów automatycznie je wypeÅ‚niajÄ…cych. MogÄ… one posÅ‚użyć np. do zarejestrowania danego użytkownika w kilkuset stronach naraz, przy czym on sam nawet nie wie, gdzie zostaÅ‚ dokÅ‚adnie "dodany". Na szczęście w tym przypadku istnieje prosty i skuteczny sposób na zabezpieczenie siÄ™. Wystarczy wrzucić na stronÄ™ obrazek, z którego internauta bÄ™dzie musiaÅ‚ przepisać kilkuznakowy kod. Dla czÅ‚owieka jest to banalne zadanie (chyba że jest niewidomy :)), ale techniki OCR (rozpoznawania tekstu), podobnie jak inne dziedziny programowania, których celem jest naÅ›ladowanie geniuszu ludzkiego mózgu, wciąż nie sÄ… na tyle dokÅ‚adne, by poradzić sobie przyzwoicie z tym zadaniem. Jak zaimplementować podane tu rozwiÄ…zanie? To temat tego artykuÅ‚u...

Plany

CaÅ‚ość przygotujemy sobie w PHP. Do pomocy użyjemy bibliotek GD w wersji 2 oraz bazy MySQL lub PostgreSQL. Tym razem użyjemy programowania strukturalnego, gdyż chcÄ™, aby tekst byÅ‚ przystÄ™pny również dla poczÄ…tkujÄ…cych. CaÅ‚ość podzielimy sobie na trzy funkcje:

DziaÅ‚anie systemu jest proste. Internauta uruchamia stronÄ™ z formularzem. Tam pierwsza z funkcji wygeneruje kod dostÄ™pu, po czym zwróci jego ID. Trafi on do kodu HTML wstawiajÄ…cego nasz obrazek. Przy próbie jego pobrania, nastÄ™pna funkcja odczyta kod na podstawie podanego ID i stworzy obraz. Dobra, internauta wpisuje kod, wysyÅ‚a formularz. Do akcji wkracza trzecia funkcja. Odczytuje ona z formularza nasz kod oraz jego ID, a nastÄ™pnie sprawdza, czy wszystko siÄ™ zgadza. JeÅ›li tak - zwraca 1, a my radoÅ›nie możemy przystÄ…pić do dalszych czynnoÅ›ci zwiÄ…zanych z obróbkÄ… wprowadzonych danych :). WyglÄ…da prosto w teorii, prawie prosto w praktyce. Bowiem o ile sam mechanizm jest stosunkowo Å‚atwy do wdrożenia, o tyle algorytm generujÄ…cy obrazek musi być już odpowiednio przemyÅ›lany. Przede wszystkim nie użyjemy żadnego fontu systemowego - taki bowiem potrafiÄ… już odczytać programy OCR. Zastosujemy system kropek pokolorowanych losowo oraz równie losowo lekko poprzesuwanych, przez co nawet te same cyfry bÄ™dÄ… nieznacznie różniÅ‚y siÄ™ wyglÄ…dem. Dla czÅ‚owieka obdarzonego szczyptÄ… wyobraźni zÅ‚ożenie ciÄ…gu cyfr ze zbioru kropek to nic trudnego. Za to programy OCR po prostu siÄ™ wykrzaczÄ…. Dodamy do tego ponadto dość interesujÄ…cy i nieregularny podkÅ‚ad graficzny, by byÅ‚o im jeszcze trudniej.

Do góry

Bazy danych

Jak wspomniaÅ‚em, nasz kod bÄ™dzie w stanie pracować zarówno pod MySQL'em, jak i pod PostgreSQL'em. O ile w plikach API oraz same zapytania praktycznie nie bÄ™dÄ… siÄ™ różnić, o tyle do stworzenia odpowiedniej tabeli musimy już koniecznie napisać różne zapytania. Najpierw dla MySQL'a:

CREATE TABLE `kody` (
`id` CHAR(32) NOT NULL ,
`kod` CHAR(6) NOT NULL ,
`czas` INT(8) NOT NULL ,
`guest_ip` INT(8) NOT NULL ,
INDEX ( `id` )
) TYPE = HEAP;

ID to trzydziestodwuznakowy identyfikator naszego kodu, który jest trzymany w drugim polu (typ CHAR informuje nas o tym, że dane tu przechowywane zawsze bÄ™dÄ… miaÅ‚y 6 znaków, ni mniej, ni wiÄ™cej). Pole czas potrzebne jest nam, aby usuwać niepotrzebne już rekordy. Dwa ostatnie pola zawierać bÄ™dÄ… adresy IP, z których nadeszÅ‚o żądanie utworzenia kodu. DziÄ™ki temu bÄ™dzie on użyteczny tylko na konkretnej maszynie i nikt inny nie bÄ™dzie mógÅ‚ z niego korzystać. Całą tabelÄ™ zadeklarowaliÅ›my jako HEAP, gdyż rekordów nie przechowujemy w niej na staÅ‚e, w sumie nic siÄ™ nie stanie, gdyby ulegÅ‚y one skasowaniu, a szybkość zawsze siÄ™ przyda.

Dla PostgreSQL'a sprawa bÄ™dzie wyglÄ…daÅ‚a troszkÄ™ inaczej. Przede wszystkim musimy użyć innych typów danych oraz stworzyć trochÄ™ inaczej indeks:

CREATE TABLE kody(
id character(32),
kod character(6),
czas integer,
guest_ip integer
);
 
CREATE INDEX id_idx ON kody USING hash (id);

Zamiast typu INT(8) użyÅ‚em tutaj zwykÅ‚ego czterobajtowego integer (ok. 4 mld kombinacji). Ponadto przy tworzeniu indeksu musiaÅ‚em okreÅ›lić jego typ. Opisuje on, w jaki sposób bÄ™dÄ… gromadzone i wyszukiwane znajdujÄ…ce siÄ™ w nim dane. Jako że my tu mamy identyfikatory tekstowe, na których bÄ™dziemy wykonywać jedynie operacjÄ™ porównania, używamy najlepszego w tej sytuacji hasha. Struktura dziaÅ‚a w mniej wiÄ™cej ten sposób, że dla każdej wartoÅ›ci można wygenerować jeden z n kluczy (zwykle dostÄ™pnych jest kilka tysiÄ™cy kombinacji). Kiedy chcemy wyszukać, powiedzmy rekord, który w naszym polu ID bÄ™dzie mieć wartość "miecio jest gÅ‚upi", odpowiedni algorytm obliczy na jej podstawie klucz. NastÄ™pnie algorytm wyszukujÄ…cy przeanalizuje tylko te rekordy, których klucz jest równy temu otrzymanemu. Pozwala to już na starcie odrzucić caÅ‚e mnóstwo rekordów, przyspieszajÄ…c tym samym całą operacjÄ™ (jeÅ›li kluczy jest 8000, rekordów 16000, to Å›rednio na każdy z nich przypadnÄ… 2 rekordy. Zatem wyszukiwarka bazy bÄ™dzie musiaÅ‚a sprawdzić tylko je, a z pozostaÅ‚ymi 15998 da sobie spokój). Dodam, iż algorytmu haszujÄ…cego używa również samo PHP do obsÅ‚ugi tablic.

Do góry

Zaczynamy kodowanie

Najpierw stworzymy bibliotekÄ™ lib.kody.php odpowiedzialnÄ… za obsÅ‚ugÄ™ kodów. Jej pierwszy kawaÅ‚ek ma za zadanie wybrać odpowiedniÄ… bazÄ™ danych:

<?php
   define('DB_MYSQL', 0);
   define('DB_POSTGRES', 1);
 
   //$db = DB_MYSQL;
   $db = DB_POSTGRES;
 
   if($db == DB_MYSQL){
      mysql_connect('localhost', 'root', '');
      mysql_select_db('artykuly');
   }else{
      pg_connect('host=localhost port=5432 user=zyx dbname=artykuly');
   }

UstawiajÄ…c zmiennej $db wartoÅ›ci DB_MYSQL albo DB_POSTGRESQL, możemy decydować, której bazy użyje biblioteka.

Drugim etapem jest stworzenie tablicy zawierajÄ…cej "wyglÄ…d" naszego fontu z cyferkami. Zawierać ona bÄ™dzie 10 elementów, po jednym dla każdej cyfry. W nich znajdzie siÄ™... kolejna tablica :), tym razem dwuwymiarowa, opisujÄ…ca, w których miejscach, poczÄ…wszy od lewego górnego rogu, pojawiać siÄ™ majÄ… kropki, które później uÅ‚ożą siÄ™ w ksztaÅ‚t cyfry. 1 oznaczać bÄ™dÄ… kropki, 0 - puste przestrzenie.

 $font = array(0 =>
array(0,1,1,0,1,0,0,1,1,0,0,1,1,0,0,1,1,0,0,1,0,1,1,0),
array(0,0,0,1,0,0,1,1,0,1,0,1,1,0,0,1,0,0,0,1,0,0,0,1),
array(0,1,1,0,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,1,1,1,1),
array(0,1,1,0,1,0,0,1,0,0,1,0,0,0,1,0,1,0,0,1,0,1,1,0),
array(1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,0,0,1,0,0,0,1,0),
array(1,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,0,0,1,0,1,1,0),
array(0,1,1,1,1,0,0,0,1,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0),
array(1,1,1,1,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,0,1,0,0),
array(0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0),
array(0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,1,0,0,0,1,1,1,1,0));

Zadaniem pierwszej funkcji bÄ™dzie pobranie adresu IP użytkownika. Aktualnie dokonuje ona jego konwersji na zwykłą liczbÄ™. Ty możesz pokusić siÄ™ także o dodanie tutaj kontroli banów (hosty, adresy itd.). Nie pobieramy tzw. adresu proxy, ponieważ bardzo Å‚atwo jest nim manipulować.

   function pobierz_ip(){
      global $IP;
 
      $IP['guest'] = $_SERVER['REMOTE_ADDR'];
      $IP['lguest'] = ip2long($IP['guest']);
   } // end pobierz_ip(); 

Wszystko zapisujemy do tablicy $IP. Do konwersji tekstowego adresu na postać liczbowÄ… używam funkcji ip2long(). JeÅ›li przetworzenie nie bÄ™dzie możliwe (np. nie podamy adresu IP :)), zostanie zwrócona wartość -1.

Druga funkcja będzie wywoływana przy tworzeniu formularza. Jej zadaniem jest wygenerowanie losowego kodu oraz jego ID, a następnie wprowadzenie tego do bazy:

   function generuj_kod(){
      global $IP, $db;
      if($IP['lguest'] == -1){
         return 0;
      }
 
      $id = md5(uniqid(time().$IP['guest']));
      $kod = str_pad((string)rand(0, 999999), 6, '0', STR_PAD_LEFT);
 
      if($db == DB_MYSQL){
         mysql_query('INSERT INTO kody VALUES(''.$id.'', ''.$kod.'', ''.time().'', ''.$IP['lguest'].'')');
      }else{
         pg_query('INSERT INTO kody VALUES(''.$id.'', ''.$kod.'', ''.time().'', ''.$IP['lguest'].'')');
      }
      return $id;
   } // end generuj_kod(); 

Do utworzenia ID wykorzystaÅ‚em zwykłą funkcjÄ™ md5() w połączeniu z uniqid(), time() oraz adresem IP. Kod generujÄ™ przy pomocy funkcji rand(), ustawiwszy jej zakres losowania od 0 do 999999. Jednak w ten sposób zostanie zwrócona liczba, która nie zawsze bÄ™dzie miaÅ‚a 6 znaków dÅ‚ugoÅ›ci (np. dla 356 otrzymamy tylko trzy znaki). PoczÄ…tek trzeba wiÄ™c koniecznie uzupeÅ‚nić zerami. Tu przydaje siÄ™ nam funkcja str_pad(). Informujemy jÄ…, że ciÄ…g wynikowy ma mieć 6 bajtów dÅ‚ugoÅ›ci, a ewentualne braki należy uzupeÅ‚nić na jego poczÄ…tku zerami. Na koniec wprowadzamy wszystko do bazy i zwracamy ID.

Kolejna funkcja pełni rolę pomocniczą - jej zadaniem jest umieszczenie na obrazku pojedynczej cyfry:

   function rysuj_numer($img, $number, $x, $y, $col){
      global $font;
      for($mx = 0; $mx < 4; $mx++){
         for($my = 0; $my < 6; $my++){
            if($font[$number][(($my )* 4)+$mx] == 1){
               imagefilledellipse($img, $x + ($mx * 6)+rand(0,1), $y + ($my * 6)+rand(0,1), 2,2, $col + rand(0, 13));
            }
         }
      }
   } // end rysuj_numer(); 

Kropki reprezentowane sÄ… tu jako niewielkie, wypeÅ‚nione kóÅ‚ka, które w dodatku nie leżą na tej samej osi! Przy ustawianiu takowych na obrazku, użyte sÄ… funkcje losowe, które mogÄ… (acz nie muszÄ…) przesunąć pozycjÄ™ kropki o jeden piksel w prawo albo w dóÅ‚. DziÄ™ki temu dwie te same cyfry bÄ™dÄ… różnić siÄ™ nie tylko kolorem poszczególnych kropek (ten także jest losowany i tu, i we wÅ‚aÅ›ciwym algorytmie), ale i samym wyglÄ…dem, co jeszcze bardziej utrudni robotÄ™ programom OCR.

Teraz serce mechanizmu - generowanie obrazka. PoczÄ…tek to typowa robota papierkowa - zassanie kodu na podstawie ID i sprawdzenie adresów IP:

   function generuj_obraz($id){
      global $IP, $db;
 
      if(strlen($id) != 32){
         return 0;
      }
 
      // pobierz kod z podanego ID, zweryfikuj adresy IP
      if($db == DB_MYSQL){
         $r = mysql_query('SELECT kod FROM kody WHERE id = ''.mysql_real_escape_string($id).'' AND guest_ip=''.$IP['lguest'].''');
         if(mysql_num_rows($r)){
            $row = mysql_fetch_row($r);
         }else{
            return 0;
         }
      }else{
         $r = pg_query('SELECT kod FROM kody WHERE id = ''.pg_escape_string($id).'' AND guest_ip=''.$IP['lguest'].''');
         if(pg_num_rows($r)){
            $row = pg_fetch_row($r);
         }else{
            return 0;
         }
      }

Teraz tworzymy obrazek (uwaga dla "starych" wyjadaczy biblioteki GD - do jego stworzenia musimy użyć funkcji imagecreatetruecolor(), jeżeli nie chcemy mieć pokopanej kolorystyki).

      // generuj obraz
      $img = imagecreatetruecolor(200, 50);
      imagefill($img, 1, 1, 0xFFFFFF - rand(0,100));

TÅ‚u również nadajemy losowy kolor, po czym zamalowujemy je ukoÅ›nymi liniami i coraz mniejszymi prostokÄ…tami o odcieniach żóÅ‚tego i błękitnego (losowych, ma siÄ™ rozumieć):

      for($i = 1; $i <= 10; $i++){
         imagerectangle($img, $i * 10, $i *2, 200 - ($i * 10), 50 - ($i * 2), 0x56DDC6 + rand(-50, 50));
      }
 
      for($i = 1; $i < 10; $i++){
         imageline($img, 0, $i*5, 199, ($i-1) *5, 0xFFFF33 + rand(-10, 20));
      }

Do rysowania kropek użyjemy predefiniowanej tablicy kolorów, by zmniejszyć prawdopodobieÅ„stwo takiego dobrania barw, że obraz stanie siÄ™ nieczytelny.

      $color_array = array(0 => 0xFF0000, 0x00AA00, 0x0000FF, 0x000000, 0xFF00FF);
      
      for($i = 0; $i < 6; $i++){
         rysuj_numer($img, (string)$row[0]{$i}, 20 + $i * 30, 6 + rand(0, 10),$color_array[rand(0,4)]);
      }

Na koniec informujemy przeglądarkę, że nadchodzi obrazek, po czym wysyłamy nasze wypociny:

      header('Content-type: image/png');
      echo imagepng($img);
   } // end generuj_obraz(); 

Ostatnia z funkcji zajmuje siÄ™ czyszczeniem tablicy kodów oraz samym ich sprawdzaniem:

   function sprawdz_kod($id, $kod){
      global $IP, $db;
 
      if(strlen($id) != 32){
         return -1;
      }
 
      // pobierz kod z podanego ID, zweryfikuj adresy IP
      if($db == DB_MYSQL){
         mysql_query('DELETE FROM kody WHERE czas < '.(time() - 3600));
 
         $r = mysql_query('SELECT id FROM kody WHERE id = ''.mysql_real_escape_string($id).'' AND kod = ''.mysql_real_escape_string($kod).'' AND guest_ip=''.$IP['lguest'].''');
 
         if(mysql_num_rows($r)){
            return 1;
         }else{
            return 0;
         }
      }else{
         mysql_query('DELETE FROM kody WHERE czas < '.(time() - 3600));
         $r = pg_query('SELECT id FROM kody WHERE id = ''.pg_escape_string($id).'' AND kod = ''.pg_escape_string($kod).'' AND guest_ip=''.$IP['lguest'].''');
         if(pg_num_rows($r)){
            return 1;
         }else{
            return 0;
         }
      }
   } // end sprawdz_kod();
 
?>

To koniec biblioteki. Teraz przydadzÄ… nam siÄ™ szablony HTML:

<?php
 
   function lay_header($title){
echo <<<EOF
<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-2"/>
      <title>{$title}</title>
   </head>
   <body>
EOF;
   } // end lay_header();
   
   function lay_footer(){
echo <<<EOF
<br/>
<div align="center">&copy; Zyx, Webcity.pl</div>
</body>
</html>
EOF;
   } // end lay_footer();
   
   function lay_form($id){
echo <<<EOF
<table width="50%" border="0">
<form method="post" action="formularz.php?co=przetworz">
<input type="hidden" name="id" value="{$id}"/>
<tr>
   <td width="50%">Przepisz sześć cyfr z widocznego tu obrazka do poniższego pola.</td>
   <td width="50%"><img src="formularz.php?co=img&id={$id}"/>
</tr>
<tr>
   <td colspan="2"><input type="text" name="code" maxlength="6"/></td>
</tr>
<tr>
   <td colspan="2"><input type="submit" value="Wyślij"/></td>
</tr>
</form>
</table>
EOF;
   } // end lay_form();
 
?>

Przyjrzyj się funkcji lay_form(). Podajemy jej ID kodu, a ten umieszczany jest w znaczniku <img> razem z adresem URL do skryptu PHP, oraz w polu "hidden", by nie uległ zatraceniu :).

Kod pliku formularz.php wyglÄ…da tak:

<?php
 
   require('./lib.kody.php');
   require('./layout.php');
 
   pobierz_ip();
 
   switch($_GET['co']){
      case 'przetworz':
         lay_header('Wyniki');
         if(sprawdz_kod($_POST['id'], $_POST['code'])){
            echo 'Kod poprawny. Dziękujemy!<br/>';
         }else{
            echo 'Wprowadziłeś niewłaściwy kod!<br/>';
         }
         lay_footer();
      case 'img':
         generuj_obraz($_GET['id']);
         break;
      default:
         lay_header('Formularz');
         lay_form(generuj_kod());
         lay_footer();
   }
?>

Wyraźnie widać, gdzie umieszczone sÄ… które funkcje i chyba Å‚atwo siÄ™ domyÅ›lić, co one robiÄ…? Gratulacje - możesz teraz uruchomić skrypt i zobaczyć, co nie dziaÅ‚a :).

Do góry

Zakończenie
Mam nadzieję, że artykuł nakierował Cię na właściwą drogę i wskazał, jak rozwiązać sam problem z klasą. Pamiętaj jednak, że to jest jedynie szkielet biblioteki. Należałoby w nim zrobić jeszcze porządną obsługę Magic Quotes, a także zwiększyć walory estetyczne generowanych kodów. Nie gwarantuję także, że nie istnieje program OCR, który nie odczytałby naszego obrazka. Po prostu patrz, jak problem rozwiązano na różnych stronach i eksperymentuj. To najlepsza droga do nauczenia się.

Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com

Do góry

Waszym zdaniem:

kukix :: 20.08.2007, 00:15 :: #92

jak dla mnie bomba... pozdr..

kukix :: 20.08.2007, 00:15 :: #93

jak dla mnie bomba... pozdr..

kukix :: 20.08.2007, 18:43 :: #94

Skrypck już podpięty pod księge a już mam dwa wpisy z linkami do stron porno.. Nie wiem, czy ktoś sie uparł i ręcznie wpisuje...
Swoją droga, ciekawe, skąd roboty wiedzą, gdzie wpisywać ten kod... (już nie wspomne o odczytaniu tych kropek :) )

Twoim zdaniem:

Reklama

banner

Partnerzy

CityDesign.pl
phpSolutions