poniedziałek, 24 sierpnia 2015

PHP a bezpiecznie połączenie z bazą danych MySQL

Wstęp


W sieci web można dziś łatwo znaleźć obfitą ilość instruktaży dotyczących metod łączenia się z bazą danych MySQL z poziomu przeglądarki internetowej za pomocą języka php. Większość z nich to jednak metody bardzo ogólne i podstawowe, generalnie bazujące na oprogramowaniu strukturalnym, bez odpowiedniego podejścia do kwestii bezpieczeństwa kodu. Tego typu tutoriale są często źródłem złych nawyków programistycznych, które stają się barierą w rozwoju dalszych umiejętności. Ponieważ kod przedstawiony w danym instruktarzu działa, większość początkujących programistów jest przekonana o jego zupełniej prawidłowości i pełnej efektywności. Wiele z przykładów przedstawianych w popularnych instruktażach związanych z tematem, to metody, które w środowisku produkcyjnym raczej by się długoterminowo nie sprawdziły i na pewno w końcu by zawiodły. W niniejszym artykule postaram się przedstawić temat korzystania z bazy danych MySQL za pomocą języka php z perspektywy bezpieczeństwa i efektywności kodu, skupiając się na metodzie OOP (programowanie obiektowe) z zastosowaniem klasy PDO. W celu ułatwienia czytelnikom zrozumienia omawianej tutaj tematyki, przytoczę konkretne i sprawdzone przeze mnie przykłady. Artykuł jest skierowany do czytelników z dobrą znajomością php i przynajmniej podstawową wiedzą dotyczącą MySQL oraz programowania obiektowego.

Klasa PDO


Klasa PDO oferuje programistom php szeroki wachlarz możliwości jeśli chodzi o bazy danych, stanowiąc swego rodzaju abstrakcyjną powłokę dostępu do danych. PDO zapewnia dość wysoki poziom bezpieczeństwa przy minimalnej ilości kodu, a jednocześnie skrypty wykonywane są bardzo szybko. Stosowanie odpowiednich sterowników PDO umożliwia współpracę klasy z rozmaitymi systemami baz danych. Na dzień dzisiejszy wspierane są następujące rozwiązania bazodanowe:

IBM DB2
ODBC v3 (IBM DB2, unixODBC and win32 ODBC)
PostgreSQL
Cubrid
FreeTDS / Microsoft SQL Server / Sybase
Firebird
SQLite 3 and SQLite 2
Microsoft SQL Server / SQL Azure
4D IBM Informix Dynamic Server
MySQL 3.x/4.x/5.x
Oracle Call Interface

PHP PDO
PHP PDO


Niezależnie od stosowanego sterownika bazy danych, w każdym przypadku odczyt i manipulacja danymi odbywają się za pomocą identycznych funkcji tej klasy, co pozwala na szybkie opanowanie dostępnych w klasie narzędzi i stosowanie ich w pracy z różnymi rodzajami baz danych.

PHP a bezpieczne połączenie z bazą danych MySQL


Za pomocą klasy PDO można korzystać z dobrodziejstw MySQL z poziomu przeglądarki internetowej w prosty i bezpieczny sposób. Postaram się wyjaśnić jak to zrobić na podstawie prostego przykładu kartoteki kontrahentów. Aby rozpocząć, należy w dowolny sposób utworzyć bazę danych o nazwie kartoteka oraz prostą tabelę o nazwie kontrahenci. Dla jasności przykładu posłużę się w tym celu klientem MySQL MariaDB w środowisku Scientific Linux 7. Bazę i tabelę można oczywiście utworzyć w dowolny sposób, np. korzystając z interfejsu phpMyAdmin, bądź podobnego oprogramowania.
MariaDB [(none)]> create database kartoteka;
Query OK, 1 row affected (0.00 sec)

MariaDB [(none)]> use kartoteka;
Database changed

MariaDB [kartoteka]> create table kontrahenci (
-> id smallint unsigned not null auto_increment primary key,
-> imie varchar(15) not null,
-> nazwisko varchar(20) not null,
-> adres varchar(25) not null ) engine=InnoDB charset utf8;
Query OK, 0 rows affected (0.06 sec)
Po utworzeniu tabeli o nazwie kontrahenci należy utworzyć użytkownika o przykładowej nazwie mike i nadać mu uprawnienia niezbędne do odczytu i zapisu danych:
MariaDB [kartoteka]> grant usage on *.* to mike@localhost identified by 'kot-dachowiec';
Query OK, 0 rows affected (0.01 sec)

MariaDB [kartoteka]> grant select, insert, update on kartoteka.kontrahenci to mike@localhost;
Query OK, 0 rows affected (0.00 sec)
Warto sprawdzić, czy uprawnienia dla użytkownika mike są odpowiednio ustawione:
MariaDB [kartoteka]> show grants for mike@localhost;
+-------------------------------------------------------------------------------------------------------------+
| Grants for mike@localhost |
+-------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'mike'@'localhost' IDENTIFIED BY PASSWORD '*391D5A941E13141F78608619467FC09552677273' |
| GRANT SELECT, INSERT, UPDATE ON `kartoteka`.`kontrahenci` TO 'mike'@'localhost' |
+-------------------------------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
Teraz można przystąpić do sporządzenia formularza html, którego kod, dla prostoty i jasności przykładu, wygląda następująco:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<html>
<h3>Dodawanie kontrahenta do kartoteki</h3>
<form method="post" action="dodaj.php">
Imię: <input type="text" name="imie" size="15" maxlength="15"> <br /><br />
Nazwisko: <input type="text" name="nazwisko" size="20" maxlength="20"> <br /><br />
Adres: <input type="text" name="adres" size="25" maxlength="25"> <br /><br />
<input type="submit" value="Dodaj">
</form>
</html>
Powyższy formularz ma za zadanie przesyłanie wprowadzonych do niego danych metodą POST do skryptu dodaj.php. Kod html formularza zawiera tylko te elementy, które są niezbędne dla zrozumienia przedstawianego tutaj przykładu, nie zawiera natomiast elementów takich jak style css, bądź innych elementów poprawiających wygląd czy funkcjonalność.

W pliku o przykładowej nazwie db_ini.php należy umieścić zmienne umożliwiające dostęp do bazy danych:
<?php
// Dostęp do bazy danych
$host = 'localhost';
$db = 'kartoteka';
$user = 'mike';
$password = 'kot-dachowiec';
?>

A oto kod skryptu dodaj.php:
<?php
// Dostęp do bazy danych
include('db_ini.php');
try
{
// Nawiązanie połączenia z bazą danych MySQL
$conn = new PDO("mysql:host={$host};dbname={$db}", $user, $password,
  array(PDO::MYSQL_ATTR_INIT_COMMAND=>"SET NAMES utf8",
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

// Dodawanie nowego wiersza danych
$dodaj = $conn->prepare("insert into kontrahenci (imie, nazwisko, adres) values(:imie,:nazwisko,:adres)");
$dodaj->execute(array(':imie'=>$_POST['imie'],':nazwisko'=>$_POST['nazwisko'],':adres'=>$_POST['adres']));

// Zakończenie połączenia
$conn = null;
}

catch (PDOException $e) {
// Obsługa wyjątków
print "Błąd: " . $e->getMessage() . "<br/>";
die();
}
?>
Skrypt dodaj.php ma za zadanie nie tylko wprowadzenie przesłanych za pomocą formularza html danych do tabeli MySQL, lecz także zapewnienie podstawowego poziomu obsługi wyjątków, którą można w prosty sposób dopasować do warunków typowego środowiska produkcyjnego. Skrypt powoduje dodanie nowego wiersza (rekordu) do tabeli kontrahentów. W przypadku wystąpienia wyjątku zostają wykonane polecenia z sekcji catch, którą można dowolnie rozbudować na miarę własnych potrzeb. Należy zwrócić uwagę na specyficzne atrybuty połączenia $conn. Po pierwsze, ponieważ tabela kontrahentów obsługuje system kodowania znaków utf-8, połączenie zostaje utworzone właśnie z obsługą tego standardu kodowania - MYSQL_ATTR_INIT_COMMAND=>"SET NAMES utf8" - co umożliwia prawidłową obsługę polskich czcionek. Po drugie, ponieważ skrypt dodaj.php ma zapewnić podstawową obsługę wyjątków, połączenie zostaje nawiązane z atrybutem PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION. W przypadku ustawień obsługi wyjątków jak w przykładzie powyższego skryptu dodaj.php, pojawienie się wyjątku nie tylko daje o sobie znać w zdefiniowany przez programistę sposób, czyli w postaci wyświetlonej informacji, ale także rozpoczęta przez skrypt transakcja dodawania wpisu zostaje automatycznie wycofana, co zapewnia dobre zabezpieczenie przed skutkami ewentualnych błędów. Przykładowo, jeśli programista przeliteruje w skrypcie nazwę kolumny 'imie' tabeli 'kontrahenci', to transakcja zostanie wycofana, a jednocześnie wyświetli się następujący komunikat:
Błąd: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'imse' in 'field list'
Przechwytywanie wyjątków daje duże możliwości programistyczne, przy jednoczesnej minimalizacji kodu.

Co bardzo istotne dla każdego świadomego programisty php, wszelkie zapytania przekazywane do MySQL poprzez klasę PDO nie stanowią jakiejkolwiek luki bezpieczeństwa. W celu uzyskania maksymalnego poziomu bezpieczeństwa, PDO umożliwia stosowanie sparametryzowanych zapytań poprzez ich przygotowanie metodą prepare oraz powiązanie z wartościami zmiennych przekazanych w tablicy poprzez metodę execute (jak w niniejszym przykładzie). W celu zachowania wysokiego poziomu bezpieczeństwa zapytań PDO można równie dobrze korzystać z metod ::bindParam lub ::bindValue. Nie ma konieczności korzystania z dodatkowych narzędzi typu mysql_real_escape_string() w przykładowym celu zapobiegania atakom sql injection. Klasa PDO eliminuje potrzebę stosowania typowo proceduralnych zabezpieczeń przed różnego rodzaju atakami związanymi z dostępem do baz danych.

W celu uzupełnienia przykładu bezpiecznego zapisu danych, udostępniam przykład odczytu danych z bazy z wykorzystaniem klasy PDO:
<?php
// Dostęp do bazy danych
include('db_ini.php');
try
{
// Nawiązanie połączenia z bazą danych MySQL
$conn = new PDO("mysql:host={$host};dbname={$db}", $user, $password,
  array(PDO::MYSQL_ATTR_INIT_COMMAND=>"SET NAMES utf8",
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

// Odczyt i wyświetlanie wierszy danych
$dane=$conn->prepare("select id, imie, nazwisko, adres from kontrahenci");
$dane->execute();
$result = $dane->fetchAll(PDO::FETCH_ASSOC);

// Zakończenie połączenia
$conn = null;
}

catch (PDOException $e) {
// Obsługa wyjątków
print "Błąd: " . $e->getMessage() . "<br/>";
die();
}

// Wyświetlanie wyników
foreach ($result as $kontrahent)
 print "{$kontrahent['id']} - {$kontrahent['imie']} {$kontrahent['nazwisko']}
 {$kontrahent['adres']} <br /&gt";
?>

Brak komentarzy:

Prześlij komentarz