Роли
Роли -- строки вида ROLE_*, определяющие права пользователя. Каждый пользователь имеет минимум ROLE_USER.
<?php
declare(strict_types=1);
namespace App\Entity;
use Symfony\Component\Security\Core\User\UserInterface;
final class User implements UserInterface
{
/** @var list<string> */
private array $roles = [];
/** @return list<string> */
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER'; // Always has ROLE_USER
return array_unique($roles);
}
public function setRoles(array $roles): void
{
$this->roles = $roles;
}
}
Стандартные роли
| Роль | Описание |
|---|---|
ROLE_USER |
Базовая роль каждого аутентифицированного пользователя |
ROLE_ADMIN |
Администратор |
ROLE_SUPER_ADMIN |
Суперадминистратор (обычно наследует все роли) |
IS_AUTHENTICATED_FULLY |
Пользователь аутентифицирован (не через remember me) |
IS_AUTHENTICATED_REMEMBERED |
Аутентифицирован, в том числе через remember me |
IS_AUTHENTICATED |
Любой способ аутентификации |
PUBLIC_ACCESS |
Доступно всем, включая анонимных |
Иерархия ролей
Иерархия позволяет наследовать роли. Пользователь с ROLE_ADMIN автоматически получает ROLE_USER.
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_EDITOR: ROLE_USER
ROLE_MODERATOR: [ROLE_EDITOR, ROLE_COMMENT_MANAGER]
ROLE_ADMIN: ROLE_MODERATOR
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_SUPER_ADMIN
└── ROLE_ADMIN
└── ROLE_MODERATOR
├── ROLE_EDITOR
│ └── ROLE_USER
└── ROLE_COMMENT_MANAGER
└── ROLE_USER
└── ROLE_ALLOWED_TO_SWITCH
Подвох экзамена: Иерархия ролей транзитивна. Если
ROLE_ADMINнаследуетROLE_MODERATOR, аROLE_MODERATORнаследуетROLE_USER, тоROLE_ADMINавтоматически имеетROLE_USER. Не нужно указывать явно.
Access Control Rules
Правила доступа определяют, кто может обращаться к определённым URL.
# config/packages/security.yaml
security:
access_control:
# Order matters! First match wins
- { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profile, roles: ROLE_USER }
- { path: ^/api, roles: IS_AUTHENTICATED }
- { path: ^/login$, roles: PUBLIC_ACCESS }
# With IP restriction
- { path: ^/internal, roles: ROLE_ADMIN, ips: [127.0.0.1, ::1] }
# With host restriction
- { path: ^/, roles: ROLE_ADMIN, host: admin\.example\.com }
# With HTTP method
- { path: ^/api/posts, roles: ROLE_EDITOR, methods: [POST, PUT, DELETE] }
- { path: ^/api/posts, roles: PUBLIC_ACCESS, methods: [GET] }
Подвох экзамена: Порядок правил в
access_controlкритически важен. Проверяется первое совпавшее правило, остальные игнорируются. Более специфичные пути должны идти ПЕРВЫМИ.
Атрибут #[IsGranted]
Атрибут для проверки доступа на уровне контроллера или метода.
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
// Applied to entire controller
#[IsGranted('ROLE_ADMIN')]
#[Route('/admin')]
final class AdminController extends AbstractController
{
#[Route('/dashboard')]
public function dashboard(): Response
{
// Only ROLE_ADMIN can access
return $this->render('admin/dashboard.html.twig');
}
// Override at method level -- more restrictive
#[IsGranted('ROLE_SUPER_ADMIN')]
#[Route('/users')]
public function users(): Response
{
return $this->render('admin/users.html.twig');
}
}
#[IsGranted] с объектом (subject)
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Post;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class PostController extends AbstractController
{
// Check via Voter: can the user edit THIS specific post?
#[IsGranted('EDIT', subject: 'post')]
#[Route('/post/{id}/edit')]
public function edit(Post $post): Response
{
// Voter checks if current user can EDIT this post
return $this->render('post/edit.html.twig', ['post' => $post]);
}
// Multiple subjects
#[IsGranted('TRANSFER', subject: ['source', 'target'])]
#[Route('/transfer/{sourceId}/{targetId}')]
public function transfer(Account $source, Account $target): Response
{
// Voter receives both accounts
return new Response('Transfer page');
}
}
#[IsGranted] с expression
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ArticleController extends AbstractController
{
#[IsGranted(new Expression(
'is_granted("ROLE_ADMIN") or (is_granted("ROLE_EDITOR") and subject.getAuthor() == user)'
), subject: 'article')]
#[Route('/article/{id}/publish')]
public function publish(Article $article): Response
{
// Admin can publish any article
// Editor can publish only own articles
return new Response('Published');
}
}
Проверка доступа в коде
В контроллере
<?php
declare(strict_types=1);
namespace App\Controller;
final class DocumentController extends AbstractController
{
public function view(Document $document): Response
{
// Method 1: denyAccessUnlessGranted (throws exception)
$this->denyAccessUnlessGranted('VIEW', $document);
// Method 2: isGranted (returns bool)
if ($this->isGranted('EDIT', $document)) {
// Show edit button
}
// Method 3: Get current user
$user = $this->getUser();
if (null === $user) {
throw $this->createAccessDeniedException();
}
return $this->render('document/view.html.twig', [
'document' => $document,
'canEdit' => $this->isGranted('EDIT', $document),
]);
}
}
В сервисе
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
final class PostService
{
public function __construct(
private readonly Security $security,
) {
}
public function delete(Post $post): void
{
// Check permission
if (!$this->security->isGranted('DELETE', $post)) {
throw new AccessDeniedException('Cannot delete this post.');
}
// Get current user
$user = $this->security->getUser();
// Proceed with deletion
}
}
В Twig
{# Check role #}
{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('admin_dashboard') }}">Admin Panel</a>
{% endif %}
{# Check with subject (voter) #}
{% if is_granted('EDIT', post) %}
<a href="{{ path('post_edit', {id: post.id}) }}">Edit</a>
{% endif %}
{# Check if authenticated #}
{% if is_granted('IS_AUTHENTICATED') %}
<p>Welcome, {{ app.user.userIdentifier }}</p>
{% endif %}
Security Expressions
Security Expressions позволяют описывать сложные правила доступа.
security:
access_control:
- { path: ^/api, roles: "is_granted('ROLE_API') and is_authenticated_fully()" }
Доступные функции и переменные
| Выражение | Описание |
|---|---|
is_granted('ROLE') |
Проверка роли |
is_granted('ATTR', object) |
Проверка через voter |
is_authenticated_fully() |
Полная аутентификация |
is_authenticated_remembered() |
Через remember me |
is_authenticated() |
Любая аутентификация |
user |
Текущий UserInterface |
subject |
Объект, переданный для проверки |
token |
Токен аутентификации |
request |
Текущий Request (в access_control) |
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class TeamController extends AbstractController
{
#[IsGranted(new Expression(
'user === subject.getOwner() or is_granted("ROLE_ADMIN")'
), subject: 'team')]
public function settings(Team $team): Response
{
return $this->render('team/settings.html.twig');
}
}
Impersonation (Switch User)
# config/packages/security.yaml
security:
firewalls:
main:
switch_user: true # Enable ?_switch_user=username
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
final class AdminController extends AbstractController
{
public function impersonate(): Response
{
// Only users with ROLE_ALLOWED_TO_SWITCH can use this
// Check if currently impersonating
if ($this->isGranted('IS_IMPERSONATOR')) {
// Currently switched to another user
}
return $this->render('admin/impersonate.html.twig');
}
}
{# Switch to user #}
<a href="{{ path('homepage', {_switch_user: '[email protected]'}) }}">
Login as user
</a>
{# Exit impersonation #}
{% if is_granted('IS_IMPERSONATOR') %}
<a href="{{ path('homepage', {_switch_user: '_exit'}) }}">
Exit impersonation
</a>
{% endif %}
Итоги
- Роли -- строки
ROLE_*, минимальная рольROLE_USER - Иерархия ролей транзитивна:
ROLE_ADMIN > ROLE_MOD > ROLE_USER access_control-- порядок правил важен, первое совпадение побеждает#[IsGranted]-- атрибут для проверки доступа на контроллерахSecurity::isGranted()-- проверка в сервисах- Security expressions позволяют сложные логические правила
is_granted('ATTR', $subject)вызывает систему Voters