Skip to content

Глава 2.1: HTTP и веб-основы

Запросы и ответы, GET/POST, заголовки, cookies, сессии


Почему важно понимать HTTP?

HTTP (HyperText Transfer Protocol) — это язык общения между браузером и сервером. Понимание HTTP критически важно для веб-разработчика:

┌─────────────────────────────────────────────────────────────────┐
│                    ПОЧЕМУ HTTP ВАЖЕН                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   🔍 Отладка         Понять почему запрос не работает          │
│   🔐 Безопасность    Защита от CSRF, XSS, session hijacking    │
│   🚀 Оптимизация     Кеширование, сжатие, keep-alive           │
│   🔌 API             REST API построен на HTTP методах          │
│   🍪 Состояние       Cookies и сессии для авторизации          │
│   📱 Интеграции      Работа с внешними сервисами               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1. Как работает HTTP

Клиент-серверная модель

┌──────────────┐         HTTP Request          ┌──────────────┐
│              │  ─────────────────────────►   │              │
│   Браузер    │                               │    Сервер    │
│   (Клиент)   │                               │    (PHP)     │
│              │   ◄─────────────────────────  │              │
└──────────────┘         HTTP Response         └──────────────┘

Цикл запрос-ответ:
1. Пользователь вводит URL или кликает ссылку
2. Браузер формирует HTTP-запрос и отправляет серверу
3. Сервер обрабатывает запрос (PHP-скрипт)
4. Сервер формирует HTTP-ответ
5. Браузер получает ответ и отображает страницу

Структура HTTP-запроса

http
POST /login.php HTTP/1.1                    ← Стартовая строка
Host: example.com                           ← Заголовки
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Cookie: session_id=abc123
User-Agent: Mozilla/5.0
                                            ← Пустая строка
username=ivan&password=secret               ← Тело запроса (для POST)

Структура HTTP-ответа

http
HTTP/1.1 200 OK                             ← Статусная строка
Content-Type: text/html; charset=UTF-8      ← Заголовки
Content-Length: 1234
Set-Cookie: session_id=xyz789
Cache-Control: no-cache
                                            ← Пустая строка
<!DOCTYPE html>                             ← Тело ответа
<html>...

Коды состояния HTTP

┌───────┬────────────────────────────────────────────────────────┐
│ Код   │ Описание                                               │
├───────┼────────────────────────────────────────────────────────┤
│ 1xx   │ Информационные                                         │
│ 2xx   │ Успех                                                  │
│  200  │   OK — запрос успешен                                  │
│  201  │   Created — ресурс создан                              │
│  204  │   No Content — успех, но тело пустое                   │
│ 3xx   │ Перенаправление                                        │
│  301  │   Moved Permanently — постоянный редирект              │
│  302  │   Found — временный редирект                           │
│  304  │   Not Modified — использовать кеш                      │
│ 4xx   │ Ошибка клиента                                         │
│  400  │   Bad Request — неверный запрос                        │
│  401  │   Unauthorized — требуется авторизация                 │
│  403  │   Forbidden — доступ запрещён                          │
│  404  │   Not Found — ресурс не найден                         │
│  405  │   Method Not Allowed — метод не разрешён               │
│  422  │   Unprocessable Entity — ошибка валидации              │
│  429  │   Too Many Requests — слишком много запросов           │
│ 5xx   │ Ошибка сервера                                         │
│  500  │   Internal Server Error — ошибка сервера               │
│  502  │   Bad Gateway — ошибка шлюза                           │
│  503  │   Service Unavailable — сервис недоступен              │
└───────┴────────────────────────────────────────────────────────┘

2. HTTP методы

Основные методы

┌─────────┬──────────────────────────────────────────────────────┐
│ Метод   │ Назначение                                           │
├─────────┼──────────────────────────────────────────────────────┤
│ GET     │ Получить данные (страницу, файл, API-ответ)         │
│ POST    │ Отправить данные (формы, создание ресурса)          │
│ PUT     │ Заменить ресурс целиком                             │
│ PATCH   │ Частично обновить ресурс                            │
│ DELETE  │ Удалить ресурс                                      │
│ HEAD    │ Получить только заголовки (без тела)                │
│ OPTIONS │ Узнать поддерживаемые методы                        │
└─────────┴──────────────────────────────────────────────────────┘

GET vs POST

┌────────────────┬─────────────────────┬─────────────────────────┐
│ Характеристика │ GET                 │ POST                    │
├────────────────┼─────────────────────┼─────────────────────────┤
│ Данные         │ В URL (?key=value)  │ В теле запроса          │
│ Видимость      │ Видны в адресной    │ Скрыты от пользователя  │
│                │ строке              │                         │
│ Закладки       │ Можно сохранить     │ Нельзя сохранить        │
│ Кеширование    │ Кешируется          │ Не кешируется           │
│ Размер данных  │ Ограничен (~2KB)    │ Практически безлимитно  │
│ Безопасность   │ Менее безопасен     │ Более безопасен         │
│ Идемпотентность│ Да (повтор = то же) │ Нет (повтор ≠ то же)    │
│ Использование  │ Получение данных    │ Отправка/изменение      │
└────────────────┴─────────────────────┴─────────────────────────┘

Когда использовать GET

php
<?php
// ✅ GET — для получения данных

// Просмотр страницы
// GET /products.php?category=phones

// Поиск
// GET /search.php?q=iphone

// Пагинация
// GET /articles.php?page=2

// Фильтрация
// GET /catalog.php?brand=apple&price_min=1000

Когда использовать POST

php
<?php
// ✅ POST — для отправки/изменения данных

// Авторизация
// POST /login.php (username, password)

// Регистрация
// POST /register.php (name, email, password)

// Создание записи
// POST /articles.php (title, content)

// Загрузка файла
// POST /upload.php (file)

// Оформление заказа
// POST /checkout.php (cart, address, payment)

3. Суперглобальные массивы

$_GET — данные из URL

php
<?php
// URL: /search.php?q=php&page=2&sort=date

// Получение параметров
$query = $_GET['q'];      // "php"
$page = $_GET['page'];    // "2" (строка!)
$sort = $_GET['sort'];    // "date"

// Безопасное получение с значением по умолчанию
$query = $_GET['q'] ?? '';
$page = (int) ($_GET['page'] ?? 1);
$sort = $_GET['sort'] ?? 'date';

// Проверка наличия параметра
if (isset($_GET['q'])) {
    // Параметр передан
}

if (!empty($_GET['q'])) {
    // Параметр передан и не пустой
}

// filter_input — более безопасный способ
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
$email = filter_input(INPUT_GET, 'email', FILTER_VALIDATE_EMAIL);

$_POST — данные из формы

php
<?php
// Форма отправлена методом POST
// <form method="POST" action="login.php">
//   <input name="username">
//   <input name="password" type="password">
// </form>

// Получение данных
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

// Проверка метода запроса
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Форма отправлена
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    
    // Валидация
    if (empty($username) || empty($password)) {
        $error = 'Заполните все поля';
    }
}

// filter_input для POST
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 150]
]);

$_REQUEST — GET + POST + COOKIE

php
<?php
// $_REQUEST содержит данные из $_GET, $_POST и $_COOKIE
// Порядок определяется в php.ini (request_order)

$value = $_REQUEST['key'];

// ⚠️ Лучше избегать $_REQUEST!
// Явно указывай источник данных:
$getParam = $_GET['param'] ?? null;
$postParam = $_POST['param'] ?? null;

$_SERVER — информация о сервере и запросе

php
<?php
// Метод запроса
echo $_SERVER['REQUEST_METHOD'];  // GET, POST, PUT...

// URI запроса
echo $_SERVER['REQUEST_URI'];     // /page.php?id=5

// Путь к скрипту
echo $_SERVER['SCRIPT_NAME'];     // /page.php

// Query string
echo $_SERVER['QUERY_STRING'];    // id=5

// Хост
echo $_SERVER['HTTP_HOST'];       // example.com
echo $_SERVER['SERVER_NAME'];     // example.com

// IP клиента
echo $_SERVER['REMOTE_ADDR'];     // 192.168.1.1

// User Agent
echo $_SERVER['HTTP_USER_AGENT']; // Mozilla/5.0...

// Referer (откуда пришёл)
echo $_SERVER['HTTP_REFERER'];    // https://google.com/

// HTTPS
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';

// Полный URL
$protocol = $isHttps ? 'https' : 'http';
$fullUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

// Путь к документам
echo $_SERVER['DOCUMENT_ROOT'];   // /var/www/html

// Порт
echo $_SERVER['SERVER_PORT'];     // 80 или 443

Практический пример: роутер

php
<?php
// Простой роутер на основе $_SERVER

$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// Удалить trailing slash
$uri = rtrim($uri, '/') ?: '/';

// Роутинг
switch (true) {
    case $uri === '/' && $method === 'GET':
        // Главная страница
        include 'pages/home.php';
        break;
        
    case $uri === '/about' && $method === 'GET':
        // О нас
        include 'pages/about.php';
        break;
        
    case preg_match('#^/products/(\d+)$#', $uri, $matches) && $method === 'GET':
        // Просмотр товара: /products/123
        $productId = (int) $matches[1];
        include 'pages/product.php';
        break;
        
    case $uri === '/login' && $method === 'GET':
        // Форма логина
        include 'pages/login-form.php';
        break;
        
    case $uri === '/login' && $method === 'POST':
        // Обработка логина
        include 'pages/login-process.php';
        break;
        
    default:
        // 404
        http_response_code(404);
        include 'pages/404.php';
}

4. HTTP заголовки

Отправка заголовков в PHP

php
<?php
// ВАЖНО: заголовки должны быть отправлены ДО любого вывода!

// Установка заголовка
header('Content-Type: text/html; charset=UTF-8');

// Статус ответа
header('HTTP/1.1 404 Not Found');
// Или:
http_response_code(404);

// Редирект
header('Location: /new-page.php');
exit;  // Важно! Прекратить выполнение после редиректа

// Редирект с кодом 301 (постоянный)
header('Location: /new-url.php', true, 301);
exit;

// Запрет кеширования
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

// Кеширование на 1 час
header('Cache-Control: public, max-age=3600');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');

// Тип контента
header('Content-Type: application/json');
header('Content-Type: text/plain');
header('Content-Type: image/png');

// Скачивание файла
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="document.pdf"');
header('Content-Length: ' . filesize($file));

// CORS (для API)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

Проверка: отправлены ли заголовки?

php
<?php
// Проверить, отправлены ли уже заголовки
if (headers_sent($file, $line)) {
    die("Заголовки уже отправлены в $file на строке $line");
}

// Безопасный редирект
function redirect(string $url): void {
    if (headers_sent()) {
        // Fallback: JavaScript редирект
        echo "<script>window.location.href='$url';</script>";
        echo "<noscript><meta http-equiv='refresh' content='0;url=$url'></noscript>";
    } else {
        header('Location: ' . $url);
    }
    exit;
}

Получение заголовков запроса

php
<?php
// Все заголовки запроса
$headers = getallheaders();
print_r($headers);
/*
[
    'Host' => 'example.com',
    'User-Agent' => 'Mozilla/5.0...',
    'Accept' => 'text/html',
    'Authorization' => 'Bearer token123',
    ...
]
*/

// Конкретный заголовок
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';

// Функция для получения заголовка
function getHeader(string $name): ?string {
    $headers = getallheaders();
    
    // Заголовки регистронезависимы
    foreach ($headers as $key => $value) {
        if (strcasecmp($key, $name) === 0) {
            return $value;
        }
    }
    
    return null;
}

$auth = getHeader('Authorization');

Практический пример: JSON API

php
<?php
// api.php — простой JSON API

header('Content-Type: application/json; charset=UTF-8');
header('Access-Control-Allow-Origin: *');

// Функция ответа
function jsonResponse(mixed $data, int $code = 200): never {
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    exit;
}

// Функция ошибки
function jsonError(string $message, int $code = 400): never {
    jsonResponse(['error' => true, 'message' => $message], $code);
}

// Получение JSON из тела запроса
function getJsonInput(): array {
    $input = file_get_contents('php://input');
    $data = json_decode($input, true);
    
    if (json_last_error() !== JSON_ERROR_NONE) {
        jsonError('Invalid JSON', 400);
    }
    
    return $data ?? [];
}

// Роутинг
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// Обработка preflight запросов (CORS)
if ($method === 'OPTIONS') {
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
    header('Access-Control-Allow-Headers: Content-Type, Authorization');
    exit;
}

// Примеры эндпоинтов
switch (true) {
    case $uri === '/api/users' && $method === 'GET':
        // Получить список пользователей
        $users = [
            ['id' => 1, 'name' => 'Иван'],
            ['id' => 2, 'name' => 'Мария'],
        ];
        jsonResponse(['users' => $users]);
        break;
        
    case $uri === '/api/users' && $method === 'POST':
        // Создать пользователя
        $data = getJsonInput();
        
        if (empty($data['name'])) {
            jsonError('Name is required');
        }
        
        // Создание пользователя...
        jsonResponse(['id' => 3, 'name' => $data['name']], 201);
        break;
        
    default:
        jsonError('Not found', 404);
}

5. Cookies (куки)

Что такое cookies?

Cookies — небольшие фрагменты данных, которые сервер отправляет браузеру, а браузер сохраняет и отправляет обратно с каждым запросом.

┌──────────────────────────────────────────────────────────────────┐
│                     КАК РАБОТАЮТ COOKIES                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Сервер отправляет cookie:                                   │
│     Set-Cookie: user_id=123; Path=/; HttpOnly                   │
│                                                                  │
│  2. Браузер сохраняет cookie                                    │
│                                                                  │
│  3. При следующих запросах браузер отправляет:                  │
│     Cookie: user_id=123                                         │
│                                                                  │
│  4. Сервер читает cookie и "узнаёт" пользователя                │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Установка cookies

php
<?php
// Базовая установка
setcookie('username', 'ivan');

// С временем жизни (1 час)
setcookie('remember_me', 'yes', time() + 3600);

// С полными параметрами (PHP 7.3+)
setcookie('session_token', 'abc123', [
    'expires' => time() + 86400 * 30,  // 30 дней
    'path' => '/',                      // Доступна на всём сайте
    'domain' => '.example.com',         // Включая поддомены
    'secure' => true,                   // Только через HTTPS
    'httponly' => true,                 // Недоступна из JavaScript
    'samesite' => 'Lax',               // Защита от CSRF
]);

// Старый синтаксис (до PHP 7.3)
setcookie(
    'old_style',
    'value',
    time() + 3600,  // expires
    '/',            // path
    '.example.com', // domain
    true,           // secure
    true            // httponly
);

Параметры cookies

┌─────────────┬──────────────────────────────────────────────────────┐
│ Параметр    │ Описание                                             │
├─────────────┼──────────────────────────────────────────────────────┤
│ name        │ Имя куки                                             │
│ value       │ Значение (строка)                                    │
│ expires     │ Unix timestamp когда истечёт (0 = сессионная)        │
│ path        │ Путь на сайте где доступна ('/' = весь сайт)        │
│ domain      │ Домен ('.site.com' включает поддомены)              │
│ secure      │ Передавать только через HTTPS                        │
│ httponly    │ Недоступна из JavaScript (защита от XSS)            │
│ samesite    │ Strict/Lax/None — защита от CSRF                    │
└─────────────┴──────────────────────────────────────────────────────┘

Чтение cookies

php
<?php
// Cookies доступны в $_COOKIE

// Проверка существования
if (isset($_COOKIE['username'])) {
    echo "Привет, " . htmlspecialchars($_COOKIE['username']);
}

// Безопасное получение
$username = $_COOKIE['username'] ?? 'Гость';
$theme = $_COOKIE['theme'] ?? 'light';

// ВАЖНО: $_COOKIE содержит значения на момент НАЧАЛА запроса!
// Если вы установили куку через setcookie(), она появится
// в $_COOKIE только при СЛЕДУЮЩЕМ запросе!

setcookie('test', 'value');
echo $_COOKIE['test'];  // ❌ Не сработает в этом же запросе!

// Обходной путь:
setcookie('test', 'value');
$_COOKIE['test'] = 'value';  // Ручная установка для текущего запроса

Удаление cookies

php
<?php
// Установить время истечения в прошлом
setcookie('username', '', time() - 3600);

// Или с теми же параметрами, что и при установке
setcookie('session_token', '', [
    'expires' => time() - 3600,
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,
    'httponly' => true,
]);

// Функция удаления
function deleteCookie(string $name, string $path = '/', string $domain = ''): void {
    setcookie($name, '', [
        'expires' => time() - 3600,
        'path' => $path,
        'domain' => $domain,
    ]);
    unset($_COOKIE[$name]);
}

Практический пример: "Запомнить меня"

php
<?php
// Генерация токена при логине
function createRememberToken(int $userId): string {
    $token = bin2hex(random_bytes(32));
    
    // Сохранить хеш токена в БД
    $hashedToken = hash('sha256', $token);
    saveTokenToDatabase($userId, $hashedToken, time() + 86400 * 30);
    
    return $token;
}

// Установка куки "Запомнить меня"
function setRememberCookie(int $userId): void {
    $token = createRememberToken($userId);
    
    setcookie('remember_token', $userId . ':' . $token, [
        'expires' => time() + 86400 * 30,  // 30 дней
        'path' => '/',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);
}

// Проверка токена при загрузке страницы
function checkRememberCookie(): ?int {
    if (!isset($_COOKIE['remember_token'])) {
        return null;
    }
    
    $parts = explode(':', $_COOKIE['remember_token'], 2);
    if (count($parts) !== 2) {
        return null;
    }
    
    [$userId, $token] = $parts;
    $hashedToken = hash('sha256', $token);
    
    // Проверить токен в БД
    if (validateTokenInDatabase($userId, $hashedToken)) {
        return (int) $userId;
    }
    
    // Токен невалидный — удалить куку
    deleteCookie('remember_token');
    return null;
}

SameSite — защита от CSRF

php
<?php
// SameSite контролирует отправку cookies при cross-site запросах

// Strict — куки НЕ отправляются при переходе с другого сайта
// (даже по обычной ссылке!)
setcookie('auth', 'token', ['samesite' => 'Strict']);

// Lax — куки отправляются только при "безопасных" переходах
// (ссылки, но НЕ формы и НЕ AJAX с другого сайта)
setcookie('auth', 'token', ['samesite' => 'Lax']);  // Рекомендуется!

// None — куки отправляются всегда (требует secure=true)
// Нужно для виджетов, встраиваемых на другие сайты
setcookie('widget', 'data', ['samesite' => 'None', 'secure' => true]);

6. Сессии (Sessions)

Что такое сессии?

Сессия — механизм хранения данных пользователя между запросами на сервере. В отличие от cookies, данные хранятся на сервере, а не у клиента.

┌──────────────────────────────────────────────────────────────────┐
│                     КАК РАБОТАЮТ СЕССИИ                          │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Пользователь заходит на сайт                                │
│                                                                  │
│  2. PHP создаёт уникальный SESSION ID (например: abc123...)     │
│                                                                  │
│  3. SESSION ID отправляется клиенту через cookie (PHPSESSID)    │
│                                                                  │
│  4. На сервере создаётся файл /tmp/sess_abc123... с данными     │
│                                                                  │
│  5. При следующем запросе браузер отправляет PHPSESSID          │
│                                                                  │
│  6. PHP читает файл сессии и восстанавливает $_SESSION          │
│                                                                  │
│  Клиент:  Cookie: PHPSESSID=abc123...                           │
│  Сервер:  /tmp/sess_abc123... → ['user_id' => 1, 'cart' => [...]]│
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Базовая работа с сессиями

php
<?php
// ВАЖНО: session_start() должен быть вызван ДО любого вывода!

// Начать сессию
session_start();

// Записать данные в сессию
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'ivan';
$_SESSION['cart'] = ['item1', 'item2'];

// Прочитать данные
$userId = $_SESSION['user_id'] ?? null;
$username = $_SESSION['username'] ?? 'Гость';

// Проверить наличие
if (isset($_SESSION['user_id'])) {
    echo "Пользователь авторизован";
}

// Удалить одно значение
unset($_SESSION['cart']);

// Получить ID сессии
$sessionId = session_id();
echo "Session ID: $sessionId";

// Регенерировать ID (после логина — для безопасности!)
session_regenerate_id(true);

Уничтожение сессии

php
<?php
session_start();

// Способ 1: Очистить все данные
$_SESSION = [];

// Способ 2: Полное уничтожение сессии (logout)
function logout(): void {
    // Очистить данные
    $_SESSION = [];
    
    // Удалить cookie сессии
    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();
        setcookie(
            session_name(),
            '',
            time() - 3600,
            $params['path'],
            $params['domain'],
            $params['secure'],
            $params['httponly']
        );
    }
    
    // Уничтожить сессию
    session_destroy();
}

Настройка сессий

php
<?php
// Настройка ДО session_start()

// Имя cookie сессии (по умолчанию PHPSESSID)
session_name('MYSESSID');

// Время жизни cookie (0 = до закрытия браузера)
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'domain' => '.example.com',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

// Папка для файлов сессий
session_save_path('/var/www/sessions');

// Теперь запускаем
session_start();

// Или через php.ini / .htaccess:
// session.name = "MYSESSID"
// session.cookie_lifetime = 0
// session.cookie_secure = 1
// session.cookie_httponly = 1
// session.cookie_samesite = "Lax"
// session.save_path = "/var/www/sessions"

Время жизни сессии

php
<?php
// Время жизни сессии на сервере (в секундах)
// По умолчанию: 1440 секунд (24 минуты)
ini_set('session.gc_maxlifetime', 3600);  // 1 час

// Время жизни cookie сессии
ini_set('session.cookie_lifetime', 0);  // 0 = до закрытия браузера

// Или для конкретной сессии (пользовательское время истечения)
session_start();
$_SESSION['expires'] = time() + 3600;  // 1 час

// Проверка при каждом запросе
session_start();
if (isset($_SESSION['expires']) && $_SESSION['expires'] < time()) {
    // Сессия истекла
    session_destroy();
    header('Location: /login');
    exit;
}
// Обновить время
$_SESSION['expires'] = time() + 3600;

Практический пример: система авторизации

php
<?php
// auth.php — функции авторизации

function startSecureSession(): void {
    if (session_status() === PHP_SESSION_NONE) {
        session_set_cookie_params([
            'lifetime' => 0,
            'path' => '/',
            'secure' => true,
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
        session_start();
    }
}

function login(int $userId, string $username): void {
    // Регенерация ID для защиты от session fixation
    session_regenerate_id(true);
    
    $_SESSION['user_id'] = $userId;
    $_SESSION['username'] = $username;
    $_SESSION['login_time'] = time();
    $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
}

function isLoggedIn(): bool {
    if (!isset($_SESSION['user_id'])) {
        return false;
    }
    
    // Проверка IP (опционально, может мешать мобильным)
    // if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
    //     logout();
    //     return false;
    // }
    
    // Проверка User-Agent
    if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        logout();
        return false;
    }
    
    return true;
}

function getCurrentUser(): ?array {
    if (!isLoggedIn()) {
        return null;
    }
    
    return [
        'id' => $_SESSION['user_id'],
        'username' => $_SESSION['username'],
    ];
}

function requireLogin(): void {
    if (!isLoggedIn()) {
        header('Location: /login.php');
        exit;
    }
}

function logout(): void {
    $_SESSION = [];
    
    if (ini_get('session.use_cookies')) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 3600, $params['path'], 
                  $params['domain'], $params['secure'], $params['httponly']);
    }
    
    session_destroy();
}

// Использование в login.php
startSecureSession();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    
    // Проверка в БД (упрощённо)
    $user = findUserByUsername($username);
    
    if ($user && password_verify($password, $user['password_hash'])) {
        login($user['id'], $user['username']);
        header('Location: /dashboard.php');
        exit;
    } else {
        $error = 'Неверный логин или пароль';
    }
}

// Использование в protected.php
startSecureSession();
requireLogin();

$user = getCurrentUser();
echo "Привет, " . htmlspecialchars($user['username']);

Flash-сообщения

php
<?php
// Flash-сообщения — данные, которые живут только до следующего запроса

function setFlash(string $key, string $message): void {
    $_SESSION['flash'][$key] = $message;
}

function getFlash(string $key): ?string {
    $message = $_SESSION['flash'][$key] ?? null;
    unset($_SESSION['flash'][$key]);
    return $message;
}

function hasFlash(string $key): bool {
    return isset($_SESSION['flash'][$key]);
}

// Использование
// В обработчике формы:
session_start();
setFlash('success', 'Данные успешно сохранены!');
header('Location: /profile.php');
exit;

// На странице profile.php:
session_start();
if ($message = getFlash('success')) {
    echo '<div class="alert alert-success">' . htmlspecialchars($message) . '</div>';
}

7. Обработка форм

Полный цикл обработки формы

php
<?php
// contact.php — форма обратной связи

session_start();

$errors = [];
$success = false;
$old = [
    'name' => '',
    'email' => '',
    'message' => '',
];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // CSRF защита
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        die('CSRF token mismatch');
    }
    
    // Получение и очистка данных
    $old['name'] = trim($_POST['name'] ?? '');
    $old['email'] = trim($_POST['email'] ?? '');
    $old['message'] = trim($_POST['message'] ?? '');
    
    // Валидация
    if (empty($old['name'])) {
        $errors['name'] = 'Введите имя';
    } elseif (mb_strlen($old['name']) < 2) {
        $errors['name'] = 'Имя слишком короткое';
    }
    
    if (empty($old['email'])) {
        $errors['email'] = 'Введите email';
    } elseif (!filter_var($old['email'], FILTER_VALIDATE_EMAIL)) {
        $errors['email'] = 'Некорректный email';
    }
    
    if (empty($old['message'])) {
        $errors['message'] = 'Введите сообщение';
    } elseif (mb_strlen($old['message']) < 10) {
        $errors['message'] = 'Сообщение слишком короткое';
    }
    
    // Если ошибок нет — обработка
    if (empty($errors)) {
        // Отправка email, сохранение в БД и т.д.
        // ...
        
        $success = true;
        $old = ['name' => '', 'email' => '', 'message' => ''];
    }
}

// Генерация CSRF токена
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>Обратная связь</title>
</head>
<body>
    <h1>Обратная связь</h1>
    
    <?php if ($success): ?>
        <div class="success">Сообщение отправлено!</div>
    <?php endif; ?>
    
    <form method="POST" action="">
        <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
        
        <div>
            <label>Имя:</label>
            <input type="text" name="name" value="<?= htmlspecialchars($old['name']) ?>">
            <?php if (isset($errors['name'])): ?>
                <span class="error"><?= htmlspecialchars($errors['name']) ?></span>
            <?php endif; ?>
        </div>
        
        <div>
            <label>Email:</label>
            <input type="email" name="email" value="<?= htmlspecialchars($old['email']) ?>">
            <?php if (isset($errors['email'])): ?>
                <span class="error"><?= htmlspecialchars($errors['email']) ?></span>
            <?php endif; ?>
        </div>
        
        <div>
            <label>Сообщение:</label>
            <textarea name="message"><?= htmlspecialchars($old['message']) ?></textarea>
            <?php if (isset($errors['message'])): ?>
                <span class="error"><?= htmlspecialchars($errors['message']) ?></span>
            <?php endif; ?>
        </div>
        
        <button type="submit">Отправить</button>
    </form>
</body>
</html>

PRG Pattern (Post-Redirect-Get)

php
<?php
// Проблема: при обновлении страницы после POST форма отправляется повторно
// Решение: PRG Pattern

// form-handler.php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Валидация и обработка...
    
    if (empty($errors)) {
        // Успех: сохраняем сообщение и редиректим
        setFlash('success', 'Данные сохранены!');
        header('Location: /success.php');
        exit;  // Важно!
    } else {
        // Ошибки: сохраняем в сессию и редиректим обратно
        $_SESSION['form_errors'] = $errors;
        $_SESSION['form_old'] = $_POST;
        header('Location: /form.php');
        exit;
    }
}

// form.php
session_start();
$errors = $_SESSION['form_errors'] ?? [];
$old = $_SESSION['form_old'] ?? [];
unset($_SESSION['form_errors'], $_SESSION['form_old']);

// Показываем форму с $errors и $old...

8. Безопасность

CSRF (Cross-Site Request Forgery)

php
<?php
// CSRF — атака, когда злоумышленник заставляет пользователя
// выполнить действие на сайте, где тот авторизован

// Пример атаки: на сайте evil.com размещён код:
// <img src="https://bank.com/transfer?to=hacker&amount=1000">
// Когда жертва открывает evil.com, её браузер отправляет запрос
// на bank.com с её cookies!

// Защита: CSRF токен
class CsrfProtection {
    public static function generateToken(): string {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public static function getTokenField(): string {
        $token = self::generateToken();
        return '<input type="hidden" name="csrf_token" value="' . $token . '">';
    }
    
    public static function verify(): bool {
        $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
        return hash_equals($_SESSION['csrf_token'] ?? '', $token);
    }
    
    public static function verifyOrDie(): void {
        if (!self::verify()) {
            http_response_code(403);
            die('CSRF token validation failed');
        }
    }
}

// Использование в форме
session_start();
?>
<form method="POST">
    <?= CsrfProtection::getTokenField() ?>
    <input name="data">
    <button>Submit</button>
</form>

<?php
// В обработчике
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    CsrfProtection::verifyOrDie();
    // Обработка формы...
}

Session Fixation

php
<?php
// Session Fixation — атака, когда злоумышленник навязывает жертве
// известный ему session ID

// Атака:
// 1. Хакер получает session ID: http://site.com/?PHPSESSID=known123
// 2. Отправляет ссылку жертве
// 3. Жертва логинится с этим session ID
// 4. Хакер использует known123 чтобы войти как жертва

// Защита: регенерация session ID при логине
function secureLogin(int $userId): void {
    // Уничтожить старую сессию
    session_regenerate_id(true);  // true = удалить старый файл
    
    // Записать данные в новую сессию
    $_SESSION['user_id'] = $userId;
    $_SESSION['created_at'] = time();
}

// Также регенерировать периодически
session_start();

if (isset($_SESSION['created_at']) && $_SESSION['created_at'] < time() - 1800) {
    // Каждые 30 минут
    session_regenerate_id(true);
    $_SESSION['created_at'] = time();
}

Session Hijacking

php
<?php
// Session Hijacking — кража session ID (через XSS, сниффинг и т.д.)

// Защита:

// 1. HttpOnly cookies (недоступны из JavaScript)
session_set_cookie_params(['httponly' => true]);

// 2. Secure cookies (только через HTTPS)
session_set_cookie_params(['secure' => true]);

// 3. Привязка к User-Agent
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];

// При каждом запросе:
if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
    session_destroy();
    die('Session expired');
}

// 4. Привязка к IP (осторожно — может меняться)
// $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];

// 5. Короткое время жизни сессии
ini_set('session.gc_maxlifetime', 1800);  // 30 минут

// 6. Регулярная регенерация ID
session_regenerate_id(true);

Защита от XSS в выводе

php
<?php
// XSS — внедрение вредоносного JavaScript

// ❌ ОПАСНО!
echo $_GET['search'];
echo $_SESSION['username'];

// ✅ БЕЗОПАСНО — всегда экранируй вывод!
echo htmlspecialchars($_GET['search'], ENT_QUOTES, 'UTF-8');

// Функция-помощник
function e(string $str): string {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

echo e($_GET['search']);
echo e($_SESSION['username']);

// В шаблонах (короткий синтаксис)
?>
<p>Поиск: <?= e($_GET['search'] ?? '') ?></p>
<p>Привет, <?= e($_SESSION['username'] ?? 'Гость') ?>!</p>

Content Security Policy

php
<?php
// CSP — заголовок, ограничивающий источники скриптов

// Базовый CSP (только свои скрипты)
header("Content-Security-Policy: default-src 'self'");

// Разрешить inline скрипты (менее безопасно)
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'");

// Разрешить скрипты с CDN
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net");

// Только отчёты (не блокирует)
header("Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report");

9. Практические примеры

Пример 1: Корзина покупок

php
<?php
// cart.php — корзина на сессиях

session_start();

// Инициализация корзины
if (!isset($_SESSION['cart'])) {
    $_SESSION['cart'] = [];
}

function addToCart(int $productId, int $quantity = 1): void {
    if (isset($_SESSION['cart'][$productId])) {
        $_SESSION['cart'][$productId] += $quantity;
    } else {
        $_SESSION['cart'][$productId] = $quantity;
    }
}

function removeFromCart(int $productId): void {
    unset($_SESSION['cart'][$productId]);
}

function updateCartQuantity(int $productId, int $quantity): void {
    if ($quantity <= 0) {
        removeFromCart($productId);
    } else {
        $_SESSION['cart'][$productId] = $quantity;
    }
}

function getCart(): array {
    return $_SESSION['cart'];
}

function getCartCount(): int {
    return array_sum($_SESSION['cart']);
}

function clearCart(): void {
    $_SESSION['cart'] = [];
}

function getCartTotal(array $products): float {
    $total = 0;
    foreach ($_SESSION['cart'] as $productId => $quantity) {
        if (isset($products[$productId])) {
            $total += $products[$productId]['price'] * $quantity;
        }
    }
    return $total;
}

// Обработка действий
$action = $_POST['action'] ?? $_GET['action'] ?? '';

switch ($action) {
    case 'add':
        $productId = (int) ($_POST['product_id'] ?? 0);
        if ($productId > 0) {
            addToCart($productId);
            setFlash('success', 'Товар добавлен в корзину');
        }
        header('Location: ' . $_SERVER['HTTP_REFERER']);
        exit;
        
    case 'remove':
        $productId = (int) ($_GET['product_id'] ?? 0);
        removeFromCart($productId);
        header('Location: /cart.php');
        exit;
        
    case 'update':
        foreach ($_POST['quantities'] ?? [] as $productId => $quantity) {
            updateCartQuantity((int) $productId, (int) $quantity);
        }
        header('Location: /cart.php');
        exit;
        
    case 'clear':
        clearCart();
        header('Location: /cart.php');
        exit;
}

Пример 2: Rate Limiting

php
<?php
// rate-limiter.php — ограничение количества запросов

class RateLimiter {
    private int $maxAttempts;
    private int $decayMinutes;
    private string $prefix;
    
    public function __construct(int $maxAttempts = 5, int $decayMinutes = 1) {
        $this->maxAttempts = $maxAttempts;
        $this->decayMinutes = $decayMinutes;
        $this->prefix = 'rate_limit_';
    }
    
    public function attempt(string $key): bool {
        $sessionKey = $this->prefix . md5($key);
        
        if (!isset($_SESSION[$sessionKey])) {
            $_SESSION[$sessionKey] = [
                'attempts' => 0,
                'reset_at' => time() + ($this->decayMinutes * 60),
            ];
        }
        
        // Сброс если время истекло
        if ($_SESSION[$sessionKey]['reset_at'] <= time()) {
            $_SESSION[$sessionKey] = [
                'attempts' => 0,
                'reset_at' => time() + ($this->decayMinutes * 60),
            ];
        }
        
        // Проверка лимита
        if ($_SESSION[$sessionKey]['attempts'] >= $this->maxAttempts) {
            return false;
        }
        
        // Увеличить счётчик
        $_SESSION[$sessionKey]['attempts']++;
        return true;
    }
    
    public function remaining(string $key): int {
        $sessionKey = $this->prefix . md5($key);
        $attempts = $_SESSION[$sessionKey]['attempts'] ?? 0;
        return max(0, $this->maxAttempts - $attempts);
    }
    
    public function resetAt(string $key): int {
        $sessionKey = $this->prefix . md5($key);
        return $_SESSION[$sessionKey]['reset_at'] ?? time();
    }
    
    public function clear(string $key): void {
        $sessionKey = $this->prefix . md5($key);
        unset($_SESSION[$sessionKey]);
    }
}

// Использование для защиты логина
session_start();

$limiter = new RateLimiter(5, 15);  // 5 попыток за 15 минут
$key = 'login_' . $_SERVER['REMOTE_ADDR'];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!$limiter->attempt($key)) {
        $waitMinutes = ceil(($limiter->resetAt($key) - time()) / 60);
        die("Слишком много попыток. Подождите $waitMinutes минут.");
    }
    
    // Попытка логина...
    if ($loginSuccess) {
        $limiter->clear($key);  // Сбросить при успехе
        // ...
    }
}

echo "Осталось попыток: " . $limiter->remaining($key);

Пример 3: Настройки пользователя

php
<?php
// settings.php — хранение настроек в cookies

class UserSettings {
    private array $defaults = [
        'theme' => 'light',
        'language' => 'ru',
        'items_per_page' => 20,
        'sidebar_collapsed' => false,
    ];
    
    private array $settings;
    
    public function __construct() {
        $this->settings = $this->defaults;
        $this->loadFromCookie();
    }
    
    private function loadFromCookie(): void {
        if (isset($_COOKIE['user_settings'])) {
            $decoded = json_decode($_COOKIE['user_settings'], true);
            if (is_array($decoded)) {
                $this->settings = array_merge($this->defaults, $decoded);
            }
        }
    }
    
    public function get(string $key, mixed $default = null): mixed {
        return $this->settings[$key] ?? $default;
    }
    
    public function set(string $key, mixed $value): self {
        if (array_key_exists($key, $this->defaults)) {
            $this->settings[$key] = $value;
            $this->saveToCookie();
        }
        return $this;
    }
    
    public function all(): array {
        return $this->settings;
    }
    
    private function saveToCookie(): void {
        $json = json_encode($this->settings);
        setcookie('user_settings', $json, [
            'expires' => time() + 86400 * 365,  // 1 год
            'path' => '/',
            'secure' => true,
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
    }
    
    public function reset(): void {
        $this->settings = $this->defaults;
        setcookie('user_settings', '', time() - 3600, '/');
    }
}

// Использование
$settings = new UserSettings();

// Получение
$theme = $settings->get('theme');
$lang = $settings->get('language');

// Изменение
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $settings->set('theme', $_POST['theme'] ?? 'light');
    $settings->set('items_per_page', (int) ($_POST['items_per_page'] ?? 20));
    header('Location: /settings.php');
    exit;
}
?>
<form method="POST">
    <select name="theme">
        <option value="light" <?= $settings->get('theme') === 'light' ? 'selected' : '' ?>>Светлая</option>
        <option value="dark" <?= $settings->get('theme') === 'dark' ? 'selected' : '' ?>>Тёмная</option>
    </select>
    <button>Сохранить</button>
</form>

10. Упражнения

Упражнение 1: Счётчик посещений (15 минут)

php
<?php
// Создай счётчик посещений страницы:
// 1. Общий счётчик (в файле) — сколько всего просмотров
// 2. Счётчик сессии — сколько раз текущий пользователь обновил страницу
// 3. Уникальные посетители (через cookies) — сколько разных пользователей

// Вывод:
// Всего просмотров: 1234
// Ваших просмотров за сессию: 5
// Уникальных посетителей: 456

Упражнение 2: Форма с валидацией (25 минут)

php
<?php
// Создай форму регистрации:
// - Имя (2-50 символов, только буквы и пробелы)
// - Email (валидный формат, уникальный)
// - Пароль (минимум 8 символов, буквы и цифры)
// - Подтверждение пароля

// Требования:
// - CSRF защита
// - Сохранение введённых данных при ошибке
// - Вывод ошибок под каждым полем
// - PRG pattern после успешной регистрации

Упражнение 3: Система "Избранное" (20 минут)

php
<?php
// Создай систему избранных товаров:
// 1. Для неавторизованных — хранение в cookies (максимум 10 товаров)
// 2. Для авторизованных — хранение в сессии
// 3. Функции: добавить, удалить, проверить наличие, получить список
// 4. При логине — перенос избранного из cookies в сессию

Упражнение 4: API с авторизацией (30 минут)

php
<?php
// Создай простой JSON API:
// - POST /api/login — получить токен
// - GET /api/profile — данные пользователя (требует токен)
// - PUT /api/profile — обновить данные (требует токен)
// - POST /api/logout — инвалидировать токен

// Требования:
// - Токен передаётся через заголовок Authorization: Bearer <token>
// - Токен хранится в сессии
// - Rate limiting: 100 запросов в минуту

11. Вопросы для самопроверки

  1. Чем отличается GET от POST?

  2. Почему нельзя хранить пароли в cookies?

  3. Что такое CSRF и как от него защититься?

  4. Зачем вызывать session_regenerate_id() после логина?

  5. Что произойдёт, если вызвать header() после вывода HTML?

  6. Как проверить, что запрос пришёл через AJAX?

  7. Что такое SameSite cookie и зачем он нужен?

  8. Почему важно использовать htmlspecialchars() при выводе данных?


12. Частые ошибки

Ошибка 1: Headers already sent

php
<?php
// ❌ Ошибка: вывод до header()
echo "Hello";
header('Location: /other.php');  // Warning: Cannot modify header information

// ✅ Решение: header() до любого вывода
header('Location: /other.php');
exit;
echo "Hello";  // Не выполнится

// ✅ Или используй output buffering
ob_start();
echo "Hello";
// ... много кода ...
header('Location: /other.php');
ob_end_clean();
exit;

Ошибка 2: Нет exit после redirect

php
<?php
// ❌ Код продолжает выполняться!
if (!$isLoggedIn) {
    header('Location: /login.php');
}
// Этот код выполнится для неавторизованных!
showSecretData();

// ✅ Всегда exit после redirect
if (!$isLoggedIn) {
    header('Location: /login.php');
    exit;
}
showSecretData();

Ошибка 3: Доверие данным из cookies

php
<?php
// ❌ ОПАСНО! Пользователь может изменить cookies
$isAdmin = $_COOKIE['is_admin'] ?? false;
if ($isAdmin) {
    showAdminPanel();
}

// ✅ Храни важные данные в сессии
if ($_SESSION['role'] === 'admin') {
    showAdminPanel();
}

Ошибка 4: Нет CSRF защиты

php
<?php
// ❌ Форма без CSRF токена
// Злоумышленник может заставить пользователя отправить форму
<form method="POST" action="/delete-account">
    <button>Удалить аккаунт</button>
</form>

// ✅ С CSRF токеном
<form method="POST" action="/delete-account">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <button>Удалить аккаунт</button>
</form>

<?php
// И проверка на сервере
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
    die('Invalid CSRF token');
}

Ошибка 5: XSS через вывод данных

php
<?php
// ❌ XSS уязвимость!
echo "Привет, " . $_GET['name'];
// URL: ?name=<script>alert('XSS')</script>

// ✅ Экранирование
echo "Привет, " . htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8');

Резюме главы

┌────────────────────────────────────────────────────────────────┐
│                      ЗАПОМНИ ГЛАВНОЕ                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  HTTP МЕТОДЫ                                                   │
│  • GET — получение данных (кешируется, в URL)                 │
│  • POST — отправка данных (не кешируется, в теле)             │
│                                                                │
│  СУПЕРГЛОБАЛЬНЫЕ МАССИВЫ                                       │
│  • $_GET — параметры из URL                                   │
│  • $_POST — данные из формы                                   │
│  • $_COOKIE — cookies                                         │
│  • $_SESSION — данные сессии                                  │
│  • $_SERVER — информация о запросе                            │
│                                                                │
│  COOKIES                                                       │
│  • setcookie() до вывода!                                     │
│  • secure=true, httponly=true, samesite='Lax'                 │
│  • Данные хранятся у клиента (не доверяй!)                    │
│                                                                │
│  СЕССИИ                                                        │
│  • session_start() до вывода!                                 │
│  • session_regenerate_id() после логина                       │
│  • Данные хранятся на сервере (безопасно)                     │
│                                                                │
│  БЕЗОПАСНОСТЬ                                                  │
│  • CSRF токены для всех форм                                  │
│  • htmlspecialchars() для всего вывода                        │
│  • exit после header('Location: ...')                         │
│  • Не доверяй данным из $_GET, $_POST, $_COOKIE               │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Следующая глава: Глава 2.2: Формы и валидация — обработка форм, фильтрация, санитизация данных

Выпущено под лицензией MIT