creearea unui REST API în Symfony 4.2 cu ajutorul FOSRestBundle + autentificare OAuth2

În acest tutorial vom încerca să creem o structură back-end pentru orice aplicație, ridicată pe Symfony 4 (momentan versiunea este 4.2), utilizând stilul de arhitectură API REST. Vom folosi o bază de date „FOSRestBundle”, vom implementa metodele „GET”, „POST”, „PUT”, „DELETE”, pentru a crea, modifica, șterge, afișa lista de produse. În plus, vom adăuga autentificarea OAuth2 cu FOSOAuthServerBundle.

Inițializare și crearea Rest API

Versiune php la moment e 7.2, deci folosim cel puțin aceasta versiune sau mai mare, în plus trebuie sa avem instalat package managerul composer. Deci, mai întâi de toate, inițializăm un proiect nou

composer create-project symfony/skeleton proiect-rest-api.local

astfel, composerul ne va crea mapa proiect cu numele proiect-rest-api.local în care va descarca skeleton-ul symfony, adica fisierele de bază ale acestui framework.

dupa cum ne propune cmd-ul, adaugăm pachetul server
în developer-mode, dar nu inainte de a intra în mapa nou creată. adică rulăm urmatoarele 2 comenzi

cd proiect-rest-api.local
composer require server --dev

Acum, să instalăm pachetele necesare, rulăm pe rând urmatoarele pachete

composer require friendsofsymfony/rest-bundle

În caz de vom primi eroare dupa instalarea acestui pachet, urmatorul pachet va repara eroarea dată

pachetul 2

composer require jms/serializer-bundle

apropo, daca vă întreabă Do you want to execute this recipe? răspuneți cu „y

Mergem la pachetul numărul 3

composer require sensio/framework-extra-bundle

Pachetul 4

composer require symfony/validator

Pachetul 5

composer require symfony/form

Pachetul 6

composer require symfony/orm-pack

Ultimul pachet ne va adăga Doctrine ORM care ne va da posibilitate și ușurință în lucrul cu Baza de date. Pentru a configura conexiunea cu baza, deschidem fișierul .env și modificam urmatoarea linie de cod

DATABASE_URL=mysql://db_user:db_pass@127.0.0.1:3306/db_name

cel puțin la mine va fi

DATABASE_URL=mysql://root:@127.0.0.1:3306/proiect_rest_api

După ce terminăm cu instalarea, să creăm o entitate de resurse pentru testare. Creați un fișier nou numit Product.php în dosarul src/Entity

<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product {
  /**
   * @ORM\Column(type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;
  /**
   * @ORM\Column(type="string", length=100)
   * @Assert\NotBlank()
   *
   */
  private $name;
  /**
   * @ORM\Column(type="text")
   * @Assert\NotBlank()
   */
  private $description;
  /**
   * @return mixed
   */
  public function getId()
  {
    return $this->id;
  }
  /**
   * @param mixed $id
   */
  public function setId($id)
  {
    $this->id = $id;
  }
  /**
   * @return mixed
   */
  public function getName()
  {
    return $this->name;
  }
  /**
   * @param mixed $name
   */
  public function setName($name)
  {
    $this->name = $name;
  }
  /**
   * @return mixed
   */
  public function getDescription()
  {
    return $this->description;
  }
  /**
   * @param mixed $description
   */
  public function setDescription($description)
  {
    $this->description = $description;
  }
}

dupa care rulăm urmatoarea comanda, pentru a crea schema bazei de date

php bin/console doctrine:schema:create

Acum trebuie să creăm un formular ProductType pentru entitatea Product în directorului src/Form pentru a gestiona și valida solicitările utilizatoror de a adăuga/modifica un product nou (daca dosarul Form nu exista, adăugați-l):

<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Entity\Product;
class ProductType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
      ->add('name')
      ->add('description')
      ->add('save', SubmitType::class)
    ;
  }
  public function configureOptions(OptionsResolver $resolver)
  {
    $resolver->setDefaults(array(
      'data_class' => Product::class,
      'csrf_protection' => false
    ));
  }
}

La pasul următor, setăm următoarea configurație pentru pachetul fos_rest, pentru asta, deschidem fișierul proiect-rest-api.local/config/packages/fos_rest.yaml

# Read the documentation: https://symfony.com/doc/master/bundles/FOSRestBundle/index.html
fos_rest:
    routing_loader:
        default_format: json
        include_format: false
    body_listener: true
    format_listener:
          rules:
              - { path: '^/', priorities: ['json'], fallback_format: json, prefer_extension: false }
    param_fetcher_listener: true
    access_denied_listener:
        json: true
    view:
        view_response_listener: 'force'
        formats:
            json: true

apoi adăugam urmatorul cod la sfârșitul fișierului config/services.yaml

    sensio_framework_extra.view.listener:
        alias: Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener

Acum este timpul să creem un controler în care să adăugăm metodele pentru a crea un produs nou, pentru a obține lista tuturor produselor, pentru a modifica și pentru a țterge produsul. După cum înțelegeți, noul controller ProductController.php ar trebui să-l creem în interiorul folderului src/Controller.

<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use App\Entity\Product;
use App\Form\ProductType;
/**
 * Product controller.
 * @Route("/api", name="api_")
 */
class ProductController extends FOSRestController
{
  /**
   * Lists all Products.
   * @Rest\Get("/products")
   *
   * @return Response
   */
  public function getProductAction()
  {
    $repository = $this->getDoctrine()->getRepository(Product::class);
    $products = $repository->findall();
    return $this->handleView($this->view($products));
  }
  /**
   * Create Product.
   * @Rest\Post("/product")
   *
   * @return Response
   */
  public function postProductAction(Request $request)
  {
    $product = new Product();
    $form = $this->createForm(ProductType::class, $product);
    $data = json_decode($request->getContent(), true);
    $form->submit($data);
    if ($form->isSubmitted() && $form->isValid()) {
      $em = $this->getDoctrine()->getManager();
      $em->persist($product);
      $em->flush();
      return $this->handleView($this->view(['status' => 'ok'], Response::HTTP_CREATED));
    }
    return $this->handleView($this->view($form->getErrors()));
  }
}

Aici am definit două route-uri, GET /api/products care ne va întoarce lista tuturor produselor, apoi POST /api/product care va executa validarea datelor cu ajutorul unui formular Symfony și va crea o nouă resursă de product, dacă datele vor fi valide.

Dupa ce am terminat cu structura de bază, să verificăm cum funcționează. În primul rând, trebuie sa pornim serverul cu ajutorul urmatoarei comenzi

php bin/console server:run

Adresa primită in cmd http://127.0.0.1:8000 va fi adresa serverului. Încercați să creați câteva resurse noi prin trimiterea datelor în format JSON. Apropo, pentru lucrul cu api, eu folosesc aplicația Postman, ceea ce vă recomand și vouă

Apoi, produsele create le putem primi înapoi folosind GET /api/products

Autentificarea OAuth2

Există mai multe metode de autorizare pentru Rest API-uri, iar OAuth2 este unul dintre cele mai populare. Permite identificarea datelor auth de la un aplicații externe, cum ar fi Google sau Facebook. În aplicația noastră, vom folosi FOSUserBundle ca furnizor de utilizatori (user provider), vom instala acest pachet, apoi vom instala FOSOAuthServerBundle

composer require friendsofsymfony/user-bundle
composer require friendsofsymfony/oauth-server-bundle

Dupa primul pachet poate aparea urmatoarea eroare

The child node „db_driver” at path „fos_user” must be configured.

Pentru asta, adăugam un fișier nou /config/packages/fos_user.yaml cu urmatorul cod

fos_user:
    db_driver: orm
    firewall_name: main
    user_class: App\Entity\User
    from_email:
      address:        resetting@example.com
      sender_name:    Demo Resetting

apoi adăugăm fișierul /config/packages/fos_auth_server.yaml cu umatorul cod

fos_oauth_server:
    db_driver: orm
    client_class:        App\Entity\Client
    access_token_class:  App\Entity\AccessToken
    refresh_token_class: App\Entity\RefreshToken
    auth_code_class:     App\Entity\AuthCode
    service:
        user_provider: fos_user.user_provider.username
        options:
            access_token_lifetime: 86400
            refresh_token_lifetime: 1209600
            auth_code_lifetime: 30

apoi deschidem config/routes.yaml și facem următoarea modificare

#index:
#    path: /
#    controller: App\Controller\DefaultController::index
fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

fos_oauth_server_authorize:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"

Creați o clasă de utilizatori pentru FOSUserBundle, src/Entity/User.php

<?php
namespace App\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
  public function __construct()
  {
    parent::__construct();
  }
}

Următorul cod, va trebui adăugat în fișierul config/packages/security.yaml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false
        oauth_authorize:
            pattern:    ^/oauth/v2/auth
            form_login:
                provider: fos_userbundle
                check_path: /oauth/v2/auth_login_check
                login_path: /oauth/v2/auth_login
                use_referer: true
        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true
            anonymous:  false
        main:
         pattern: ^/
         form_login:
             provider: fos_userbundle
             csrf_token_generator: security.csrf.token_manager
         anonymous: true

    access_control:
        - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
        - { path: ^/createClient, roles: [ IS_AUTHENTICATED_ANONYMOUSLY ] }

Astfel am făcut ca routerele /api să fie disponibile doar pentru utilizatorii autentificați prin adăugarea lor în blocul access_control.

Pentru lucrul cu utilizatorii, trebuie de mai adăugat pachetul pentru lucrul cu mailuril, pentru asta rulăm

composer require swiftmailer-bundle

dacă dupa instalare, primit eroarea

The service „fos_user.listener.flash” has a dependency on a non-existent service „translator”.

nu avem decât să mai instalam un pachet de translation

composer require symfony/translation

Dacă și în urma acestei instalări primim eroarea

The service „fos_user.resetting.controller” has a dependency on a non-existent service „templating”.

deschidem fișierul /config/packages/framework.yaml și adăugam la sfârșit

templating:
        engines: ['twig']

Adăugăm fișierul src/Entity/AccessToken.php

<?php
namespace App\Entity;
use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class AccessToken extends BaseAccessToken
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
  /**
   * @ORM\ManyToOne(targetEntity="Client")
   * @ORM\JoinColumn(nullable=false)
   */
  protected $client;
  /**
   * @ORM\ManyToOne(targetEntity="App\Entity\User")
   */
  protected $user;
}

src/Entity/AuthCode.php

<?php
namespace App\Entity;
use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class AuthCode extends BaseAuthCode
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
  /**
   * @ORM\ManyToOne(targetEntity="Client")
   * @ORM\JoinColumn(nullable=false)
   */
  protected $client;
  /**
   * @ORM\ManyToOne(targetEntity="App\Entity\User")
   */
  protected $user;
}

src/Entity/Client.php

<?php
namespace App\Entity;
use FOS\OAuthServerBundle\Entity\Client as BaseClient;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class Client extends BaseClient
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
  public function __construct()
  {
    parent::__construct();
  }
}

apoi src/Entity/RefreshToken.php

<?php
namespace App\Entity;
use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;
use Doctrine\ORM\Mapping as ORM;
/**
 * @ORM\Entity
 */
class RefreshToken extends BaseRefreshToken
{
  /**
   * @ORM\Id
   * @ORM\Column(type="integer")
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
  /**
   * @ORM\ManyToOne(targetEntity="Client")
   * @ORM\JoinColumn(nullable=false)
   */
  protected $client;
  /**
   * @ORM\ManyToOne(targetEntity="App\Entity\User")
   */
  protected $user;
}

Mai rulăm odata crearea schemei, pentru a crea ultimele entități create. Pnetru asta, mai intâi de toate recomand sa stergeți totate tabelele din baza de date, pentru ca rularea sa pargurga fara greseli.

php bin/console doctrine:schema:create

in resultat, în baza de date trebuie sa primim urmatoarele tabele

apoi, mai rulăm server:run în caz că a fost oprit

php bin/console server:run

acum dacă trimitem o interogare GET la http://127.0.0.1:8000/api/products vom primi urmatoarea eroare

După ce terminăm configurația pachetelor, trebuie să creăm un client OAuth și un utilizator pentru a genera token-uri de acces. Pentru a face acest lucru, să adăugăm un controler SecurityController.php

<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use FOS\RestBundle\Controller\Annotations as FOSRest;
use Symfony\Component\HttpFoundation\Response;
use FOS\OAuthServerBundle\Model\ClientManagerInterface;
class SecurityController extends FOSRestController
{
  private $client_manager;
  public function __construct(ClientManagerInterface $client_manager)
  {
    $this->client_manager = $client_manager;
  }
  /**
   * Create Client.
   * @FOSRest\Post("/createClient")
   *
   * @return Response
   */
  public function AuthenticationAction(Request $request)
  {
    $data = json_decode($request->getContent(), true);
    if (empty($data['redirect-uri']) || empty($data['grant-type'])) {
      return $this->handleView($this->view($data));
    }
    $clientManager = $this->client_manager;
    $client = $clientManager->createClient();
    $client->setRedirectUris([$data['redirect-uri']]);
    $client->setAllowedGrantTypes([$data['grant-type']]);
    $clientManager->updateClient($client);
    $rows = [
      'client_id' => $client->getPublicId(), 'client_secret' => $client->getSecret()
    ];
    return $this->handleView($this->view($rows));
  }
}

Pentru crearea unui client nou accesăm POST http://127.0.0.1:8000/createClient , unde trebuie să specificăm URI-urile de redirecționare pe care dorim să le utilizăm și tipurile de permisiuni ale acestui client.

Pentru a crea un user, putem accesa consola cu

php bin/console fos:user:create test_user

Acum putem primi un access token, efectuând o interogare POST la OAuth2 bundle http://127.0.0.1:8000/oauth/v2/token .

Dacă totul merge bine, ar trebui să primim tokenul de acces pe care ar trebui să-l trimitem la request-urile API în header, după cum urmează

Authorization: Bearer NjE0ZTYyYzUzYzU4ZjU1MzE1NzAyYzIyZmE1MTk2ZjQ4ODNjNzYzYWVjZDE0OWI4YjlkMTA5NmU5YzA3ZDgyZA

Spre exemplu, deja, pentru a crea un produs, trebue ca în request header sa trimitem Autentificația astfel

Asta-i tot pentru moment, implementarea platformei API a fost finalizată. Am adăugat două puncte finale pentru a crea și afișa lista de resurse și a adăugat o autorizație OAuth2.

Pentru mai multe detalii, am creat un repozitorii pe hithub

https://github.com/marikv/symfony-rest-api-oauth2

Succese! 😉

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>