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...
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.
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.
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">© 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 :).
Autor: Tomasz "Zyx" Jędrzejewski, www.zyxist.com
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 :) )