JsonRpc сервер - реализация сервера по спецификации JsonRpc 2.0.
Поддерживаемые версии:
- Laravel >= 10.0
- PHP >= 8.2
Поддерживает:
- вызов удаленных методов по нотификации имяКонтроллера_имяМетода, либо с разделением логики на несколько ресурсных точек входа
- вызов нескольких удаленных методов в одном запросе
- передача параметров в метод контроллера по имени
- маппинг параметров в DTO
- аутентификация с помощью токена, переданного в заголовке
- контроль доступа по IP-адресам
- контроль доступа к методам для разных сервисов - ACL
- возможность поддержки нескольких серверов в рамках одно сервиса
- расширение возможностей с помощью механизма Middleware
- кеширование всех роутов для увеличения быстродействия
Установка через composer:
composer require tochka-developers/jsonrpcДля Laravel есть возможность опубликовать конфигурацию для всех пакетов:
php artisan vendor:publishДля того чтобы опубликовать только конфигурацию данного пакета, можно воспользоваться опцией tag
php artisan vendor:publish --tag="jsonrpc-config"Пропишите в вашем route.php:
\Route::post('/v1/public/jsonrpc', function (Request $request) {
$server = new JsonRpcServer(ServerConfig::makeFromConfigFile('default'));
return $server->handle($request->getContent());
});return [
// можно настроить несколько разных конфигурация для разных точек входа,
// чтобы указать в маршруте, какая именно конфигурация должна быть использована - используйте имя при создании сервера
'default' => [
// Краткое описание сервера
'summary' => 'Основная точка входа',
// Полное описание сервера
'description' => 'Основная точка входа',
// Namespace, в котором находятся контроллеры
'namespace' => 'App\\Http\\Controllers\\Api\\',
// Suffix для контроллеров
'controllerSuffix' => 'Controller',
// Разделитель для имен методов
'methodDelimiter' => '_',
// Использовать методы родителя при наследовании
'allowParentMethods' => false,
// список Middleware, обрабатывающих запросы
// описание middleware ниже
'middleware' => [ //
Tochka\JsonRpc\Middleware\LogMiddleware::class => [
// Канал лога, в который будут записываться все логи
'channel' => 'default',
// Заголовки которые нужно писать в лог
'headers' => ['accept', 'encoding']
],
Tochka\JsonRpc\Middleware\TokenAuthMiddleware::class => [
'headerName' => 'X-Tochka-Access-Key',
// Ключи доступа к API
'tokens' => [
'troll' => env('JSONRPC_KEY_TROLL', ''),
],
],
Tochka\JsonRpc\Middleware\AccessControlListMiddleware::class => [
'acl' => [
'*' => '*',
FooController::class => ['service'],
FooController::class . '@method' => ['service2'],
],
],
],
],
];Обработчики позволяют подготовить запрос, прежде чем вызывать указанный метод. Список обработчиков задается в параметре
jsonrpc.middleware. Это массив, в котором необходимо перечислить в порядке очереди классы обработчиков.
По умолчанию доступны следующие обработчики:
Tochka\JsonRpc\Middleware\LogMiddleware- логирование входящих запросовTochka\JsonRpc\Middleware\TokenAuthMiddleware- авторизация сервиса по токену в заголовкеTochka\JsonRpc\Middleware\ServiceValidationMiddleware- валидация сервиса по его IP-адресуTochka\JsonRpc\Middleware\AccessControlListMiddleware- ACL - правила доступа разных сервисов к разным методам и группам Jsonrpc-сервера
Кроме того, вы можете использовать свои Middleware для любых других целей (авторизация через BasicAuth, дополнительная фильтрация/валидация данных, etc)
Необходима, если вы хотите ограничить доступ к сервису и использовать в качестве аутентификации доступ по токену в заголовке. В конфигурацию обработчика передаются следующие параметры:
[
'headerName' => 'X-Tochka-Access-Key',
'tokens' => [
'service_foo' => env('JSONRPC_KEY_SERVICE_FOO', \Str::uuid()->toString()),
'service_bar' => env('JSONRPC_KEY_SERVICE_BAR', \Str::uuid()->toString()),
]headerName- имя заголовка, в котором клиент должен передать токенtokens- список сервисов-клиентов и их токены
Если запрос был осуществлен без данного заголовка, либо с токеном, которого нет в списке - клиенту вернется ошибка.
Если аутентификация прошла успешно - клиент будет опознан как service_foo (service_bar), что позволит
контролировать доступ к методам. Если аутентификация отключена - клиент будет опознан как guest.
Подключайте, если необходимо ограничить список IP-адресов, с которых сервис-клиент может осуществлять запросы. В конфигурацию обработчика передаются следующие параметры:
[
'servers' => [
'service_foo' => ['192.168.0.1', '192.168.1.5'],
'service_bar' => '*',
],
]В указанном примере авторизоваться с ключом доступа сервиса service_foo могут только клиенты с IP адресами
192.168.0.1 и 192.168.1.5. Сервис service_bar может авторизоваться с любых IP адресов.
Если включен обработчик AccessControlListMiddleware, то будет осуществлен контроль доступа к методам.
В конфигурацию обработчика передаются следующие параметры:
[
'acl' => [
'*' => ['service1', 'service2'],
'App\Http\TestController' => ['service1'],
'App\Http\TestController@isActivationAllowed' => ['service2'],
]
]Для логирования входящих запросов используется LogMiddleware. В конфигурацию обработчика передаются следующие параметры:
[
'channel' => 'jsonrpc',
'hideParams' => [
UserController::class => ['password', 'old_password', 'new_password'],
],
]channel- канал логирования. Любой из каналов, описанных в конфигурацииlogging.phphideParams- скрывает из логирования указанные параметры (в случае конфиденциальных данных).
Описывается с помощью ассоциативного массива, в котором ключами выступает целевой метод, в котором необходимо скрыть параметры, а значения - имена параметров, которые необходимо скрыть. В качестве ключей может быть:
*- правило распространяется для всего сервера (для всех контроллеров и методов)App\Http\Controllers\UserController- правило распространяется для всех методов указанного контроллераApp\Http\Controllers\UserController@auth- правило распространяется для указанного метода в указанном контроллере
Для ускорения поиска нужного метода и правильной просадки параметров пакет генерирует список всех маршрутов с описанием этих параметров.
Для вывода всех доступных маршрутов вы можете воспользоваться командой artisan jsonrpc:route:list {?server}:
> php artisan jsonrpc:route:list
+---------------------------+-----------------------------------------+----------------------+
| Method | Controller | Call |
+---------------------------+-----------------------------------------+----------------------+
| user_test1 | App\Http\Controllers\Api\UserController | test1 |
| user_test2 | App\Http\Controllers\Api\UserController | test2 |
| user_test4 | App\Http\Controllers\Api\UserController | test4 |
+--------------------------------------------------------------------------------------------+
При каждом входящем запросе схема маршрутов заново пересчитывается. Чтобы этого не происходило, необходимо закешировать
все маршруты с помощью команды jsonrpc:route:cache {?serverName}. Построенная схема сохранится в файл и будет использоваться каждый
раз при следующих запросах.
Учтите это при разработке - пока есть построенная закешированная схема маршрутов - любые изменения маршрутов в коде не будут отражаться на выполнении запросов.
Рекомендуется выполнять команду кеширования маршрутов сразу после деплоя на рабочие инстансы (по аналогии с командой
Laravel config:cache).
Чтобы очистить кеш - выполните команду jsonrpc:route:clear {?serverName}. После очистки кеша маршруты снова будут строиться каждый
раз при входящем запросе.
При запросе, сервер берём все параметры указанные в методе и пытается просадить их в метод. Все параметры которые не указаны в методе будут проигнорированы. При создании списка маршрутов для каждого параметра определяется тип и в зависимости от типа входной параметр будет проверен и подставлен в метод.
Тип параметра не указан или указан как mixed. Просто подставляем.
/**
* В этом примере два входных параметра, оба могут быть любого типа
*
*/
public function test1($a, mixed $b)
{
//....
}Тип параметра примитив, так же может быт UnionType из примитивов (int|string|bool)
/**
* Пример простых типов
*
*/
public function test1(int $a, string $b, ?string $c, bool|string|int|float|object $d)
{
//....
}Будет создан экземпляр указанного Enum`a
/**
* Будет создан экземпляр MyEnum
*
*/
public function test1(MyEnum $a)
{
//....
}Параметром является объект который будет создан сервером. При этом тип должен быть задан явно и не содержать перечисления типов. В таком случае создаётся неоднозначность того что должно быть создано. Если вам нужно создавать разные объекты в зависимости от какой-то логики, то лучше обернуть это в объект обёртку.
/**
* Тут сервер попытается создать объект указанного класса.
*
*/
public function test1(SomeObject $a)
{
//....
}/**
* Такое работать не будет
* @deprecated
*/
public function test1(SomeObject|SomeObject2 $a)
{
//....
}Параметр, который создаётся из DI, при этом от входных параметров он не зависит. Создать такой параметр можно указав аттрибут ApiDi.
/**
* В этом примере входных параметров у апи нет, но при вызове будет создан AuthManager через контейнер.
*
*/
public function test1(#[ApiDI] AuthManager $b)
{
//....
}В этом варианте все входные параметры будут представлены в виде параметров одного объекта
/**
* Все параметры будут преобразованы в параметры объекта AllParamsRequestObject
*
*/
public function test1(#[ApiParams] AllParamsRequestObject $a)
{
//....
}
class AllParamsRequestObject {
public string $a;
public string $b;
public bool $c;
}Объекту можно добавить трейт WithValidation, если переопределить методы то можно настроить валидациия для данного объекта.
/** Если метод вернёт пустой массив, то валидация будет проигнорирована */
public static function rules(): array
{
return [
'date' => ['nullable', 'date:Y-m-d'],
];
}
/**
* Можно переопределить сообщения об ошибках
*/
public static function messages(): array
{
return [];
}
/**
* Можно переопределить атрибуты передаваемые в Validator
*/
public static function attributes(): array
{
return [];
}Объекту можно добавить трейт WithDataMap, если переопределить метод dataMap, то можно управлять созданием объекта.
// В случае если трейт подключён, но метод не переопределён, то запустится
// стандартное создание объекта, как если бы трейт не был указан
public static function dataMap(RouteParam $param, array|object $data): self
{
return new self($data)
}По умолчанию в схему маршрутизации попадают все публичные методы всех контроллеров, найденных в указанном в конфигурации
пространстве имен. Если вам необходимо исключить из маршрутизации часть методов - воспользуйтесь аннотацией
ApiIgnore:
/**
* Использование аннотации/атрибута @ApiIgnore для класса исключает все методы класса из маршрутизации
* @ApiIgnore
*/
class ApiController extends BaseController
{
// ...
}Можно проверить все параметры запроса с помощью объекта запроса.
public function testValidation(#[ApiDi] \Tochka\JsonRpc\Support\JsonRpcRequest $request, object $data)
{
// в validated попадут только те поля для которых указаны правила валидации
$validated = $request->validate([
'object.field' => 'int|string',
'object.field2' => 'required|bool',
]);
}Стандартный метод $request->validate при ошибках валидации выбрасывает InvalidParametersException. Если по какой-то причине вы хотите обработать ошибки самостоятельно, можно вызвать validateSilent
public function testValidation(#[ApiDi] \Tochka\JsonRpc\Support\JsonRpcRequest $request, object $data)
{
// В этом случае не будет исключений при провале валидации
$validated = $request->validateSilent([
'object.field' => 'int|string',
'object.field2' => 'required|bool',
]);
// получить список ошибок
$request->getValidationErrors();
// получить список полей что не прошли валидацию
$request->getValidationFailed();
}Клиент посылает валидный JsonRpc2.0-запрос:
{
"jsonrpc": "2.0",
"method": "client_getInfoById",
"params": {
"clientCode": "100500",
"fromAgent" : true
},
"id": "service-ab34f8290cfa367dacb"
}JsonRpc сервер пытается найти указанный метод client_getInfoById.
Имя метода разбивается на части: имяКонтроллера_имяМетода.
Класс контроллера ищется по указанному пространству имен (параметр jsonrpc.controllerNamespace) с указанным суффиксом
(по умолчанию Controller). Для нашего примера сервер попытается подключить класс 'App\Http\Controller\ClientController'.
Если контроллер не существует - клиенту вернется ошибка Method not found.
В найденном контроллере вызывается метод getInfoById.
Все переданные параметры будут переданы в метод по именам.
То есть в контроллере должен быть метод getInfoById($clientCode, $fromAgent).
Все параметры будут отвалидированы по типам (если типы указаны). Кроме того, таким способом можно указывать необязательные
параметры в методе - в таком случае их необязательно передавать в запросе, вместо непереданных параметров будут
использованы значения по умолчанию из метода.
Если не будет передан один из обязательных параметров - клиенту вернется ошибка.
По спецификации JsonRpc разрешено вызывать несколько методов в одном запросе. Для этого необходимо валидные JsonRpc2.0-вызовы передать в виде массива. Каждый вызываемый метод будет вызван из соответствующего контроллера, а вернувшиеся результаты будут возвращены клиенту в том порядке, в котором пришли запросы.
В ответе клиенту всегда присутствует параметр id, если таковой был передан клиентом. Данный параметр также позволяет идентифицировать ответы на свои запросы на стороне клиента.
Для документирования JsonRpc используется спецификация OpenRpc (https://spec.open-rpc.org/).
Для генерации OpenRpc-схемы можете использовать совместимый с текущей версией jsonrpc-server пакет tochka-developers/openrpc
- Если будете использовать tochka-developers/openrpc. Перед установкой убедитесь что версия tochka-developers/jsonrpc-client больше чем 3.11 (в более старых несовместимость с phpdocumentor/reflection-docblock v6)
- Удалить из config/jsonrpc строки endpoint, dynamicEndpoint
- Если используете атрибут #[ApiIgnore], замените на @ApiIgnore (это временное решение для переезда, в дальнейшем методы апи будут указываться явно через #[ApiMethod])
- Обновляем версию
composer r tochka-developers/jsonrpc:^v5.0
// если испльзуете tochka-developers/openrpc, её тоже нужно обновить
composer r tochka-developers/jsonrpc:^v5.0 tochka-developers/openrpc:^v2.0
- Замените вызов сервера
// до
Route::post('/api/v1/public/jsonrpc', function (Request $request) {
return JsonRpcServer::handle($request->getContent(), 'default');
});
// после
Route::post('/api/v1/public/jsonrpc', function (Request $request) {
$config = ServerConfig::makeFromConfigFile('default');
$server = new JsonRpcServer($config);
return $server->handle($request->getContent());
});- Если стоит tochka-developers/openrpc, обновите его конфигурацию
Breaking changes
- убрана зависимость bensampo/laravel-enum, если она вам нужна, поставьте самостоятельно, если этот тип объект используется как параметр апи, вам нужно сделать свой PropertyCaster