惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Каналы и авторизация в Centrifugo: как безопасно подключить real-time в Laravel
maxyc_webber · 2026-05-15 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение10 мин

Охват и читатели0

Туториал

В предыдущей статье мы разобрали архитектуру Laravel + Centrifugo: Laravel отвечает за бизнес-логику, права доступа и события, Centrifugo — за WebSocket-соединения, каналы и доставку сообщений, а frontend — за подключение, подписку и обновление интерфейса. Теперь нужно разобрать самый важный слой этой схемы — каналы и авторизацию.

Real-time без авторизации опасен. Если пользователь может подписаться на чужой канал, он может получать чужие уведомления, статусы заказов, сообщения, события админки или финансовые обновления. В обычном HTTP API такая ошибка выглядела бы как доступ к чужому endpoint. В WebSocket-архитектуре ошибка такая же, просто выглядит менее очевидно.

Поэтому при интеграции Centrifugo и Laravel нужно разделять две задачи:

  1. Аутентификация подключения: кто этот пользователь?

  2. Авторизация подписки: имеет ли этот пользователь право слушать конкретный канал?

Для первой задачи используется connection token. Для второй — subscription token. Оба механизма строятся вокруг JWT, но решают разные задачи. Centrifugo использует JWT от backend-приложения для безопасной аутентификации real-time подключения и определения пользователя в системе.  

Что такое канал в Centrifugo

Канал в Centrifugo — это логический маршрут доставки сообщений. Backend публикует сообщение в канал, а все клиенты, подписанные на этот канал, получают публикацию. Centrifugo работает по модели PUB/SUB: есть издатели сообщений, есть подписчики, а канал связывает их между собой.  

Примеры каналов:

public:news
public:stats
$users:15
$orders:7821
$admin:payments
chat:room:45

Для Laravel-разработчика канал можно воспринимать как адрес доставки real-time события. Например, если заказ 7821 изменил статус, Laravel может опубликовать событие в канал заказа или в персональный канал пользователя.

Пример публикации:

{
  "channel": "$users:15",
  "data": {
    "type": "order.status.changed",
    "orderId": 7821,
    "status": "paid"
  }
}

Сам по себе канал не является безопасным. Это просто строка. Если frontend может свободно подписаться на любую строку, значит он может попробовать подписаться на чужой канал. Поэтому безопасность строится не на названии канала, а на проверке прав подписки.

Публичные и приватные каналы

В Centrifugo можно использовать публичные и приватные каналы.

Публичные каналы подходят для данных, которые действительно можно показывать всем подключённым клиентам. Например:

public:news
public:system-status
public:landing-counter

Такие каналы могут использоваться для публичных счётчиков, общей ленты событий, демонстрационных страниц, открытых статусов или данных без персональной чувствительности.

Приватные каналы нужны для персональных и ограниченных данных:

$users:15
$orders:7821
$admin:payments
$moderation:queue

В Centrifugo каналы, начинающиеся с символа $, считаются приватными. Для подписки на такой канал backend должен выдать отдельный токен подписки, чтобы можно было контролировать право пользователя на доступ к каналу.  

Это важный принцип. Подключиться к Centrifugo — ещё не значит получить право слушать все каналы. Пользователь может быть успешно аутентифицирован, но это не даёт ему доступ к чужим заказам, чужим уведомлениям или административным событиям.

Для Laravel-проекта безопаснее начинать именно с приватных каналов. Публичные каналы стоит использовать только там, где утечка данных невозможна по определению.

Connection token: кто подключается к Centrifugo

Connection token нужен для подключения пользователя к Centrifugo. Его задача — доказать Centrifugo, что клиент действительно связан с пользователем вашего Laravel-приложения.

Типовой поток такой:

1. Пользователь авторизован в Laravel.
2. Frontend запрашивает у Laravel connection token.
3. Laravel генерирует JWT с идентификатором пользователя.
4. Frontend передаёт этот token при подключении к Centrifugo.
5. Centrifugo принимает подключение и знает user ID.

Пример endpoint-а в Laravel:

namespace App\Http\Controllers\Realtime;

use Firebase\JWT\JWT;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CentrifugoConnectionTokenController
{
    public function __invoke(Request $request): JsonResponse
    {
        $user = $request->user();

        $token = JWT::encode(
            payload: [
                'sub' => (string) $user->id,
                'exp' => time() + 300,
            ],
            key: config('services.centrifugo.token_secret'),
            alg: 'HS256',
        );

        return response()->json(data: [
            'token' => $token,
        ]);
    }
}

Поле sub указывает идентификатор пользователя. Время жизни токена лучше делать ограниченным. Бессрочные токены для real-time подключения — плохая привычка. Они удобны ровно до первого инцидента.

Маршрут должен быть закрыт обычной Laravel-авторизацией:

use App\Http\Controllers\Realtime\CentrifugoConnectionTokenController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->get(
    '/realtime/connection-token',
    CentrifugoConnectionTokenController::class,
);

На frontend это может выглядеть так:

import { Centrifuge } from 'centrifuge';

const centrifuge = new Centrifuge('wss://example.com/connection/websocket', {
    getToken: async function () {
        const response = await fetch('/realtime/connection-token', {
            headers: {
                Accept: 'application/json',
            },
            credentials: 'same-origin',
        });

        const data = await response.json();

        return data.token;
    },
});

centrifuge.connect();

Connection token отвечает только на вопрос: «Кто подключился?». Он не должен автоматически давать доступ ко всем каналам.

Subscription token: право доступа к конкретному каналу

Subscription token нужен для подписки на конкретный канал. Это отдельная проверка. Centrifugo поддерживает механизм channel JWT authorization: клиент передаёт subscription token при подписке, а корректный токен сообщает Centrifugo, что подписку нужно принять.  

Это принципиальное отличие от connection token.

Connection token:

Пользователь 15 действительно подключается к Centrifugo.

Subscription token:

Пользователь 15 действительно имеет право слушать канал $users:15.

Если эту разницу игнорировать, появляется типичная уязвимость: пользователь авторизован, но может подписаться на чужой канал.

Пример endpoint-а для выдачи subscription token:

namespace App\Http\Controllers\Realtime;

use Firebase\JWT\JWT;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CentrifugoSubscriptionTokenController
{
    public function __invoke(Request $request): JsonResponse
    {
        $user = $request->user();
        $channel = (string) $request->input(key: 'channel');

        abort_if(
            boolean: $channel !== '$users:' . $user->id,
            code: 403,
        );

        $token = JWT::encode(
            payload: [
                'sub' => (string) $user->id,
                'channel' => $channel,
                'exp' => time() + 300,
            ],
            key: config('services.centrifugo.token_secret'),
            alg: 'HS256',
        );

        return response()->json(data: [
            'token' => $token,
        ]);
    }
}

Маршрут также должен быть закрыт middleware auth:

use App\Http\Controllers\Realtime\CentrifugoSubscriptionTokenController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->post(
    '/realtime/subscription-token',
    CentrifugoSubscriptionTokenController::class,
);

Главный смысл этого кода — не генерация JWT. Главный смысл — проверка перед генерацией:

$channel !== '$users:' . $user->id

Пользователь с ID 15 может получить token только для канала $users:15. Если он попросит $users:16, Laravel вернёт 403 Forbidden.

Иначе зачем вообще была вся авторизация.

Подписка frontend на приватный канал

На frontend подписка на приватный канал должна получать subscription token с backend-а.

Пример:

const userId = 15;
const userChannel = `$users:${userId}`;

const subscription = centrifuge.newSubscription(userChannel, {
    getToken: async function () {
        const response = await fetch('/realtime/subscription-token', {
            method: 'POST',
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json',
            },
            credentials: 'same-origin',
            body: JSON.stringify({
                channel: userChannel,
            }),
        });

        if (response.status === 403) {
            throw new Error('Subscription forbidden');
        }

        const data = await response.json();

        return data.token;
    },
});

subscription.on('publication', function (context) {
    if (context.data.type === 'order.status.changed') {
        updateOrderStatus(
            context.data.orderId,
            context.data.status,
        );
    }
});

subscription.subscribe();

Этот код демонстрирует механику, но есть важное уточнение: userId на frontend не должен быть источником истины. Пользователь может изменить JavaScript в браузере, подменить channel name и запросить другой канал. Поэтому Laravel обязан проверять канал на своей стороне.

Более аккуратный вариант — не давать frontend самому конструировать чувствительные каналы. Laravel может вернуть список доступных каналов вместе с начальным состоянием страницы:

{
  "user": {
    "id": 15,
    "name": "Max"
  },
  "realtime": {
    "channels": [
      "$users:15"
    ]
  }
}

Но даже в этом случае проверка subscription token на стороне Laravel всё равно обязательна. Данные, пришедшие от клиента, нельзя считать доверенными только потому, что пять секунд назад backend сам их отдал.

Проверка прав на стороне Laravel

Для персонального канала $users:{id} проверка простая: ID в канале должен совпадать с ID текущего пользователя.

Но реальные проекты редко ограничиваются только персональными каналами. Часто появляются каналы заказов, чатов, админских разделов, проектов, команд и организаций.

Примеры правил:

$users:15
Пользователь может слушать только свой канал.

$orders:7821
Пользователь может слушать канал заказа, только если заказ принадлежит ему.

chat:room:45
Пользователь может слушать комнату, только если он участник чата.

$admin:payments
Пользователь может слушать канал, только если у него есть административная роль.

project:88
Пользователь может слушать канал, только если он состоит в проекте.

Пример проверки канала заказа:

namespace App\Http\Controllers\Realtime;

use App\Models\Order;
use Firebase\JWT\JWT;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class CentrifugoOrderSubscriptionTokenController
{
    public function __invoke(Request $request): JsonResponse
    {
        $user = $request->user();
        $channel = (string) $request->input(key: 'channel');

        abort_if(
            boolean: ! Str::startsWith($channel, '$orders:'),
            code: 403,
        );

        $orderId = (int) Str::after($channel, '$orders:');

        $order = Order::query()->findOrFail($orderId);

        abort_if(
            boolean: $order->user_id !== $user->id,
            code: 403,
        );

        $token = JWT::encode(
            payload: [
                'sub' => (string) $user->id,
                'channel' => $channel,
                'exp' => time() + 300,
            ],
            key: config('services.centrifugo.token_secret'),
            alg: 'HS256',
        );

        return response()->json(data: [
            'token' => $token,
        ]);
    }
}

В production лучше не делать один огромный контроллер с if на все типы каналов. Нормальный вариант — вынести проверку в отдельный resolver или policy-слой.

Например:

namespace App\Services\Realtime;

use App\Models\User;

class RealtimeChannelAuthorizationService
{
    public function canSubscribe(
        User $user,
        string $channel,
    ): bool {
        if ($channel === '$users:' . $user->id) {
            return true;
        }

        if ($this->isOrderChannelAllowed(
            user: $user,
            channel: $channel,
        )) {
            return true;
        }

        return false;
    }

    private function isOrderChannelAllowed(
        User $user,
        string $channel,
    ): bool {
        // Проверка канала заказа через модель, policy или repository.
        return false;
    }
}

Контроллер тогда становится чище:

namespace App\Http\Controllers\Realtime;

use App\Services\Realtime\RealtimeChannelAuthorizationService;
use Firebase\JWT\JWT;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CentrifugoSubscriptionTokenController
{
    public function __construct(
        private readonly RealtimeChannelAuthorizationService $authorizationService,
    ) {
    }

    public function __invoke(Request $request): JsonResponse
    {
        $user = $request->user();
        $channel = (string) $request->input(key: 'channel');

        abort_if(
            boolean: ! $this->authorizationService->canSubscribe(
                user: $user,
                channel: $channel,
            ),
            code: 403,
        );

        $token = JWT::encode(
            payload: [
                'sub' => (string) $user->id,
                'channel' => $channel,
                'exp' => time() + 300,
            ],
            key: config('services.centrifugo.token_secret'),
            alg: 'HS256',
        );

        return response()->json(data: [
            'token' => $token,
        ]);
    }
}

Так архитектура остаётся расширяемой. Можно добавлять новые типы каналов, не превращая endpoint выдачи токенов в свалку условий.

Пример: пользователь может подписаться только на свой канал

Разберём самый базовый и самый полезный сценарий: пользовательские уведомления через канал $users:{id}.

Есть пользователь с ID 15. Его персональный real-time канал:

$users:15

Laravel публикует туда события:

$publisher->publish(
    channel: '$users:' . $user->id,
    data: [
        'type' => 'notification.created',
        'notificationId' => $notification->id,
    ],
);

Frontend пользователя подписывается на этот канал:

const subscription = centrifuge.newSubscription('$users:15', {
    getToken: getSubscriptionToken,
});

Пользователь пытается подменить канал на $users:16. Frontend отправляет запрос:

{
  "channel": "$users:16"
}

Laravel проверяет:

abort_if(
    boolean: $channel !== '$users:' . $request->user()->id,
    code: 403,
);

Если текущий пользователь — 15, доступ к $users:16 запрещён.

Это минимальная, но принципиальная защита.

Ошибки, которые чаще всего ломают безопасность real-time

Первая ошибка — считать, что если пользователь авторизован в Laravel, он автоматически имеет право на любой канал Centrifugo. Нет. Авторизация подключения и авторизация подписки — разные задачи.

Вторая ошибка — доверять имени канала с frontend. Клиент может прислать любой channel name. Laravel должен проверить его через текущего пользователя, модель, роль, policy или доменное правило.

Третья ошибка — делать каналы публичными «для простоты». Для публичных данных это допустимо. Для пользовательских уведомлений, заказов, чатов, платежей и админских событий — нет.

Четвёртая ошибка — выдавать слишком долгоживущие токены. Чем дольше живёт token, тем дольше сохраняется риск при его утечке или изменении прав пользователя.

Пятая ошибка — отправлять в канал больше данных, чем нужно. Даже правильно авторизованный канал не должен получать лишние поля. Payload должен быть минимальным.

Практические правила проектирования каналов

Для Laravel + Centrifugo можно использовать следующие правила.

Персональные события отправлять в канал пользователя:

$users:{userId}

События конкретной сущности отправлять в канал сущности только при наличии понятной проверки доступа:

$orders:{orderId}
chat:room:{roomId}
project:{projectId}

Административные события отделять от пользовательских:

$admin:orders
$admin:payments
$admin:risk-events

Не использовать guessable channel name как единственную защиту. Канал $orders:7821 легко угадать. Защита должна быть не в сложности строки, а в backend-проверке.

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

Заключение

Каналы и авторизация — центральная часть безопасной real-time архитектуры на Laravel и Centrifugo. Канал определяет, куда доставляется сообщение. Connection token подтверждает, кто подключается к Centrifugo. Subscription token подтверждает, что этот пользователь имеет право слушать конкретный канал.

Laravel должен оставаться владельцем бизнес-правил и прав доступа. Именно backend решает, может ли пользователь подписаться на $users:{id}$orders:{orderId}chat:room:{id} или $admin:payments. Centrifugo доставляет сообщения, но не должен подменять доменную авторизацию приложения.

Без этой границы real-time быстро становится уязвимостью. С этой границей он становится нормальной частью архитектуры: безопасной, расширяемой и понятной для backend-разработчиков.