If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribePara utilizar tokens de API en Swagger, tenemos que escribir la palabra "Bearer" (portador) y luego el token. ¡Lamentable! Sobre todo si pretendemos que lo utilicen usuarios reales. ¿Cómo podemos solucionarlo?
Recuerda que Swagger se genera enteramente a partir del documento de especificaciones OpenAPI que construye API Platform. Puedes ver este documento consultando la fuente de la página -puedes verlo todo ahí mismo- o yendo a /api/docs.json
. Hace unos minutos, hemos añadido una configuración a API Platform llamada Authorization
:
api_platform: | |
... lines 2 - 7 | |
swagger: | |
api_keys: | |
access_token: | |
name: Authorization | |
type: header | |
... lines 13 - 18 |
El resultado final es que ha añadido estas secciones de seguridad aquí abajo. Sí, es así de sencillo: esta configuración activó estas nuevas secciones en este documento JSON: nada más. Swagger entonces lee eso y sabe que debe hacer que esta "Autorización" esté disponible.
Así que indagué un poco directamente en el sitio de OpenAPI y descubrí que sí tiene una forma de definir un esquema de autenticación en el que no necesitas pasar manualmente la parte del "Portador". Desgraciadamente, a menos que me lo esté perdiendo, la configuración de API Platform no permite añadirlo. Entonces, ¿hemos terminado? De ninguna manera, y por una razón increíble.
Para crear este documento JSON, internamente, API Platform crea un objeto OpenApi
, rellena todos estos datos en él y luego lo envía a través del serializador de Symfony. Esto es importante porque podemos modificar el objeto OpenApi
antes de que pase por el serializador. ¿Cómo? El objeto OpenApi
se crea a través de un núcleoOpenApiFactory
... y podemos decorarlo.
Compruébalo: en el directorio src/
, crea un nuevo directorio llamadoApiPlatform/
... y dentro, una nueva clase PHP llamada OpenApiFactoryDecorator
. Haz que implemente OpenApiFactoryInterface
. Luego ve a "Código"->"Generar" oCommand
+N
en un Mac para implementar el único método que necesitamos: __invoke()
:
... lines 1 - 2 | |
namespace App\ApiPlatform; | |
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |
use ApiPlatform\OpenApi\OpenApi; | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
public function __invoke(array $context = []): OpenApi | |
{ | |
// TODO: Implement __invoke() method. | |
} | |
} |
Ahora mismo, existe un servicio central OpenApiFactory
en la API Platform que crea el objeto OpenApi
con todos estos datos. Éste es nuestro astuto plan: vamos a decirle a Symfony que utilice nuestra nueva clase como OpenApiFactory
en lugar de la del núcleo. Pero... definitivamente no queremos reimplementar toda la lógica del núcleo. Para evitarlo, también le diremos a Symfony que nos pase el núcleo original OpenApiFactory
.
Puede que te resulte familiar lo que estamos haciendo. Es la decoración de clases: una estrategia orientada a objetos para extender clases. Es muy fácil de hacer en Symfony y API Platform lo aprovecha mucho.
Siempre que hagas decoración, crearás un constructor que acepte la interfaz que estás decorando. Así que OpenApiFactoryInterface
. Lo llamaré$decorated
. Y déjame poner private
delante de eso:
... lines 1 - 4 | |
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |
... lines 6 - 9 | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
public function __construct(private OpenApiFactoryInterface $decorated) | |
{ | |
} | |
... lines 15 - 23 | |
} |
Perfecto.
Aquí abajo, para empezar, di $openApi = $this->decorated
y luego llama al método __invoke()
pasándole el mismo argumento: $context
:
... lines 1 - 9 | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 12 - 15 | |
public function __invoke(array $context = []): OpenApi | |
{ | |
$openApi = $this->decorated->__invoke($context); | |
... lines 19 - 22 | |
} | |
} |
Eso llamará a la fábrica del núcleo que hará todo el trabajo duro de crear el objeto OpenApi
completo. Aquí abajo, devuelve eso:
... lines 1 - 9 | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 12 - 15 | |
public function __invoke(array $context = []): OpenApi | |
{ | |
$openApi = $this->decorated->__invoke($context); | |
... lines 19 - 21 | |
return $openApi; | |
} | |
} |
¿Y entre medias? Sí, ¡ahí es donde podemos liarnos! Para asegurarnos de que esto funciona, por ahora, simplemente vuelca el objeto $openApi
:
... lines 1 - 9 | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 12 - 15 | |
public function __invoke(array $context = []): OpenApi | |
{ | |
$openApi = $this->decorated->__invoke($context); | |
dump($openApi); | |
return $openApi; | |
} | |
} |
En este momento, desde un punto de vista orientado a objetos, esta clase está configurada correctamente para la decoración. Pero el contenedor de Symfony sigue configurado para utilizar elOpenApiFactory
normal: no va a utilizar nuestro nuevo servicio en absoluto. De alguna manera tenemos que decirle al contenedor que, en primer lugar, el servicio principal OpenApiFactory
debe ser sustituido por nuestro servicio, y en segundo lugar, que el servicio principal original debe pasarse a nosotros.
¿Cómo podemos hacerlo? Encima de la clase, añade un atributo llamado #[AsDecorator]
y pulsa tabulador para añadir esa declaración use
. Pásale el id de servicio del núcleo originalOpenApiFactory
. Puedes indagar un poco para encontrarlo o normalmente la documentación te lo dirá. En realidad, API Platform documenta la decoración de este servicio, así que en sus documentos encontrarás que el identificador del servicio es api_platform.openapi.factory
:
... lines 1 - 6 | |
use Symfony\Component\DependencyInjection\Attribute\AsDecorator; | |
'api_platform.openapi.factory') ( | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 12 - 23 | |
} |
¡Eso es! Gracias a esto, cualquiera que antes utilizara el servicio principal deapi_platform.openapi.factory
recibirá en su lugar nuestro servicio, pero nos pasará el original.
Así que... ¡debería funcionar! Para probarlo, dirígete a la página principal de la API y actualízala. ¡Sí! Cuando esta página se carga, renderiza el documento JSON de OpenAPI en segundo plano. ¡El volcado en la barra de herramientas de depuración web demuestra que ha dado con nuestro código! Y fíjate en ese precioso objeto OpenApi
: lo tiene todo, incluido security
, que coincide con lo que vimos en el JSON. Así que ahora, ¡podemos retocarlo!
El código que voy a poner aquí es un poco específico del objeto OpenApi
y de la configuración exacta que sé que necesitamos en el JSON final de la Open API:
... lines 1 - 9 | |
'api_platform.openapi.factory') ( | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 13 - 16 | |
public function __invoke(array $context = []): OpenApi | |
{ | |
$openApi = $this->decorated->__invoke($context); | |
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject(); | |
... lines 22 - 26 | |
return $openApi; | |
} | |
} |
Obtenemos el objeto $securitySchemes
, y luego anulamos access_token
. Esto coincide con el nombre que utilizamos en la configuración. Establece un nuevo objeto SecurityScheme()
con dos argumentos con nombre: type: 'http'
y scheme: 'bearer'
:
... lines 1 - 5 | |
use ApiPlatform\OpenApi\Model\SecurityScheme; | |
... lines 7 - 9 | |
'api_platform.openapi.factory') ( | |
class OpenApiFactoryDecorator implements OpenApiFactoryInterface | |
{ | |
... lines 13 - 16 | |
public function __invoke(array $context = []): OpenApi | |
{ | |
$openApi = $this->decorated->__invoke($context); | |
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject(); | |
$securitySchemes['access_token'] = new SecurityScheme( | |
type: 'http', | |
scheme: 'bearer', | |
); | |
return $openApi; | |
} | |
} |
¡Ya está! Primero actualiza el documento JSON sin procesar para que podamos ver qué aspecto tiene. Déjame buscar "Portador". ¡Ya está! ¡Hemos modificado el aspecto del JSON!
¿Qué opina Swagger de esta nueva configuración? Actualiza y pulsa "Autorizar". Genial: access_token
, http, Bearer
. Ve a robar un token de API... pégalo sin decir Bearer
primero y dale a "Autorizar". Probemos la misma ruta. Uy, tengo que darle a "Probar". Y... ¡precioso! Mira esa cabecera Authorization
! Nos ha pasado Bearer
. Misión cumplida.
Por cierto, podrías pensar, dado que estamos anulando por completo la configuración deaccess_token
, que podríamos simplemente eliminarla de api_platform.yaml
. Por desgracia, por razones sutiles que tienen que ver con cómo se genera la documentación de seguridad, seguimos necesitándola. Pero diré# overridden in OpenApiFactoryDecorator
:
api_platform: | |
... lines 2 - 7 | |
swagger: | |
api_keys: | |
# overridden in OpenApiFactoryDecorator | |
access_token: | |
... lines 12 - 19 |
Esto era sólo un ejemplo de cómo podrías ampliar tu documento de especificaciones de la API Abierta. Pero si alguna vez necesitas modificar algo más, ahora ya sabes cómo.
A continuación, hablemos de los ámbitos.
Hey @Oleh-K!
Sorry for the slow reply! Hmm. I wonder: did you add the access_token
config to api_platform.yaml
? I actually think your solution is superior. My guess (I could be wrong) is that you don't have this config. And so, this line:
$securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject();
is using the new \ArrayObject
part. But in my code, because I have the config, the $openApi->getComponents()->getSecuritySchemes()
is returning an ArrayObject
. The difference is that, in my case, I'm them "mutating" this existing object. But in your case, your "mutation" the new \ArrayObject()
, which is then never actually set into the OpenApi
object. Basically, my code was short-sighted and would ONLY work in the case where $openApi->getComponents()->getSecuritySchemes()
DOES return an ArrayObject
.
Am I correct? Does $openApi->getComponents()->getSecuritySchemes()
return an ArrayObject
or is it null
? Anyway, your solution is superior, I believe, as it will work in call cases.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"api-platform/core": "^3.0", // v3.1.2
"doctrine/annotations": "^2.0", // 2.0.1
"doctrine/doctrine-bundle": "^2.8", // 2.8.3
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.14", // 2.14.1
"nelmio/cors-bundle": "^2.2", // 2.2.0
"nesbot/carbon": "^2.64", // 2.66.0
"phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
"phpstan/phpdoc-parser": "^1.15", // 1.16.1
"symfony/asset": "6.2.*", // v6.2.5
"symfony/console": "6.2.*", // v6.2.5
"symfony/dotenv": "6.2.*", // v6.2.5
"symfony/expression-language": "6.2.*", // v6.2.5
"symfony/flex": "^2", // v2.2.4
"symfony/framework-bundle": "6.2.*", // v6.2.5
"symfony/property-access": "6.2.*", // v6.2.5
"symfony/property-info": "6.2.*", // v6.2.5
"symfony/runtime": "6.2.*", // v6.2.5
"symfony/security-bundle": "6.2.*", // v6.2.6
"symfony/serializer": "6.2.*", // v6.2.5
"symfony/twig-bundle": "6.2.*", // v6.2.5
"symfony/ux-react": "^2.6", // v2.7.1
"symfony/ux-vue": "^2.7", // v2.7.1
"symfony/validator": "6.2.*", // v6.2.5
"symfony/webpack-encore-bundle": "^1.16", // v1.16.1
"symfony/yaml": "6.2.*" // v6.2.5
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
"mtdowling/jmespath.php": "^2.6", // 2.6.1
"phpunit/phpunit": "^9.5", // 9.6.3
"symfony/browser-kit": "6.2.*", // v6.2.5
"symfony/css-selector": "6.2.*", // v6.2.5
"symfony/debug-bundle": "6.2.*", // v6.2.5
"symfony/maker-bundle": "^1.48", // v1.48.0
"symfony/monolog-bundle": "^3.0", // v3.8.0
"symfony/phpunit-bridge": "^6.2", // v6.2.5
"symfony/stopwatch": "6.2.*", // v6.2.5
"symfony/web-profiler-bundle": "6.2.*", // v6.2.5
"zenstruck/browser": "^1.2", // v1.2.0
"zenstruck/foundry": "^1.26" // v1.28.0
}
}
Maybe something has been changed in API Platform with updates, since this video was recorded. I've tried, and
securitySchemes
hasn't been overridden.After some workaround I've stopped on this solution: