Два уровня:
roles.Бандл занимается авторизацией. Аутентификация остаётся на firewall’е; бандл никогда не смотрит креды напрямую.
По умолчанию: если опустить roles — диспатчер пропускает авторизацию совсем:
#[Rpc\Method('public.ping')]
final class Ping
{
public function __invoke(): array { return ['pong' => true]; }
}
Анонимные запросы проходят, при условии что ваш firewall тоже их пускает
на /rpc.
Secure-by-default режим. Задайте
security.default_roles(см. Configuration reference) — тогда любой метод без явногоroles:наследует эти роли, а анонимными остаются только перечисленные вpublic_prefixes/public_methods.prefix_roles(напр.admin.* → ROLE_ADMIN) задаёт дефолтные роли точечно по префиксу имён, безroles:на каждом хендлере.
#[Rpc\Method('user.delete', roles: ['ROLE_ADMIN'])]
final class DeleteUser { /* … */ }
При вызове диспатчер дёргает AuthorizationCheckerInterface::isGranted() по
каждой роли. Нет роли → бросает AccessDeniedException (-32001).
Если symfony/security-bundle не установлен, а метод объявляет roles —
бандл падает на первом же вызове с понятным сообщением “install
symfony/security-bundle”. Никакого silent bypass.
// Any (default) — хотя бы одна роль.
#[Rpc\Method('billing.refund', roles: ['ROLE_SUPPORT', 'ROLE_ADMIN'])]
// All — все роли.
#[Rpc\Method(
'compliance.export',
roles: ['ROLE_ADMIN', 'ROLE_COMPLIANCE'],
rolesMatch: RoleMatch::All,
)]
Поменять дефолт для методов, которые не указали rolesMatch:
json_rpc_server:
security:
roles_match: all # или 'any'
По дефолту AccessDenied называет недостающие роли:
One of the following roles is required: ROLE_BILLING_INTERNAL_ADMIN
Удобно в dev. В prod некоторые команды считают role identifier’ы внутренними — переверните флаг:
json_rpc_server:
security:
expose_role_names: false
Теперь сообщение просто Access denied. HTTP body всё ещё несёт
error.code: -32001, просто без утечки.
Бандл ничего не ставит со стороны firewall’а. Типичный сетап если /rpc
аутентифицируется через JWT:
# config/packages/security.yaml
security:
firewalls:
rpc:
pattern: ^/rpc
stateless: true
jwt: ~
# или другая ваша схема
То, что лежит в token storage как UserInterface, становится Context::$user
и питает RoleMatch проверки.
public function __invoke(MyRequest $req, Context $ctx): array
{
$userId = $ctx->user?->getUserIdentifier(); // null для anon
$isAdmin = $ctx->hasRole('ROLE_ADMIN');
// …
}
Context read-only, per-call. См. Context.
Если используется #[Rpc\Cache], бандл поставляется с UserScope — кэш
ключится per user identifier:
#[Rpc\Method('user.profile', roles: ['ROLE_USER'])]
#[Rpc\Cache(ttl: 60, scope: UserScope::class)]
final class GetMyProfile { /* … */ }
См. Кэширование.
RateLimitScope::User ключит счётчик rate limit’а по user identifier’у:
#[Rpc\Method('billing.heavyReport', roles: ['ROLE_USER'])]
#[Rpc\RateLimit(limit: 5, intervalSec: 60, scope: RateLimitScope::User)]
final class HeavyReport { /* … */ }
Анонимные шарят слот anon — обычно это нужное поведение (троттлить аноним
жестко).
/rpc, /mcp/call, /rpc/streamrolesMatch: All для критичныхexpose_role_names: false в продеscope: Ip)max_request_size — ваш максимум приемлемого payload’а (default 1 MB)mcp.apply_rate_limit: true