Глава 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-запроса
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/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
// ✅ 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
// ✅ 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
// 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
// Форма отправлена методом 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
// $_REQUEST содержит данные из $_GET, $_POST и $_COOKIE
// Порядок определяется в php.ini (request_order)
$value = $_REQUEST['key'];
// ⚠️ Лучше избегать $_REQUEST!
// Явно указывай источник данных:
$getParam = $_GET['param'] ?? null;
$postParam = $_POST['param'] ?? null;$_SERVER — информация о сервере и запросе
<?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
// Простой роутер на основе $_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
// ВАЖНО: заголовки должны быть отправлены ДО любого вывода!
// Установка заголовка
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
// Проверить, отправлены ли уже заголовки
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
// Все заголовки запроса
$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
// 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
// Базовая установка
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
// 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
// Установить время истечения в прошлом
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
// Генерация токена при логине
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
// 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
// ВАЖНО: 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
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
// Настройка ДО 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
// Время жизни сессии на сервере (в секундах)
// По умолчанию: 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
// 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
// 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
// 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
// Проблема: при обновлении страницы после 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// Создай счётчик посещений страницы:
// 1. Общий счётчик (в файле) — сколько всего просмотров
// 2. Счётчик сессии — сколько раз текущий пользователь обновил страницу
// 3. Уникальные посетители (через cookies) — сколько разных пользователей
// Вывод:
// Всего просмотров: 1234
// Ваших просмотров за сессию: 5
// Уникальных посетителей: 456Упражнение 2: Форма с валидацией (25 минут)
<?php
// Создай форму регистрации:
// - Имя (2-50 символов, только буквы и пробелы)
// - Email (валидный формат, уникальный)
// - Пароль (минимум 8 символов, буквы и цифры)
// - Подтверждение пароля
// Требования:
// - CSRF защита
// - Сохранение введённых данных при ошибке
// - Вывод ошибок под каждым полем
// - PRG pattern после успешной регистрацииУпражнение 3: Система "Избранное" (20 минут)
<?php
// Создай систему избранных товаров:
// 1. Для неавторизованных — хранение в cookies (максимум 10 товаров)
// 2. Для авторизованных — хранение в сессии
// 3. Функции: добавить, удалить, проверить наличие, получить список
// 4. При логине — перенос избранного из cookies в сессиюУпражнение 4: API с авторизацией (30 минут)
<?php
// Создай простой JSON API:
// - POST /api/login — получить токен
// - GET /api/profile — данные пользователя (требует токен)
// - PUT /api/profile — обновить данные (требует токен)
// - POST /api/logout — инвалидировать токен
// Требования:
// - Токен передаётся через заголовок Authorization: Bearer <token>
// - Токен хранится в сессии
// - Rate limiting: 100 запросов в минуту11. Вопросы для самопроверки
Чем отличается GET от POST?
Почему нельзя хранить пароли в cookies?
Что такое CSRF и как от него защититься?
Зачем вызывать
session_regenerate_id()после логина?Что произойдёт, если вызвать
header()после вывода HTML?Как проверить, что запрос пришёл через AJAX?
Что такое SameSite cookie и зачем он нужен?
Почему важно использовать
htmlspecialchars()при выводе данных?
12. Частые ошибки
Ошибка 1: Headers already sent
<?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
// ❌ Код продолжает выполняться!
if (!$isLoggedIn) {
header('Location: /login.php');
}
// Этот код выполнится для неавторизованных!
showSecretData();
// ✅ Всегда exit после redirect
if (!$isLoggedIn) {
header('Location: /login.php');
exit;
}
showSecretData();Ошибка 3: Доверие данным из cookies
<?php
// ❌ ОПАСНО! Пользователь может изменить cookies
$isAdmin = $_COOKIE['is_admin'] ?? false;
if ($isAdmin) {
showAdminPanel();
}
// ✅ Храни важные данные в сессии
if ($_SESSION['role'] === 'admin') {
showAdminPanel();
}Ошибка 4: Нет CSRF защиты
<?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
// ❌ 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: Формы и валидация — обработка форм, фильтрация, санитизация данных