Управление доступом в PHP: от основ до продвинутой защиты

Раздел: Администрирование PHP -> Безопасность

Управление правами доступа в PHP

Как построить гибкую систему прав на основе ролей и разрешений с кэшированием?

Наиболее эффективное решение для большинства проектов — ролевая модель (RBAC) с хранением связей между пользователями, ролями и разрешениями в базе данных и кэшированием результатов для снижения нагрузки. Такой подход позволяет легко масштабировать систему, добавлять новые роли и права без изменения кода, а также проводить аудит.

Цель: предоставить каждому пользователю ровно те возможности, которые разрешены его ролями, без дублирования логики. Случаи использования: интернет-магазины, панели администрирования, корпоративные порталы.


// Пример сервиса проверки прав (фрагмент)
class AccessService {
    private $cache;
    private $db;

    public function __construct(PDO $db, CacheInterface $cache) {
        $this->db = $db;
        $this->cache = $cache;
    }

    public function hasPermission(int $userId, string $permission): bool {
        $key = "user_permissions_$userId";
        $permissions = $this->cache->get($key);
        if ($permissions === null) {
            $stmt = $this->db->prepare('
                SELECT p.name
                FROM permissions p
                JOIN role_permissions rp ON rp.permission_id = p.id
                JOIN user_roles ur ON ur.role_id = rp.role_id
                WHERE ur.user_id = :userId
            ');
            $stmt->execute([':userId' => $userId]);
            $permissions = array_column($stmt->fetchAll(), 'name');
            $this->cache->set($key, $permissions, 3600);
        }
        return in_array($permission, $permissions, true);
    }
}
  

Пояснение

Сервис кэширует список разрешений пользователя на 1 час, чтобы избежать повторных запросов к БД. При каждом вызове hasPermission проверка происходит в памяти, что ускоряет работу.

Типичные ошибки:

  • Забыть сбросить кэш после изменения прав пользователя. Решение: при каждом обновлении прав удалять соответствующий ключ из кэша.
  • Использовать только одну таблицу ролей без разрешений — негибко.
  • Хранить права в сессии, что приводит к устареванию данных при длительном сеансе.

Как сделать простую проверку роли через глобальную сессию?

Для небольших сайтов, где количество ролей ограничено (например, admin, user), можно хранить роль непосредственно в сессии после аутентификации. Это быстро и не требует дополнительных запросов к БД.

Цель: минимальная реализация без сложной инфраструктуры. Случай использования: личный блог или простая админка.


// После входа пользователя
$_SESSION['role'] = 'admin';

// При проверке доступа
if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin') {
    // разрешить действие
}
  

Проблемы:

  • Невозможность гранулярного управления — каждое действие проверяется отдельно.
  • При смене роли администратором данные в сессии остаются старыми до выхода пользователя.
  • Отсутствие логирования попыток доступа.

Как использовать ACL (Access Control Lists) с явными разрешениями для каждого пользователя?

ACL — модель, где каждому пользователю напрямую сопоставляется список разрешённых объектов и операций. Это даёт максимальную гибкость, но требует много записей в БД.

Цель: точная настройка прав для каждого отдельного пользователя, например, доступ к определённым документам. Случай использования: системы документооборота.


// Таблица acl: user_id, resource, action, allowed
function checkAcl(PDO $db, int $userId, string $resource, string $action): bool {
    $stmt = $db->prepare('
        SELECT allowed FROM acl 
        WHERE user_id = :uid AND resource = :res AND action = :act
    ');
    $stmt->execute([':uid' => $userId, ':res' => $resource, ':act' => $action]);
    $row = $stmt->fetch();
    return $row ? (bool)$row['allowed'] : false; // по умолчанию запрещено
}
  

Ошибка: забыть добавить запись для нового ресурса — доступ будет запрещён всегда. Рекомендуется включить наследование от роли.

Как внедрить middleware для проверки прав в Laravel?

Современные фреймворки предлагают встроенные механизмы. В Laravel можно создать middleware, который перехватывает запрос и вызывает проверку разрешения через Gate или Policy.

Цель: централизованная фильтрация запросов без дублирования кода. Случай использования: любой проект на Laravel.


// Middleware: app/Http/Middleware/CheckPermission.php
public function handle($request, Closure $next, $permission) {
    if (Gate::denies($permission)) {
        abort(403);
    }
    return $next($request);
}

// В маршруте:
Route::middleware('permission:edit-posts')->group(function () {
    Route::resource('posts', 'PostController');
});
  

Проблема: неправильное имя разрешения в маршруте — нужно синхронизировать с определением Gate. Решение: использовать константы или перечисления.

Расширенные примеры реализации

Пример 1. Полная реализация RBAC на чистом PHP с PDO

Ниже приведён код, который демонстрирует загрузку ролей и разрешений из базы данных, кэширование в массиве и проверку нескольких разрешений.

Пример

<?php
class RbacManager {
    private $pdo;
    private $permissionsCache = [];

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function loadPermissions(int $userId): array {
        if (!isset($this->permissionsCache[$userId])) {
            $stmt = $this->pdo->prepare('
                SELECT DISTINCT p.name 
                FROM permissions p
                JOIN role_permissions rp ON rp.permission_id = p.id
                JOIN user_roles ur ON ur.role_id = rp.role_id
                WHERE ur.user_id = :uid
            ');
            $stmt->execute([':uid' => $userId]);
            $this->permissionsCache[$userId] = array_column($stmt->fetchAll(), 'name');
        }
        return $this->permissionsCache[$userId];
    }

    public function hasPermission(int $userId, string $permission): bool {
        return in_array($permission, $this->loadPermissions($userId), true);
    }

    public function hasAnyPermission(int $userId, array $permissions): bool {
        $userPerms = $this->loadPermissions($userId);
        return !empty(array_intersect($permissions, $userPerms));
    }
}

// Использование:
$manager = new RbacManager($pdo);
if ($manager->hasPermission(42, 'delete-post')) {
    echo 'Пользователь может удалять посты';
}
?>
Пользователь может удалять посты

Пояснение:

Метод loadPermissions кэширует разрешения в свойстве класса, поэтому при повторных вызовах в рамках одного запроса база не опрашивается. hasAnyPermission позволяет проверить наличие хотя бы одного из списка прав.

Пример 2. Middleware для Laravel с поддержкой нескольких ролей

Создадим middleware, который проверяет, имеет ли пользователь одну из указанных ролей.

Пример

// app/Http/Middleware/CheckRole.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Auth;

class CheckRole
{
    public function handle($request, Closure $next, ...$roles)
    {
        $user = Auth::user();
        if (!$user || !$user->hasAnyRole($roles)) {
            abort(403, 'Доступ запрещён');
        }
        return $next($request);
    }
}

// В User.php модели
public function hasAnyRole(array $roles): bool {
    return $this->roles()->whereIn('name', $roles)->exists();
}

// Регистрация в kernel.php и использование:
Route::middleware('role:admin,moderator')->get('/admin', function () {
    return view('admin');
});
При попытке доступа пользователем без роли admin или moderator возвращается 403

Пояснение:

Middleware принимает список ролей как аргументы через двоеточие. Метод hasAnyRole делегирует проверку связи many-to-many с таблицей ролей.

Пример 3. Использование битовых масок для хранения прав

Для очень распространённых проектов можно хранить права в виде битовой маски в одном целом числе, например, каждая операция — степень двойки.

Пример

// Определение прав
define('READ', 1);   // 1
define('WRITE', 2);  // 10
define('DELETE', 4); // 100
define('ADMIN', 8);  // 1000

// Права пользователя: read + write
$rights = READ | WRITE;  // 3

// Проверка
if ($rights & READ) {
    echo 'Есть право на чтение';
}
if ($rights & DELETE) {
    echo 'Есть право на удаление';
} else {
    echo 'Нет права на удаление';
}
Есть право на чтение
Нет права на удаление

Пояснение:

Битовые маски очень компактны (одно целое число вместо массива), но неудобны для крупных наборов прав (ограничение размера int). Часто используются для базовых CRUD-операций.

Пример 4. Кэширование разрешений в Redis

Если приложение работает на нескольких серверах, общий кэш (Redis) позволяет синхронизировать данные о правах между ними.

Пример

use Predis\Client as Redis;

class RedisRbac {
    private $redis;
    private $pdo;

    public function __construct(Redis $redis, PDO $pdo) {
        $this->redis = $redis;
        $this->pdo = $pdo;
    }

    public function check(int $userId, string $perm): bool {
        $key = "rbac:$userId";
        $permissions = $this->redis->smembers($key);
        if (empty($permissions)) {
            $stmt = $this->pdo->prepare('SELECT p.name FROM permissions p ...'); // как в примере 1
            $stmt->execute([':uid' => $userId]);
            $permissions = $stmt->fetchAll(\PDO::FETCH_COLUMN);
            if ($permissions) {
                $this->redis->sadd($key, ...$permissions);
                $this->redis->expire($key, 3600);
            }
        }
        return $this->redis->sismember($key, $perm);
    }

    public function invalidate(int $userId): void {
        $this->redis->del("rbac:$userId");
    }
}
(без вывода, но метод check возвращает true/false)

Пояснение:

В Redis используются множества (sets) для хранения разрешений пользователя. Транзакция sadd и проверка sismember очень быстры. Метод invalidate сбрасывает кэш при изменении прав.

Управление правами доступа в PHP - comments

En
права php (php)