Webcity.pl


  Własny mechanizm ses...

  Galeria w PHP

  SimpleXML nadchodzi!

  Obsługa MySQL'a w PH...

  Cachowanie zapytań S...

 

 02.02.06 - [new] E-video - artykuł

 30.01.06 - [update] Artykuł o sesjach

 18.12.05 - [update] PDO - artykuł

 19.08.05 - [new+upd] Aktualizacja materia...

 26.04.05 - [update] Nowy artykuł

Twój domowy serwer!

  Pomocy WML
[php]Sesje nie działają, ...
[actionscript 2.0] [flash...
Problem z talicami w php
[ocena] llll.pl darmowe a...
Kamera na stronie interne...
Kamera na stronie interne...
[xml] Jak wyciągnąć dane
Wizualizacja w JS - POMOC
Web Developer - stała / f...

... i wiele innych wątków na forum ›

Partnerzy
› allRSS.info - katalog zasobów RSS
› iloveflyer.org - webdesign
› webserv.pl - serverpack
› skryptoteka.pl - mnóstwo skryptów
› vel.pl - hosting
› TelePraca.net - pracuj swobodnie
› PHP Solutions - magazyn PHP
› HELION - wydawnictwo informatyczne

Ksišżka dnia

CityMag
Wpisz swój e-mail, aby zaprenumerować nasz Magazyn, który zawiera najnowsze informacje ze świata i najciekawsze teksty.
 

Szukasz czegoś?
Nasza wyszukiwarka znajdzie wszystko, czego szukasz.
 

Szukanie zawansowane


  Reklama
  Redakcja
  Hosting
  Kanał

© WebCity.pl Team
 
 
  Forum Forum
Kursy Kursy
Porady Porady
Recenzje Recenzje
Newsy Newsy
Katalog stron WWW Katalog
Skrypty PHP Skrypty
Download Oferty i praca
Artykuły:
 Teoria
 Praktyka
 Promocja
 Inne
 

 Webcity.pl |

[PHP] Wprowadź numer widoczny na obrazku

Autor: Zyx
   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:
  • generuj_kod() - jej zadaniem będzie utworzenie 6-cyfrowego kodu.
  • generuj_obraz($id) - generuje obrazek przy pomocy biblioteki GD
  • sprawdz_kod($id, $kod) - sprawdza, czy wprowadzono prawidłowy kod.
    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.

       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.

       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 :).

       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ę.

  • Powrót