Как легко и быстро развернуть API на фреймворке Symfony с уже встроенной валидацией и авторизацией по токену?
Можно воспользоваться бандлом для Symfony 6+
Установка бандла
Как и любая другая библиотека ставится этот бандл с помощью композера:
composer require otezvikentiy/json-rpc-api
Далее проверяем, что бандл попал в инсталляцию в файле config/bundles.php файле:
<?php // config/bundles.php return [ //... OV\JsonRPCAPIBundle\OVJsonRPCAPIBundle::class => ['all' => true], ];
После этого нам нужно создать файл config/routes/ov_json_rpc_api.yaml со следующим содержимым:
# config/routes/ov_json_rpc_api.yaml ov_json_rpc_api: resource: '@OVJsonRPCAPIBundle/config/routes/routes.yaml'
И в файлике config/services.yaml в секцию services д��бавить раздел с той папкой, которая предполагается для реализации всех ваших дальнейших методов API. Например так:
# config/services.yaml services: App\RPC\V1\: resource: '../src/RPC/V1/{*Method.php}' tags: - { name: ov.rpc.method, namespace: App\RPC\V1\, version: 1 }
На этом всё! Установка закончена - вы можете создавать методы своего API!
Испытания и первые методы
Теперь вы можете создать ваш первый метод API. Давайте попробуем реализовать какой-нибудь тестовый простой метод. Для этого нам потребуется создать 3 файлика вот по такой вот схеме:
└── src └── RPC └── V1 └── getProducts ├── GetProductsRequest.php └── GetProductsResponse.php └── GetProductsMethod.php
<?php namespace App\RPC\V1\getProducts; class GetProductsRequest { private int $id; private string $title; /** * @param int $id */ public function __construct(int $id) { $this->id = $id; } /** * @return int */ public function getId(): int { return $this->id; } /** * @param int $id */ public function setId(int $id): void { $this->id = $id; } /** * @return string */ public function getTitle(): string { return $this->title; } /** * @param string $title */ public function setTitle(string $title): void { $this->title = $title; } }
<?php namespace App\RPC\V1\getProducts; class GetProductsResponse { private bool $success; private string $title; /** * @param string $title * @param bool $success */ public function __construct(string $title, bool $success = true) { $this->success = $success; $this->title = $title; } /** * @return string */ public function getTitle(): string { return $this->title; } /** * @param string $title */ public function setTitle(string $title): void { $this->title = $title; } /** * @return bool */ public function isSuccess(): bool { return $this->success; } /** * @param bool $success */ public function setSuccess(bool $success): void { $this->success = $success; } }
<?php namespace App\RPC\V1; use OV\JsonRPCAPIBundle\Core\Annotation\JsonRPCAPI; use App\RPC\V1\getProducts\GetProductsRequest; use App\RPC\V1\getProducts\GetProductsResponse; /** * * @JsonRPCAPI(methodName = "getProducts") */ #[JsonRPCAPI(methodName: 'getProducts')] class GetProductsMethod { /** * @param GetProductsRequest $request * @return GetProductsResponse */ public function call(GetProductsRequest $request): GetProductsResponse { // здесь осуществляете всю логику вашего метода API $id = $request->getId(); return new GetProductsResponse($request->getTitle().'OLOLOLOLO'); } }
Теперь вы можете выполнить curl-запрос например так:
curl --header "Content-Type: application/json" --request POST --data '{"jsonrpc": "2.0","method": "getProducts","params": {"title": "AZAZAZA"},"id": 1}' http://localhost/api/v1
И получите ответ:
{"jsonrpc":"2.0","result":{"title":"AZAZAZAOLOLOLOLO","success":true},"id":null}
Получается для реализации простейшего Json RPC API вам потребуется создать всего 3 класса.
Авторизация по токену
Авторизация по токену реализуется стандартно по документации symfony, но все же расскажу еще разок.
Чтобы доступ в ваше API был по токену - вот пример реализации что для этого нужно сделать:
1) создаем сущность нашего токена в БД в файле src/Entity/ApiToken.php
<?php namespace App\Entity; use DateTime; use DateTimeInterface; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class ApiToken { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private int $id; #[ORM\Column(type: 'string', length: 500, nullable: false)] private string $token; #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: false)] private DateTimeInterface $expiresAt; #[ORM\ManyToOne(inversedBy: 'apiTokens')] #[ORM\JoinColumn(nullable: false)] private User $user; /** * @return int */ public function getId(): int { return $this->id; } /** * @param int $id * * @return ApiToken */ public function setId(int $id): ApiToken { $this->id = $id; return $this; } /** * @return string */ public function getToken(): string { return $this->token; } /** * @param string $token * * @return ApiToken */ public function setToken(string $token): ApiToken { $this->token = $token; return $this; } /** * @return DateTimeInterface */ public function getExpiresAt(): DateTimeInterface { return $this->expiresAt; } /** * @param DateTimeInterface $expiresAt * * @return ApiToken */ public function setExpiresAt(DateTimeInterface $expiresAt): ApiToken { $this->expiresAt = $expiresAt; return $this; } /** * @return User */ public function getUser(): User { return $this->user; } /** * @param User $user * * @return ApiToken */ public function setUser(User $user): ApiToken { $this->user = $user; return $this; } /** * @return bool */ public function isValid(): bool { return (new DateTime())->getTimestamp() > $this->expiresAt->getTimestamp(); } }
2) создаем кастомный аутентификатор в файле src/Security/ApiKeyAuthenticator.php
<?php namespace App\Security; use App\Entity\ApiToken; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class ApiKeyAuthenticator extends AbstractAuthenticator { /** * @param EntityManagerInterface $em */ public function __construct( private readonly EntityManagerInterface $em ){ } /** * @param Request $request * * @return bool */ public function supports(Request $request): bool { return str_contains($request->getRequestUri(), '/api/v'); } /** * @param Request $request * * @return Passport */ public function authenticate(Request $request): Passport { $apiToken = $request->headers->get('X-AUTH-TOKEN'); if (null === $apiToken) { throw new CustomUserMessageAuthenticationException('No API token provided'); } $apiTokenEntity = $this->em->getRepository(ApiToken::class)->findOneBy(['token' => $apiToken]); if (is_null($apiTokenEntity)) { throw new CustomUserMessageAuthenticationException('No API token provided'); } return new SelfValidatingPassport(new UserBadge( $apiTokenEntity->getUser()->getId(), function () use ($apiTokenEntity) { return $this->em->getRepository(User::class)->find($apiTokenEntity->getUser()->getId()); } )); } /** * @param Request $request * @param TokenInterface $token * @param string $firewallName * * @return Response|null */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; } /** * @param Request $request * @param AuthenticationException $exception * * @return Response|null */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $data = [ 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) ]; return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); } }
3) добавляем новый firewall в файле config/security.yaml
security: # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: # used to reload user from session & other features (e.g. switch_user) app_user_provider: entity: class: App\Entity\User property: email firewalls: api: pattern: ^/api provider: app_user_provider custom_authenticators: - App\Security\ApiKeyAuthenticator
4) создаем миграцию для токена, проводим миграцию, создаем какой-нибудь тестовый токен и можем пользоваться. ))) С токеном запрос curl будет выглядеть примерно так:
curl --header "X-AUTH-TOKEN: your-token-here" --header "Content-Type: application/json" --request POST --data '{"jsonrpc": "2.0","method": "getProducts","params": {"title": "AZAZAZA"},"id": 1}' http://localhost/api/v1
