Tutorijal - PHP - Kreiranje forme za prijavu korisnika
Uvod
Da bismo na što bolji način zaokružili uvodnu diskusiju o 'osnovnim tehnikalijama u programskom jeziku PHP' i (što je najvažnije) - da bismo isprobali kako sve funkcioniše u praksi - kreiraćemo formular za prijavu korisnika:
Sistem za prijavu tipično funkcioniše kroz interakciju tri PHP skripte:
prijava.php
- stranica preko koje se unose podaci za prijavuobrada.php
- skripta preko koje se uneti podaci proveravaju u odnosu na podatke iz bazepovezivanje.php
- pomoćna skripta za povezivanje sa bazom
Shodno tome da li je korisnik uneo korektne podatke (ili nije), sistem, u nastavku:
- dozvoljava prijavu korisnika - ukoliko su podaci u redu (ili)
- onemogućava dalji pristup - ukoliko nisu uneti odgovarajući podaci
Krenimo redom ....
Šta projekat obuhvata?
Sa jedne strane, kreiranje sistema za prijavu korisnika, prilično je jednostavan zadatak, ali - sa druge strane - postoje stvari o kojima se uvek mora voditi računa (a pogotovo u fazi upoznavanja sa back-end programiranjem).
U pitanju je pre svega sigurnost sajta, koja može biti ugrožena na nekoliko načina:
- korisnici (namerno ili slučajno), mogu uneti podatke koji su u stanju da ugroze sajt (i stoga moramo biti pažljivi pri obradi podataka koje su korisnici uneli)
- sa naše strane (kao back-end programeri), moramo biti korektni prema korisnicima, odnosno, moramo se potruditi da zaštitimo podatke koje korisnici predaju na obradu, što se posebno odnosi na korisničke lozinke (koje nikako ne treba čuvati u izvornom obliku!)
Navedenim temama posvetićemo posebnu pažnju (usput ćemo se naravno osvrtati i na druge tehnike koje smo ranije pominjali), a sam projekat biće podeljen na sledeće celine:
- kreiranje forme za prijavu
- objašnjenje postupka za pravilno skladištenje lozinki
- kreiranje i popunjavanje baze podataka sa korisničkim podacima (uz vođenje računa o pravilnom skladištenju lozinki)
- obrada korisničkih podataka (tokom prijave korisnika)
- sprečavanje unošenja nedozvoljenog SQL koda (SQL injection), preko polja za upis
- prosleđivanje informacije o neuspešnom pokušaju prijave (u smeru: od stranice za obradu - prema stranici za prijavu), preko superglobalne promenljive
$_SESSION
Projekat je "zvanično" vezan za izradu stranice za prijavu korisnika, ali, osvrnućemo se i na dodatne tehnike koje se koriste pri "prečišćavanju" primljenih podataka na formama za registraciju, što će čitaocima dati sasvim dovoljno informacija da samostalno izrade formular za registraciju (posle izrade formulara koji opisujemo u članku).
Za sam početak, obavićemo najjednostavniji zadatak - kreiraćemo formu za prijavu.
Forma za prijavu
U prigodno imenovanom folderu koji će sadržati sve skripte koje su vezane za projekat kojim se bavimo, kreirajte prvo datoteku sa nazivom prijava.php
(u pitanju je glavna stranica sa formularom za prijavu), i unesite u datoteku sledeći kod:
Datoteka prijava.php
povezana je sa CSS datotekom stil.css
, koja će imati sledeći sadržaj (vezu smo prethodno uspostavili preko taga <link>
):
Pošto smo uspostavili mehanizam za prosleđivanje podataka, spremni smo da se pozabavimo i obradom podataka koje je korisnik prosledio pri pokušaju prijave.
Međutim, pre nego što uopšte pokušamo da korisniku odobrimo pristup, moramo primetiti sledeće:
- sistem za prijavu podrazumeva pristup bazi podataka sa korisničkim podacima (ali - baza još uvek ne postoji)
- unos lozinki u izvornom obliku, predstavlja veliki sigurnosni rizik
Prvo ćemo se pozabaviti tematikom pravilnog skladištenja lozinki u izmenjenom obliku (a "usput" ćemo diskutovati i o obradi drugih podataka koji se unose u sistem preko formulara).
Obrada podataka iz formulara i skladištenje lozinki u bazama podataka
Zapisivanje lozinki u bazama podataka, doslovno u obliku u kome ih korisnici unose, predstavlja veliki sigurnosni rizik!
Ukoliko se desi da neko neovlašćeno pristupi bazi podataka sa korisničkim podacima, u kojoj se lozinke skladište u izvornom obliku (prosto rečeno, ako neko "hakuje" bazu i preuzme sadržaj tabela), imaće direktan uvid u podatke za prijavu većeg broja korisnika i biće u stanju da se prijavi na bilo koji od naloga, bez imalo dodatnog truda - čime se potencijalno može izazvati velika šteta.
Da bismo sprečili neovlašćeni pristup podacima (ili makar da bismo otežali neovlašćeni pristup podacima, koliko god je moguće), lozinke se ne skladište neposredno, već, u kriptovanom ("haširanom") obliku, koji nastaje upotrebom posebnih funkcija (koje će biti tema narednih poglavlja).
U pitanju je sigurnosni mehanizam koji, za bilo koju unetu nisku znakova, kreira nisku od 60 naizgled nasumičnih znakova (koja odgovara unetoj lozinki), čime se postiže - "ako ništa drugo" - da lica koja su neovlašćeno pristupila podacima iz baze ne mogu podatke za prijavu da (is)koriste neposredno.
Način kreiranja kriptovanog oblika koji ćemo prikazati, nije ni izdaleka komplikovan, * ali, to (nažalost) nije jedino o čemu se mora voditi računa.
Pre nego što se lozinka kriptuje, moramo biti sigurni da sam podatak ne sadrži specijalne znakove koji se mogu umetnuti preko HTML formulara, promeniti tok izvršavanja skripte i dovesti do gubitka podataka (ili našteti sistemu i korisnicima na drugi način).
Naime, budući da se lozinke (kao i razni drugi podaci), unose preko HTML formulara (formulara za registraciju / prijavu / pretragu / online kupovinu / objavu komentara i sl), pri čemu su formulari tipično dostupni javno, nije teško uvideti, da - u većini situacija - "bilo ko" može uneti "bilo šta" u neki od pomenutih formulara.
Prethodno navedeni pristup otvara mogućnosti zloupotrebe, i stoga je zadatak back-end programera da preduprede pokušaje unošenja neovlašćenog SQL ili HTML koda u sistem za obradu podataka.
"Sanitacija" korisničkih podataka - sprečavanje ugrađivanja nedozvoljenog SQL i HTML koda (SQL i HTML injection)
Postoji pravilo koje navodi da "ne treba verovati" podacima koje korisnici unose preko formulara, i to pravilo (praksa je pokazala) - treba uvek poštovati!
Bez preduzimanja posebnih koraka, sistem za obradu korisničkih zahteva (kome se prosleđuju podaci uneti preko korisničkih formulara), ne prepoznaje znake kao što su '
(apostrof) i "
(navodnici), kao opšte podatke iz upita, već - kao deo sopstvenog SQL koda, što (kao što smo prethodno nagovestili), predstavlja poveći problem i otvara mogućnost zloupotrebe.
Uzmimo (primera radi) da se podaci uneti preko <input>
polja (iz HTML forme) prosleđuju sledećem upitu:
U normalnim okolnostima, prikazani upit izvršava se na sledeći način:
Pod uslovom da su uneti podaci u redu (smatraćemo da jesu), sistem u nastavku dozvoljava prijavu.
Međutim, ukoliko neko prosledi sledeće podatke:
.... upit više nije ni najmanje (!) bezopasan:
Budući da je sistem "nekritički" prihvatio znak '
(apostrof), navedeni znak je zatvorio nisku 'pera94'
i omogućio da se između korisničkog imena i "pravog" apostrofa koji je unet kao deo upita u PHP skripti, umetne proizvoljan SQL kod:
- umetnuti kod je prepravio smisao upita ("dozvolio je prijavu" preko bilo kog postojećeg korisničkog imena (u gornjem primeru,
pera94
)) - takođe je poništen ostatak upita (
AND lozinka='$lozinka'
), prostim stavljanjem ostatka niske pod komentar (u različitim implementacijama SQL-a, komentar tipično započinje niskom sa dve crtice--
)
Srećom, ovakvi pokušaji 'obijanja', mogu se preduprediti prilično lako.
Dovoljno je da se unete niske 'provuku' kroz dve specijalizovane funkcije za "sanitaciju":
mysqli_real_escape_string
je funkcija koja ukida funkcionalnost znakova koji su deo SQL kodahtmlentities
je funkcija koja ukida funkcionalnost znakova koji su deo HTML koda
Preko sledećeg koda ....
.... postiže se da uneti podatak bude tretiran kao obična niska (gornja skripta, zarad preglednosti, sadrži samo obradu vezanu za lozinku, ali, svakako je potrebno da se prikazani proces sanitacije obavi i za sve ostale unete podatke).
Najprostije rečeno, od niski ....
.... obradom preko funkcije mysqli_real_escape_string
(za prvu nisku) i htmlentities
(za drugu), nastaju niske:
Budući da prva niska sada sadrži odgovarajuće escape sekvence za specijalne znakove (koje sugerišu SQL-u da znakove koji inače imaju specijalno značenje, treba tretirati kao obične znakove), dok su u drugoj niski pojave HTML znakova zamenjene odgovarajućim HTML entitetima (što neće pokrenuti kreiranje HTML/DOM objekata) - može se smatrati je bezbedno koristiti obrađene niske u daljem toku izvršavanja skripte.
Međutim, napomenućemo i to da ne treba smatrati da je svako unošenje apostrofa u HTML polja "pokušaj hakovanja", jer (recimo) sledeće niske ....
.... predstavljaju poznata imena koja se svakodnevno "provuku" kroz internet pretraživače više desetina hiljada puta. *
Upravo je zato naša obaveza da udesimo (preko funkcija koje smo naveli), da apostrofi budu svedeni na escape sekvence \'
(što će biti prepoznato kao običan znak koji je deo unetog podatka, a ne kao znak koji ima posebno značenje u jeziku SQL) - čime se praktično omogućavaju "legitimni" vidovi upotrebe apostrofa na formularima.
Provera unetog podatka u odnosu na očekivani obrazac (preg_match)
Postupak za sanitaciju koji smo prikazali (ili postupak preko pripremljenih iskaza koji ćemo tek prikazati), obavezno se primenjuje na svaki uneti podatak (i tokom registracije, i tokom prijave korisnika), ali - pri registraciji - pažnju treba posvetiti i tome da podaci koje korisnik prosleđuje obavezno budu uneti u odgovarajućem formatu (dok, u formularima za prijavu, tipično možemo pustiti korisnike da "pišu šta god žele").
U "idejnom smislu", ne želimo da dopustimo korisnicima da pišu "bilo šta", ali:
- sa jedne strane, praktično se ne može "zabraniti" korisnicima da unose proizvoljne podatke u polja za unos
- sa druge strane, podaci svakako prolaze proces "sanitacije"
Takođe, pri obavljanju prijave, nije bitno da li se neki od unetih podataka poklapa sa predviđenim formatom, * već je samo bitno da li uneti podaci odgovaraju konkretnim podacima iz baze podataka, ** i stoga bi proces provere formata podataka koji su prosleđeni iz forme - samo nepotrebno opteretio skriptu za obradu unetih podataka (u meri koja ni izdaleka nije "zabrinjavajuća", ali, svejedno težimo tome da programski kod uvek bude optimalan).
Preko regularnih izraza (koji se koriste na formama za registraciju), nije teško proveriti da li podaci poštuju određene standarde:
- uneta imena i prezimena mogu sadržati samo znakove ćirilice i latinice, razmake i crtice
- korisnička imena moraju imati odgovarajuću (minimalnu) dužinu i biti sačinjena od odgovarajućih znakova
- dužina lozinki mora biti minimalno osam znakova (tipična vrednost) i, takođe, lozinke moraju sadržati sve kategorije znakova (velika i mala slova, cifre, specijalne znakove) *
- brojevi telefona mogu sadržati samo cifre, kose i ravne crte, i moraju biti formatirani na propisan način
- e-mail adrese takođe moraju imati pravilan format ("korisničko_ime@domen", bez nedozvoljenih znakova)
Pogledajmo neke od regularnih izraza koji odgovaraju prethodno nabrojanim zahtevima (sa prikazanim izrazima upoznali smo se (u najvećoj meri), * u članku o regularnim izrazima):
Za proveru u PHP-u, koristi se funkcija preg_match
(pattern/regular expression match - funkcija koja vraća vrednost true
ako se uneta niska poklapa sa navedenim obrascem, ili vrednost false
, ako se niska ne poklapa sa obrascem), a za primer ćemo uzeti proveru unete e-mail adrese.
Posle provere unetog podatka preko funkcija mysqli_real_escape_string
i htmlentities
(što smo već obavili), proverićemo i da li je e-mail adresa formatirana pravilno.
Funkciji pregmatch
može se proslediti regularni izraz sa slike #14 (u svojstvu argumenta), ali, tipično se u PHP-u zadatak provere e-mail adrese obavlja uz korišćenje još jedne funkcije:
- funkcija
preg_match
proverava (samo) da li niska sadrži nedozvoljene znake - funkcija
filter_var
proverava da li se niska podudara sa određenim formatom
Sledeći kod ....
.... obaviće zadatak provere e-mail adrese.
Na ovom mestu verovatno ćete se zapitati (reklo bi se, sasvim opravdano), zašto je uopšte potrebno koristiti funkciju preg_match
, budući da funkcija filter_var
proverava da li se niska uklapa u obrazac za formatiranje e-mail adresa?!
Odgovor je jednostavan, ali, istovremeno .... "pomalo čudan".
Funkcija filter_var
(zaista) proverava da li se uneta niska poklapa sa obrascima za formatiranje email adresa, ali, pri tom se poštuju međunarodni standardi stari preko 25 godina, koji dopuštaju da se u email adresi nađu i znakovi kao što su #
, !
, ~
i slični.
Dakle, "što se tiče funkcije filter_var", niska korisnik@domen.rs
formatirana je kao email adresa (što je tačno), ali je isto tako i niska k0r!$n1k#@d0m#n.r$
- takođe formatirana kao email adresa (što možda jeste u skladu sa prvobitnim standardima, ali, nikako nije nešto što se sreće u praksi).
U svakom slučaju, upotrebili smo sve mehanizme provere i zaštite koji se razumno mogu predvideti, i sada ("tek sada"), kada smo (konačno!) "prečistili" unete podatke, možemo ih uneti u tabelu.
Kreiranje kriptovanog oblika lozinke - password_hash()
Za razliku od ostalih podataka, koji - posle "bezbedonosnih provera" - mogu odmah biti upisani u tabelu, lozinke se prvo podvrgavaju procesu enkripcije, po sledećoj proceduri:
- korisnik, pri registraciji naloga, unosi lozinku (obavezno dvaput, zarad provere), nakon čega se uneti podatak prosleđuje skripti za kreiranje naloga.
- uneta lozinka se podvrgava procesu "sanitacije" (ukidanje funkcionalnosti specijalnih znakova)
- lozinka se hash-uje i skladišti u bazi
Sam proces kreiranja "haširanog" oblika lozinke, podrazumeva upotrebu posebnih funkcija koje koriste kriptografske algoritme (koji nisu 'baš skroz jednostavni' za razumevanje, i o njima ćemo diskutovati neki drugi put), ali, procedura za kreiranje hash koda u PHP-u je prilično jednostavna za implementaciju i podrazumeva sledeće korake:
Ako unesemo sledeću lozinku ....
.... posredstvom funkcije password_hash
, od unete lozinke * nastaje sledeći hash kod: **
Drugi argument koji se predaje funkciji password_hash
, predstavlja izabrani metod kriptovanja (PASSWORD_DEFAULT
je standardni (podrazumevani) metod, dok, po želji (najčešće samo zarad kompatibilnosti sa starijim, već postojećim bazama podataka), možemo koristiti i druge metode).
Obrađeni podatak se sada može upisati u bazu, i stoga ćemo se u sledećem poglavlju posvetiti upravo unosu podataka, dok ćemo o proceduri provere unete lozinke (u odnosu na sačuvani hash kod), pisati u pretposlednjem poglavlju (u kome ćemo prikazati celokupnu skriptu za prijavu korisnika).
Podaci za prijavu
Pošto smo se postarali da podaci koje unosimo budu "onakvi kakvi bi trebalo da budu", možemo podatke (zapravo) i upisati u tabelu.
U pitanju je postupak koji se tipično obavlja pri registraciji, ali, pošto članak na eklektičan način obrađuje obe teme: registraciju i prijavu, jednostavno ćemo podatke uneti ručno (za sam početak), da bismo sistemu obezbedili početnu funkcionalnost.
Kreiranje baze podataka sa korisnicima
Prema uputstvima iz članka o povezivanju PHP skripti sa MySql bazama podataka, kreirajte skriptu za povezivanje sa serverom (za početak, nemojte unositi ime baze - iz očiglednih razloga), i potom pokrenite sledeći upit za kreiranje baze podataka (još jednom da se podsetimo: potrebno je smestiti skripte u zajednički folder i dodeliti im prepoznatljiva imena):
Posle kreiranja baze, možemo kreirati i popuniti tabelu sa korisnicima.
Kreiranje tabele sa korisničkim podacima
Prepravite sada skriptu povezivanje.php
, tako da se pri povezivanju koristi i naziv baze ("korisnici"), i kreirajte zatim novu skriptu, preko koje ćemo kreirati tabelu korisnici
:
Sada se tabela može popuniti korisničkim podacima.
Unos korisničkih podataka u tabelu
Podaci preuzeti iz forme za registraciju, mogu se uneti u tabelu korišćenjem odgovarajuće skripte:
U pitanju je skripta kakvu bismo koristili za obradu podataka koji su prosleđeni preko formulara za registraciju, ali, budući da to (zvanično) nije tema ovog članka, koristićemo pojednostavljenu skriptu sa "hardkodiranim" podacima i nećemo obavljati provere (mi nećemo sami sebe "hakovati", a nemojte ni vi):
Sada, kada postoji (bar jedan) korisnički nalog u tabeli sa korisnicima (bez čega mehanizam za prijavu praktično ne može da funkcioniše), možemo se posvetiti izradi skripte za prijavu (što - ako smo usput možda i zaboravili - i dalje "zvanično" predstavlja glavnu temu članka). :)
Obrada podataka za prijavu
Kreirajte datoteku sa nazivom obrada.php
i unesite u datoteku sledeći kod (objašnjenja su u komentarima):
Pred kraj, pogledaćemo kako korisnika možemo obavestiti o neuspešnom pokušaju prijave, vraćanjem na stranicu za prijavu i označavanjem polja sa pogrešno unetim podacima.
Vraćanje podataka stranici za prijavu, preko superglobalne promenljive $_SESSION
Sa jedne strane, može se reći da je krajnje adekvatno da se skripta za obradu unetih podataka (u našem slučaju, skripta obrada.php
), koristi i za ispis poruka o greškama pri prijavljivanju.
Međutim, nepisana pravila modernog web dizajna nalažu, da - u slučaju neuspešnog pokušaja prijave - korisnike treba obaveštavati preko istog formulara preko koga su prvobitno pokušali da obave prijavu (pri čemu, zbog brzine izvršavanja, često deluje da se korisnik nije ni udaljio sa stranice za prijavu).
Sa navedenim pristupom već smo se upoznali u članku o superglobalnoj promenljivoj $_SESSION, pa ovoga puta neće biti teško da ideje koje smo ranije izneli implementiramo u okviru formulara za prijavu (bitni detalji implementacije navedeni su u komentarima):
Sada možemo prepraviti i formu za prijavu (zarad preglednosti nećemo prikazivati celu skriptu):
Osvrnimo se na nekoliko detalja:
- budući da su atributi u
<input>
poljima zapisani pedantno (jedni ispod drugih), lako je bilo zaključiti gde treba "uglaviti" blokove PHP koda - informacije o pojavi grešaka prikazuju se uključivanjem crvenih okvira na
<input>
poljima i ispisom grešaka u pomoćnom<div>
elementu (ispod forme)
Za sam kraj (kao što smo ranije najavili), sledi uvod u moderniji i popularniji način za prosleđivanje upita.
Kratak osvrt na pripremljene iskaze ("prepared statements")
Pripremljeni iskazi (eng. prepared statement(s)), pojavljuju se kao dodatna mogućnost na MySql serverima i praktično predstavljaju 'šablone za izvršavanje upita'.
Način upotrebe je idejno sličan postupku koji smo prethodno primenjivali, * i obuhvata dve etape:
- pripremu i prosleđivanje šablona
- prosleđivanje podataka i izvršavanje upita
U prvoj etapi priprema se šablon (koji nalikuje 'običnom' upitu, ali sadrži i generičke oznake za vrednosti polja (?
)), nakon čega se šablon prosleđuje MySql serveru, što praktično 'otvara mogućnost' da navedeni šablon posluži za kreiranje upita koji će biti pokrenut(i) u nekom kasnijem trenutku.
U drugoj etapi (koja tipično sledi neposredno nakon prve), prosleđuju se realni parametri i pokreće se upit (pri čemu se sanitacija podataka podrazumeva, to jest, odvija se automatski, bez potrebe za eksplicitnim navođenjem naredbi sa kojima smo se ranije upoznali).
Pogledajmo primer:
Kao što vidimo, prve dve komande ne obuhvataju (kao u ranijim primerima), prosleđivanje i pokretanje upita, već: pripremu šablona, i prosleđivanje šablona na MySql server.
U primeru koji smo prikazali (koji predstavlja tipičan tok izvršavanja komandi), u sledećih nekoliko naredbi definišu se promenljive koje sadrže vrednosti polja ('budućeg upita'), nakon čega se poziva funkcija mysqli_stmt_bind_param
, preko koje se navedene PHP promenljive povezuju sa placeholderima ?
iz ranije prosleđenog upita.
Na kraju, preko funkcije mysqli_stmt_execute
, na MySql serveru se pokreće upit (koji je ranije pripremljen).
U nekom kasnijem trenutku, isti objekat ($stmt
), može se koristiti za prosleđivanje (praktično) istog upita, ali - sa drugim parametrima (pri čemu se navode samo 'nove' vrednosti promenljivih).
Na primer:
Preko primera koji smo prikazali, moguće je sagledati opšte 'tehničke karakteristike' pripremljenih iskaza:
- objekat
$stmt
povezuje se sa određenim upitom (u konkretnom primeru, sa upitomINSERT INTO t1(ime, prezime, email) VALUES (?, ?, ?)
) - generičke vrednosti polja iz šablona (
?
), povezuju se sa konkretnim vrednostima pre pokretanja upita
Način povezivanja i pokretanja, ni u kom slučaju nije komplikovan za razumevanje, međutim, potrebno je dodatno se osvrnuti na drugu stavku iz gornje liste:
- pripremljeni iskazi se tipično povezuju sa promenljivama (baš kao u primeru koji smo prikazali)
- u trenutku pokretanja upita, koriste se trenutne vrednosti promenljivih, ali .... upit je i 'nadalje' povezan sa promenljivom
- ukoliko se vrednost određene promenljive promeni, pri sledećem pokretanju upita biće upotrebljena nova vrednost
Summa summarum: sintaksa pripremljenih iskaza je ponešto različita i neznatno kompleksnija (u odnosu na 'ručno prečišćavanje podataka'), ali, pristup je u idejnom smislu gotovo isti (a vredi pomenuti i to da pripremljeni iskazi postoje i u drugim popularnim jezicima, i koriste se na vrlo sličan način).
Za kraj ....
Za kraj ostaje da vam ponovo predložimo da isprobate tehnike koje smo opisali, kroz sledeće primere:
- probajte (ponovo) da kreirate formular za prijavu korisnika, ali - ovoga puta samostalno
- probajte da predate upite preko pripremljenih iskaza (bar ponegde)
- probajte da samostalno kreirate formular za registraciju korisnika
Takođe, ukoliko želite da isprobate formular koji smo izrađivali u članku, možete ispratiti sledeći link (ukoliko već niste):
Formular za prijavu korisnikaPodaci za prijavu su oni koje smo koristili u članku.