Webprogramozás

Munkamenet, hitelesítés

Horváth Győző
Egyetemi docens
1117 Budapest, Pázmány Péter sétány 1/c., 2.408-as szoba
Tel: (1) 372-2500/8469
horvath.gyozo@inf.elte.hu

Ismétlés

Ismétlés

  • Dinamikus szerveroldali webprogramozás
    • Program → HTML
  • Output (HTML generálás)
  • Input (link, űrlap)
  • Adattárolás: fájlok
    • Fájlszerkezet-vezérelt
    • Adatszerkezet-vezérelt
    • Sorosítás
    • Segédosztályok (pl. JsonStorage)

Példa

include('jsonio.php');
include('jsonstorage.php');
include('movierepository.php');
include('userrepository.php');

$movieRepository = new MovieRepository();
$userRepository = new UserRepository();

$userId = $userRepository->insert(["username" => "q"]);
$movieRepository->insert(["title" => "Something", "user_id" => $userId]);
var_dump($movieRepository->all());
class UserRepository extends JsonStorage {
  public function __construct() {
    parent::__construct('./data/users.json');
  }
}
class MovieRepository extends JsonStorage {
  public function __construct() {
    parent::__construct('./data/movies.json');
  }
}

Szerverprogram bemeneti adatai

Hasznos függvények

Szövegműveletek

  • szöveg: karakterek tömbje
  • explode($elválasztó, $s): elemekre bontás
  • implode($elválasztó, $tömb): összefűzés
  • substr($s, $kezdet, $hossz): részszöveg
  • ltrim($s), rtrim($s), trim($s): fehérszóköz-eltávolítás.
  • strstr($miben, $mit), strpos($miben, $mit), strrpos($miben, $mit): részszöveg keresése
  • preg_match($minta, $s): regkif illeszkedése

Dátum és idő

  • date($formátum): az aktuális idő kiírása a megadott formátumban.
  • time(): Unix idő visszaadása másodpercben.
  • strtotime($s): szövegként megadott dátum Unix időbe átalakítása.
  • getdate(): dátuminformációk visszaadása tömbként.
  • date_*: dátummal kapcsolatos további függvények
  • DateTime, DateInterval, DatePeriod osztályok

Dátum és idő

date("Y:m:d G:i:s"); // 2019:11:24 17:18:59
time(); // 1574615959 (s)
strtotime('2019-12-12'); // 1576108800
strtotime('2019-12-12T12:34:12'); // 1576154052
strtotime('now'); // 1574616109
strtotime('last day of next month'); // 1577813072

$d1 = new DateTime('+2 days');
$d2 = new DateTime('-2 weeks');
$interval = $d1->diff($d2);
$interval->format('%R%a days'); // -16 days

$d1 = new DateTimeImmutable('+2 days');
$d2 = $d1->add(new DateInterval('P2Y4DT6H8M'));
$interval = $d2->diff($d1);
$interval->format('%R%a days'); // -735 days

HTTP fejlécek

  • header()
    • HTTP fejlécek leküldése
    • Pl.: header('Location: index.php');
      • átirányítás másik oldalra
function redirect($page) {
  header("Location: index.php?{$page}");
}

Külön fájlba helyezés

  • Funkcionálisan elváló részeket külön fájlokban tárolni
    • átláthatóság
  • Fájlok beemelése:
    • include($fájlnév) – hiba esetén warninggal továbbmegy
    • include_once($fájlnév)
    • require($fájlnév) – hiba esetén hibával megáll
    • require_once($fájlnév)
  • Mintha odamásolnánk a fájl tartalmát

Munkamenet-kezelés

HTTP állapotmentesség

  • A HTTP állapotmentes protokoll
  • Nem emlékezik az előző kérés adataira
  • Függetlenül kezeli a kéréseket
    • Ugyanazon kliens különböző kéréseit
    • Különböző kliensek kéréseit

1. probléma

  • Ugyanazon kliens több kérése között az állapot megtartása
  • Pl. kosár
  • Megoldás: HTTP kéréstől külön tárolni

1. probléma

2. probléma

Mindegyik kliens ugyanazon az adaton osztozkodik

Munkamenet-kezelés

Kliensenkénti adattárolás

Munkamenet-kezelés

  • Kliensek megkülönböztetése
  • Kliensenkénti adattárolás
  • Példák:
    • levelezés, dokumentumok
    • internetbank
    • webáruház kosara, online szerkesztők
  • Megoldás
    • kliens oldalon
    • szerver oldalon

Munkamenet-kezelési lehetőségek

Példa

Tároljuk egy számláló értékét felhasználónként, és minden kérésnél növeljük a számláló értékét eggyel!

Kliens oldali állapottartás

  • Az adatot a kliensen tároljuk
  • Minden kérésnél felküldjük a szerverre
  • A szerver visszaadja a kliensnek
  • Kliens oldali technológiák
    • URL
    • Rejtett mező
    • Süti

URL

session_url.php?counter=1
<?php
print_r($_GET);

$counter = $_GET['counter'] ?? 0;
$counter += 1;

var_dump($counter);
?>
<a href="session_url.php?counter=<?php echo $counter; ?>">Increment</a>

URL

  • Hátránya:
    • Minden linkhez oda kell generálni
      • Ha egyről is lemarad, elvész az adat
    • Sok adat nem fér el benne
      • URL hossza limitált
    • Feltűnő (zavaró)
    • Könnyen átírható
  • Előny
    • Könyvjelzőzhető

Rejtett mező

  • URL: kevés adat, feltűnő, manipulálható
  • → űrlap rejtett mezője

    <input type="hidden" name="counter" value="4">
  • Előny
    • sok adat
    • nem feltűnő
  • Hátrány
    • manipulálható
    • csak űrlapok esetén
    • normál linkeknél JavaScript kell

Rejtett mező

<?php
print_r($_POST);

$counter = $_POST['counter'] ?? 0;
$counter += 1;

var_dump($counter);
?>

<form action="" method="post">
  <input type="hidden" name="counter" value="<?= $counter ?>">
  <button>Increment</button>
</form>

<a href="session_hidden.php">Increment (not working)</a>

Süti

  • Rejtett mező: macerás, manipulálható → süti
  • HTTP kérés és PHP ($_COOKIES)

    Cookie: név1=érték1; név2=érték2; név3=érték3
  • HTTP válasz és PHP:

    Set-Cookie: név=érték[; expires=dátum][; domain=domain][; path=path][; secure]
    // Általános formája
    $siker = setcookie($név[, $érték [, $expires = 0 [, $path [, $domain [, $secure = false]]]]]);
    // Néhány példa
    setcookie('alma', 'piros');
    setcookie('körte', 'sárga', time() + 60); //lejárat 60 mp múlva

Süti

<?php
var_dump($_COOKIE);

$counter = $_COOKIE['counter'] ?? 0;
$counter += 1;
setcookie('counter', $counter);

var_dump($counter);
?>

<a href="session_cookie.php">Increment</a>

Süti

  • Előny
    • nem feltűnő
    • automatikus küldése
  • Hátrány
    • manipulálható (kódolható)
    • limitált adatmennyiség
    • letiltható

Kliensoldali megoldások

  • Adat a kliensen van
  • Manipulálható
  • Sok adat esetén feleslegesen sok adat megy oda-vissza a kliens és szerver között

Szerveroldali megoldások

  • Tároljuk az adatot a szerveren
    • → nem manipulálható kliens oldalon
    • → nem kell sok adatot küldözgetni
  • A kliens megkülönböztetése továbbra is szükséges
  • → tokent kap, amivel azonosítja magát és hozzáfér a tokenhez tartozó adatokhoz
  • Token kliensoldali megoldással közlekedik
    • süti (alapértelmezett)
    • URL (ha nincs süti)

Szerveroldali megoldás

Több kliens

Szerveroldali megoldás

Egy kliens különböző kérései

Szerveroldali megoldás

  • Adatok tárolása
    • fájlban (ld. PHP)
    • adatbázisban
  • Plusz erőforrás a szervertől
  • A tokenre nagyon kell vigyázni!
    • ellopható
    • kilépés

Munkamenet-kezelés PHP-ban

Munkamenet-kezelés PHP-ban

  • Munkamenethez tartozó adatok:
    • $_SESSION
  • Munkamenet-kezelő függvények:
    • session_start()
    • session_destroy()

Példa – számláló

<?php
session_start();
var_dump($_SESSION);

$counter = $_SESSION['counter'] ?? 0;
$counter += 1;
$_SESSION['counter'] = $counter;

var_dump($counter);
?>

<a href="session_php.php">Increment</a>

Munkamenet megszüntetése

session_start();

$_SESSION = [];
session_destroy();

Hitelesítés és jogosultságkezelés

Hitelesítés

  • Ki használja az alkalmazást?
  • → azonosított felhasználó
    • felhasználónév, stb.
  • → névtelen felhasználó
    • vendég

Jogosultság­kezelés

  • Engedélyezett-e a hozzáférése az adott felhasználónak?
  • Bizonyos oldalak, funkciók csak azonosított felhasználók számára érhetőek el

Technológiák

  • .htaccess, .htpasswd
    • könyvtár alapú védelem
  • WWW-Authenticate (HTTP)
    • Módok
      • Basic
      • Digest (titkosított)
    • PHP-ból kiolvasható
  • Ezek PHP-tól független technológiák
  • Plusz adat tárolására nem alkalmasak
  • Csak hitelesítésre

Hitelesítés munkamenettel

  • Azonosított felhasználó munkamenetében egy speciális kulcsot helyezünk el
  • Ezzel jelezzük, hogy már azonosítottuk
  • Folyamat
    • beléptető űrlap
    • sikeres belépés esetén → kulcs
    • minden oldalon: ha ez a kulcs megvan, akkor azonosított
    • ettől függően más logika, más nézet lehet

Segédosztály

class UserRepository extends JsonStorage {
  public function __construct() {
    parent::__construct('./data/users.json');
  }
}

Regisztráció

Űrlap

<?php if (isset($errors['global'])) : ?>
  <p><span class="error"><?= $errors['global'] ?></span></p>
<?php endif; ?>
<form action="" method="post">
  Username:
  <input type="text" name="username">
  <?php if (isset($errors['username'])) : ?>
    <span class="error"><?= $errors['username'] ?></span>
  <?php endif; ?>
  <br>
  Password:
  <input type="password" name="password">
  <?php if (isset($errors['password'])) : ?>
    <span class="error"><?= $errors['password'] ?></span>
  <?php endif; ?>
  <br>
  <button>Register</button>
</form>

Regisztráció

$userRepository = new UserRepository();

function validate($input, &$data, &$errors, $userRepository) {
  // username, password not empty
  // ...

  if (count($errors) === 0) {
    if (user_exists($userRepository, $input['username'])) {
      $errors['global'] = "User already exists";
    } 
  }

  return count($errors) === 0;
}
function user_exists($userRepository, $username) {
  $users = $userRepository->filter(function ($user) use ($username) {
    return $user['username'] === $username;
  });
  return count($users) >= 1;
}
function add_user($userRepository, $user) {
  $user['password'] = password_hash($user['password'], PASSWORD_DEFAULT);
  return $userRepository->insert($user);
}

$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors, $userRepository)) {
    add_user($userRepository, $data);
    header('Location: login.php');
    exit();
  }
}

Beléptetés

Űrlap

<?php if (isset($errors['global'])) : ?>
  <p><span class="error"><?= $errors['global'] ?></span></p>
<?php endif; ?>
<form action="" method="post">
  Username:
  <input type="text" name="username">
  <?php if (isset($errors['username'])) : ?>
    <span class="error"><?= $errors['username'] ?></span>
  <?php endif; ?>
  <br>
  Password:
  <input type="password" name="password">
  <?php if (isset($errors['password'])) : ?>
    <span class="error"><?= $errors['password'] ?></span>
  <?php endif; ?>
  <br>
  <button>Login</button>
</form>

Beléptetés

function validate($input, &$data, &$errors, $userRepository) {
  // username, password not empty
  // ...

  if (count($errors) === 0) {
    if (!check_user($userRepository, $input['username'], $input['password'])) {
      $errors['global'] = "Login error";
    } 
  }

  return count($errors) === 0;
}
function check_user($userRepository, $username, $password) {
  $users = $userRepository->filter(function ($user) use ($username) {
    return $user['username'] === $username;
  });
  if (count($users) === 1) {
    $user = array_values($users)[0];
    return password_verify($password, $user["password"]) 
      ? $user 
      : false;
  }
  return false;
}
function login($user) {
  $_SESSION["user"] = $user;
}

session_start();

$userRepository = new UserRepository();
$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors, $auth)) {
    login($data);
    header('Location: login.php');
    exit();
  }
}

Hitelesítés ellenőrzése

  • munkamenet indítása
  • létezik-e a kulcs a munkamenet adatai között
  • kulcs értéke indifferens
session_start();
function is_authenticated() {
  return isset($_SESSION["user"]);
}

Kijelentkezés

  • Kulcs kivétele a munkamenetből
  • Általában az egész munkamenet megszüntetése (feladatfüggő)
function logout() {
  unset($_SESSION["user"]);
}

Jogosultságvizsgálat

Egyszerűsített

session_start();
if (!is_authenticated()) {
  header("Location: login.php");
  exit();
}

Segédosztály

class Auth {
  private $userRepository;
  public function __construct() {
    $this->userRepository = new UserRepository();
  }
  public function register($user) {
    $user['password'] = password_hash($user['password'], PASSWORD_DEFAULT);
    return $this->userRepository->insert($user);
  }
  public function user_exists($username) {
    $users = $this->userRepository->filter(function ($user) use ($username) {
        return $user['username'] === $username;
    });
    return count($users) >= 1;
  }
  public function login($user) {
    $_SESSION["user"] = $user;
  }
  public function check_credentials($username, $password) {
    $users = $this->userRepository->filter(function ($user) use ($username) {
      return $user['username'] === $username;
    });
    if (count($users) === 1) {
      $user = array_values($users)[0];
      return password_verify($password, $user["password"]) 
        ? $user 
        : false;
    }
    return false;
  }
  public function is_authenticated() {
    return isset($_SESSION["user"]);
  }
  public function logout() {
    unset($_SESSION["user"]);
  }
}

Segédosztály használata

Regisztráció

function validate($input, &$data, &$errors, $auth) {
  // ...
  if (count($errors) === 0) {
    if ($auth->user_exists($input['username'])) {
      $errors['global'] = "User already exists";
    } 
  }
  return count($errors) === 0;
}

$auth = new Auth();
$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors, $auth)) {
    $auth->register($data);
    header('Location: login.php');
    exit();
  }
}

Segédosztály használata

Beléptetés

function validate($input, &$data, &$errors, $auth) {
  // ...
  if (count($errors) === 0) {
    if (!$auth->check_credentials($input['username'], $input['password'])) {
      $errors['global'] = "Login error";
    } 
  }

  return count($errors) === 0;
}

session_start();

$auth = new Auth();
$data = [];
$errors = [];
if ($_POST) {
  if (validate($_POST, $data, $errors, $auth)) {
    $auth->login($data);
    header('Location: login.php');
    exit();
  }
}

Űrlapfeldolgozás

Űrlapfeldolgozás lépései

  1. Űrlap megjelenítése
  2. Küldés → adatok feldolgozása (mentése)
  3. Siker oldal megjelenítése

Hogyan jelenítsük meg a siker oldalt?

Mi történik az oldal frissítésekor?

Példa

if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    $todoRepository->insert([
      'title' => $data['title'],
    ]);
  }
}
$todos = $todoRepository->all();

POST-REDIRECT-GET módszer

Ötlet: POST konvertálása GET-té átirányítással

  1. Az űrlap megjelenítése GET metódussal
  2. Adat elküldése POST metódussal
  3. Sikeres feldolgozás esetén átirányítás a siker oldalra
  4. Siker oldal megjelenítése GET metódussal
if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    $todoRepository->insert([
      'title' => $data['title'],
    ]);
    header('Location: todo.php');
    exit();
  }
}
$todos = $todoRepository->all();

Hibakezelés

“Laza” PRG

  1. Az űrlap megjelenítése GET metódussal
  2. Adat elküldése POST metódussal
  3. Hiba esetén az űrlap megjelenítése hibaüzenetekkel és a felküldött adatokkal
  4. Adat újbóli elküldése POST metódussal
  5. Sikeres feldolgozás esetén átirányítás a siker oldalra
  6. Siker oldal megjelenítése GET metódussal

Példa

function validate($input, &$data, &$errors) {
  // ...
}

$todoRepository = new TodoRepository();
$data = [];
$errors = [];

if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    $todoRepository->insert([
      'title' => $data['title'],
    ]);
    header('Location: todo.php');
    exit();
  }
}

$todos = $todoRepository->all();
?>
<form action="" method="post">
  Movie: <input type="text" name="title">
  <?php if (isset($errors['title'])) : ?>
    <span class="error"><?= $errors['title'] ?></span>
  <?php endif ?>
  <button>Add todo</button>
</form>
<ul>
  <?php foreach($todos as $todo) : ?>
    <li><?= $todo['title'] ?></li>
  <?php endforeach ?>
</ul>

Hibakezelés 2.

“Extrém” PRG: oldalt megjeleníteni csak GET metódussal lehet!

  1. Az űrlap megjelenítése GET metódussal
  2. Adat elküldése POST metódussal
  3. Hiba esetén átirányítás az 1. pontra
    • a hibaüzeneteket és a felküldött adatokat a két kérés között meg kell őrizni
    • munkamenet-kezelés (flash adatok)
  4. Adat újbóli elküldése POST metódussal
  5. Sikeres feldolgozás esetén átirányítás a siker oldalra
  6. Siker oldal megjelenítése GET metódussal

Flash adatok

Olyan adatok, amelyek csak egy kérés idejéig élnek a munkamenetben

(Nálunk: amíg ki nem vesszük őket)

function set_flash_data($key, $value) {
  $_SESSION[$key] = $value;
}

function get_flash_data($key) {
  $value = $_SESSION[$key] ?? null;
  unset($_SESSION[$key]);
  return $value;
}

Példa: Todo

session_start();
$todoRepository = new TodoRepository();
$data = [];
$errors = get_flash_data('errors') ?? [];
$input = get_flash_data('input') ?? [];

if ($_POST) {
  if (validate($_POST, $data, $errors)) {
    $todoRepository->insert([
      'title' => $data['title'],
    ]);
    header('Location: todo.php');
    exit();
  } else {
    set_flash_data('errors', $errors);
    set_flash_data('input', $_POST);
    header('Location: todo.php');
    exit();
  }
}

$todos = $todoRepository->all();
?>
<form action="" method="post">
  Todo:
  <input type="text" name="title" value="<?= $input['title'] ?? '' ?>">
  <?php if (isset($errors['title'])) : ?>
    <span class="error"><?= $errors['title'] ?></span>
  <?php endif; ?>
  <button>Add todo</button>
</form>
<ul>
  <?php foreach($todos as $todo) : ?>
    <li><?= $todo['title'] ?></li>
  <?php endforeach ?>
</ul>

Fájlfeltöltés

Űrlap és feldolgozás

<?php var_dump($errors) ?>
<form action="" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <button>Upload</button>
</form>
$errors = [];
if ($_FILES) {
  if (array_key_exists('file', $_FILES) &&
      $_FILES['file']['error'] == 0) {

    $from = $_FILES['file']['tmp_name'];
    $to = 'files\\' . $_FILES['file']['name'];
    move_uploaded_file($from, $to); 
  } else {
    $errors[] = 'Error during upload!';
  }
}

$_FILES

Array
(
  [file] => Array
    (
      [name] => Desert_small.jpg
      [type] => image/jpeg
      [tmp_name] => C:\eltescorm\tmp\php1750.tmp
      [error] => 0
      [size] => 18218
    )
)

Könyvtárlista

<ul>
<?php foreach($files as $file) : ?>
  <li><?= $file ?></li>
<?php endforeach ?>
</ul>
function getFiles($dir) {
  $fajls = [];
  $d = opendir($dir);
  while (($f = readdir($d)) !== false) {
    $fajls[] = $f;
  }
  closedir($d);
  return $fajls;
}
$files = getFiles('files\\');

Fejlesztői környezet

Fejlesztői környezet részei

  • Szerver oldalon
    • webszerver
    • PHP
  • Kliens oldalon
    • böngésző
  • Fejlesztői környezet
    • Szerkesztő program (HTML, PHP, CSS)
    • SFTP, SCP kliens az állományok webszerverre töltéséhez

Otthoni fejlesztéshez

Megjegyzendő

  • Munkamenet
    • kliensenkénti adattárolás
    • session_start(), $_SESSION
  • Hitelesítás
    • speciális kulcs a munkamenetben
  • Űrlapfeldolgozás
    • PRG irányelv