Web Development z Symfony: Aplikacja serwerowa do wysyłki powiadomień push z FCM [część 6]

in polish •  7 years ago  (edited)

email-marketing-2362038_640.png

Czego nauczę się w tym poradniku?

  • Jak utworzyć mechanizm głosujący do sprawdzania uprawnień użytkowników

Wymagania

  • System z rodziny UNIX/Linux
  • Serwer Apache2 z PHP w wersji 7.1
  • Baza danych MySQL
  • Projekt Symfony pobrany stąd
  • Dowolny edytor tekstowy
  • Istniejący projekt w Firebase
  • Narzędzie composer

Poziom trudności

  • Średni

Treść poradnika

Poradnik jest kolejnym z serii Web Development, gdzie opisywane są procesy budowy aplikacji serwerowej w Symfony, zdolnej do wysyłki powiadomień push na urządzenia mobilne. W poprzednim artykule przedstawiony został proces tworzenia przyjaznej konfiguracji dla modułu z zastosowaniem komponentu Config. Dodatkowo opisane zostały czynności umożliwiające tworzenie luźnych powiązań pomiędzy klasami encji podczas mapowania relacyjnego ORM.

Przykładowy projekt wykorzystywany na potrzeby tego poradnika znajduje się tutaj. Jest to w zasadzie kod, który otrzymamy po ukończeniu poprzedniego kompendium. Wskazanym jest pobranie wspomnianego projektu z repozytorium github.

Jakie aspekty Web Development z Symfony zostaną przedstawione w tym poradniku?

  • Proces budowy mechanizmu głosującego, umożliwiającego weryfikację uprawnień użytkownika do wyświetlenia danego zasobu RESTful API.

Przykłady zastosowania funkcjonalności utworzonych w tym poradniku znajdują się na jego końcu.

Jak utworzyć mechanizm głosujący do sprawdzania uprawnień użytkowników?

System uwierzytelniający, opisany w piątej części z serii Web Development z Symfony, umożliwia dostęp do zasobów RESTful API tylko autoryzowanym użytkownikom. Oznacza to, że każdy, kto posiada klucz api key, powiązany z konkretną encją użytkownika, może komunikować się z aplikacją za pomocą protokołu HTTP.

W dzisiejszym artykule pójdziemy o krok dalej. Oprócz samego systemu uwierzytelniającego, do aplikacji zostanie dodany mechanizm głosujący, odpowiedzialny za weryfikację czy dany użytkownik posiada adekwatne uprawnienia umożliwiające dostęp do konkretnego zasobu.

Modyfikacja klasy encji urządzenia

Mechanizm głosujący będzie miał za zadanie decydować czy dany użytkownik może wchodzić w interakcję z obiektem encji konkretnego urządzenia. Logika odpowiedzialna za przebieg procesu weryfikacji będzie sprowadzać się do sprawdzenia czy dany obiekt urządzenia powiązany jest z obiektem autoryzowanego użytkownika. Aby było to jednak możliwe, w pierwszej kolejności należy utworzyć mapowanie ORM dla relacji urządzenie - użytkownik.

Warstwa abstrakcyjna

Nim przejdziemy do procesu ustawiania mapowania, rekomendowane jest dodanie deklaracji dla metod setUser(UserInterface $user) oraz getUser(), odpowiedzialnych kolejno za ustawienie własności użytkownika oraz jej zwrócenie, w obiekcie implementującym interfejs DeviceInterface.

    // src/Ptrio/MessageBundle/Model/DeviceInterface.php
   // other method declarations
    /**
     * @param UserInterface $user
     */
    public function setUser(UserInterface $user);
    /**
     * @return UserInterface|null
     */
    public function getUser(): ?UserInterface;

Następnie w klasie abstrakcyjnej Device należy dodać deklarację dla własności Device::$user.

    // src/Ptrio/MessageBundle/Model/Device
    // other properties
    /**
     * @var UserInterface|null
     */
    protected $user;

W następnym kroku koniecznym jest dodanie implementacji dla metod User::setUser(UserInterface $user) oraz User::getUser().

    /**
     * {@inheritdoc}
     */
    public function setUser(UserInterface $user)
    {
        $this->user = $user;
    }
    /**
     * {@inheritdoc}
     */
    public function getUser(): ?UserInterface
    {
        return $this->user;
    }
Klasa właściwa

Ze względu na fakt, że klasa mechanizmu głosującego będzie jedynie sprawdzać czy obiekt konkretnego urządzenia powiązany jest z użytkownikiem wykonującym żądanie, wystarczające będzie ustawienie mapowania ORM dla relacji jednokierunkowej wiele do jednego.

    // src/Ptrio/MessageBundle/Entity/Device.php
    // other properties
    /**
     * @ORM\ManyToOne(targetEntity="App\Ptrio\MessageBundle\Model\UserInterface")
     * @ORM\JoinColumn(name="user_id", referencedColumnName="id")
     */
    protected $user;
Aktualizacja konfiguracji modułu

Jak widać na powyższym przykładzie, wartość parametru targetEntity to App\Ptrio\MessageBundle\Model\UserInterface, czyli pełna nazwa interfejsu implementowanego przez klasę User.

Jak wspomniano wcześniej, w poprzednim poradniku, na przykładzie encji urządzenia, opisany został process tworzenia luźnych powiązań pomiędzy obiektami w mapowaniu relacyjnym ORM, które możliwe jest dzięki obiektowi typu ResolveTargetEntityListener, odpowiedzialnemu za zamianę abstrakcyjnego typu na klasę właściwą, podczas procesu inicjalizacji danego modułu aplikacji.

Aby relacja urządzenie - użytkownik mogła zostać nawiązana, konieczna jest aktualizacja plików znajdujących się w katalogu src/Ptrio/MessageBundle/DependencyInjection.

W pliku Configuration.php, do węzła classes należy dodać węzeł interface

->arrayNode('user')
                    ->children()
                        ->arrayNode('classes')
                            ->children()
                                ->scalarNode('model')->isRequired()->cannotBeEmpty()->end()
                                ->scalarNode('interface')->cannotBeEmpty()->defaultValue('App\Ptrio\MessageBundle\Model\UserInterface')->end() // add this line
                            ->end()
                        ->end()
                    ->end()
                ->end()

Jak widać na przykładzie powyżej, wartością domyślną dla węzła interface jest App\Ptrio\MessageBundle\Model\UserInterface.

Przykładowa tablica zwracana przez drzewo konfiguracyjne znajduje się poniżej.

[
    [
        // other array elements
        'user' => [
            'classes' => [
                'model' => 'App\Ptrio\MessageBundle\Entity\User',
                'interface' => 'App\Ptrio\MessageBundle\Model\UserInterface'
            ]
        ]
    ]
]

W kolejnym kroku, do metody PtrioMessageExtension::prepend(ContainerBuilder $container) należy dodać wpis informujący o tym, jaki interfejs ma zostać zastąpiony klasą właściwą.

        if (isset($bundles['DoctrineBundle'])) {
            $container
                ->loadFromExtension('doctrine', [
                    'orm' => [
                        'resolve_target_entities' => [
                            $config['device']['classes']['interface'] => $config['device']['classes']['model'],
                            $config['user']['classes']['interface'] => $config['user']['classes']['model'], // add this line
                        ],
                    ],
                ])
            ;
        }

Następnie należy wykonać migracje bazy danych, aby zaktualizować strukturę tabel.

Klasa mechanizmu głosującego

Rolą mechanizmu głosującego będzie zezwolenie na interakcję z obiektem encji urządzenia, w przypadku gdy ten powiązany jest z użytkownikiem wykonującym żądanie, lub zwrócenie wyjątku wraz z adekwatnym kodem HTTP, gdy dany użytkownik nie posiada odpowiednich uprawnień.

Informacja: Ze względu na fakt, że wraz z frameworkiem Symfony dostarczona jest również bazowa klasa Symfony\Component\Security\Core\Authorization\Voter\Voter dla obiektu głosującego, nie będzie konieczne tworzenie warstwy abstrakcyjnej.

Klasa właściwa

Należy rozpocząć, dodając plik DeviceVoter.php do katalogu src/Ptrio/MessageBundle/Security/Voter.

Klasa właściwa DeviceVoter musi implementować metody Voter::supports($attribute, $subject) oraz Voter::voteOnAttribute($attribute, $subject, TokenInterface $token).

<?php
// src/Ptrio/MessageBundle/Security/Voter/DeviceVoter.php
namespace App\Ptrio\MessageBundle\Security\Voter;
use App\Ptrio\MessageBundle\Model\DeviceInterface;
use App\Ptrio\MessageBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class DeviceVoter extends Voter
{
    /**
     * {@inheritdoc}
     */
    protected function supports($attribute, $subject)
    {
        return $subject instanceof DeviceInterface;
    }
    /**
     * {@inheritdoc}
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        if (!$user instanceof UserInterface) {
            return false;
        }        
        /** @var DeviceInterface $device */
        $device = $subject;        
        return $device->getUser() === $user;
    }
}

Omówmy teraz po krótce co dzieje się wewnątrz klasy DeviceVoter.

Metoda DeviceVoter::supports($attribute, $subject) odpowiada za sprawdzenie czy dany atrybut i obiekt, do którego uprawnienia są weryfikowane, wspierane są przez mechanizm głosujący.

Informacja: W powyższym przykładzie argument $attribute nie jest wykorzystywany, ponieważ decyzja o możliwości interakcji z obiektem urządzenia podejmowana jest wyłącznie na podstawie istniejącej relacji urządzenie - użytkownik.

Metoda DeviceVoter::voteOnAttribute($attribute, $subject, TokenInterface $token) jest odpowiedzialna za sprawdzenie czy dostęp do danego zasobu może zostać umożliwiony. W tym przypadku weryfikowane jest czy dane urządzenie powiązane jest z użytkownikiem wykonującym żądanie. Metoda zwraca wartość typu boolean.

Konfiguracja usług

Aby usługa mechanizmu głosującego mogła zostać wykorzystana w projekcie, jej definicję należy dodać do pliku services.yaml.

    # src/Ptrio/MessageBundle/Resources/config/services.yaml
    ptrio_message.device_voter:
        class: 'App\Ptrio\MessageBundle\Security\Voter\DeviceVoter'
        tags:
            - { name: security.voter }

Usługa ptrio_message.device_voter powinna zostać oznaczona jako security.voter.

Aktualizacja klasy kontrolera urządzeń

Na tym etapie konieczna będzie modyfikacja klasy kontrolera urządzeń DeviceController, tak aby korzystała ona z mechanizmu głosującego. Zmiany powinny zostać wprowadzone w metodach DeviceController::getDeviceAction(string $deviceName), DeviceController::postDeviceAction(Request $request) oraz DeviceController::deleteDeviceAction(string $deviceName).

Wewnątrz metody DeviceController::getDeviceAction(string $deviceName) należy dodać odwołanie do metody ControllerTrait::denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.').

    /**
     * @param string $deviceName
     * @return Response
     */
    public function getDeviceAction(string $deviceName): Response
    {
        if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
            $this->denyAccessUnlessGranted(null, $device); // add this line
            $view = $this->view($device, 200);
        } else {
            $view = $this->view(null, 404);
        }
        return $this->handleView($view);
    }

Jak widać na powyższym przykładzie, metoda ControllerTrait::denyAccessUnlessGranted($attributes, $subject = null, string $message = 'Access Denied.') znajduje się w zasięgu klasy DeviceController. Spowodowane jest to tym, że klasa kontrolera urządzeń DeviceController rozszerza klasę bazową FOS\RestBundle\Controller\FOSRestController, która z kolei korzysta z cechy Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait.

W następnym kroku, wewnątrz metody DeviceController::postDeviceAction(Request $request) należy dodać logikę odpowiedzialną za przypisanie tworzonemu urządzeniu korespondującego obiektu użytkownika.

    /**
     * @param Request $request
     * @return Response
     */
    public function postDeviceAction(Request $request): Response
    {
        /** @var DeviceInterface $device */
        $device = $this->deviceManager->createDevice();
        $form = $this->formFactory->create(DeviceType::class, $device);
        $form->submit($request->request->all());
        if ($form->isSubmitted() && $form->isValid()) {
            $device = $form->getData();
            $device->setUser($this->getUser()); // add this line
            $this->deviceManager->updateDevice($device);
            $view = $this->view(null, 204);
        } else {
            $view = $this->view($form->getErrors(), 422);
        }
        return $this->handleView($view);
    }

W ostatnim kroku związanym z edycją kontrolera urządzeń, należy zaktualizować metodę DeviceController::deleteDeviceAction(string $deviceName).

    /**
     * @param string $deviceName
     * @return Response
     */
    public function deleteDeviceAction(string $deviceName): Response
    {
        if ($device = $this->deviceManager->findDeviceByName($deviceName)) {
            $this->denyAccessUnlessGranted(null, $device);
            $this->deviceManager->removeDevice($device);
            $view = $this->view(null, 204);
        } else {
            $view = $this->view(null, 404);
        }
        return $this->handleView($view);
    }
Modyfikacja klasy komendy AddDeviceCommand

Ze względu na to, iż tworzona aplikacja umożliwia również interakcję z obiektami urządzeń za pomocą interfejsu konsolowego, konieczne będzie zaktualizowanie klasy AddDeviceCommand, odpowiedzialnej za tworzenie nowych urządzeń, tak aby możliwe było powiązanie danego urządzenia z obiektem użytkownika.

Obiekt użytkownika powiązanego z urządzeniem będzie wyszukiwany w bazie danych za pomocą managera użytkowników na podstawie nazwy użytkownika przekazanej jako argument komendy ptrio:message:add-device.

Na początku należy dodać import interfejsu UserManagerInterface na górze klasy, pomiędzy operatorami namespace i class.

use App\Ptrio\MessageBundle\Model\UserManagerInterface;

Następnie koniecznym jest utworzenie własności AddDeviceCommand::$userManager, która przechowywać będzie obiekt managera użytkowników.

    /**
     * @var UserManagerInterface
     */
    private $userManager;

W kolejnym kroku powinien zostać zmodyfikowany konstruktor klasy.

    /**
     * AddDeviceCommand constructor.
     * @param DeviceManagerInterface $deviceManager
     * @param UserManagerInterface $userManager
     */
    public function __construct(
        DeviceManagerInterface $deviceManager,
        UserManagerInterface $userManager // add this line
    )
    {
        $this->deviceManager = $deviceManager;
        $this->userManager = $userManager; // add this line
        parent::__construct();
    }

Konieczne będzie również dodanie wymaganego argumentu username wewnątrz metody AddDeviceCommand::configure().

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setDefinition([
            new InputArgument('name', InputArgument::REQUIRED),
            new InputArgument('token', InputArgument::REQUIRED),
            new InputArgument('username', InputArgument::REQUIRED), // add this line
        ]);
    }

W ostatnim kroku, wewnątrz metody AddDeviceCommand::execute(InputInterface $input, OutputInterface $output) należy dodać logikę odpowiedzialną za odnalezienie obiektu użytkownika po nazwie i powiązanie go z obiektem urządzenia.

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input->getArgument('name');
        $token = $input->getArgument('token');
        $username = $input->getArgument('username');  // add this line
        if (null === $this->deviceManager->findDeviceByName($name)) {
            $device = $this->deviceManager->createDevice();
            $device->setName($name);
            $device->setToken($token);
            $device->setUser($this->userManager->findUserByUsername($username)); // add this line
            $this->deviceManager->updateDevice($device);
            $output->writeln('Device `'.$name.'` created!');
        } else {
            $output->writeln('Device with this name already exists!');
        }
    }
Aktualizacja konfiguracji usług

Należy zaktualizować definicję usługi ptrio_message.add_device_command dodając usługę ptrio_message.user_manager jako argument konstruktora.

    ptrio_message.add_device_command:
        class: 'App\Ptrio\MessageBundle\Command\AddDeviceCommand'
        arguments:
            - '@ptrio_message.device_manager'
            - '@ptrio_message.user_manager' // add this argument
        tags:
            - { name: console.command }
Aktualizacja konfiguracji modułu FOSRestBundle

Nim przejdziemy do przykładów, konieczne będzie jeszcze skonfigurowanie modułu FOSRestBundle tak aby wyjątki zwracane przez kontroler DeviceController serializowane były do formatu JSON/XML.

Poniższy wpis należy dodać do pliku config/packages/fos_rest.yaml.

    exception:
            enabled: true

Przykłady

Dodawanie nowego urządzenia
php bin/console ptrio:message:add-device ipad-piotr 'd1KeQHgkIoo:APA91b...' piotr42

Urządzenie można dodać także za pomocą protokołu HTTP.

curl -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' -H 'Content-Type: application/json' -X POST -d '{"name":"ipad-piotr","token":"d1KeQHgkIoo:APA91bGBG7vg9rfX0PFb.."}' http://localhost/api/v1/devices
Wyświetlanie szczegółów urządzenia
curl -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/ipad-piotr

W przypadku gdy mechanizm głosujący zezwoli na dostęp do zasobu, zawartość podobna do poniżej powinna zostać wyświetlona.

{"id":3,"name":"ipad-piotr","token":"d1KeQHgkIoo:APA91bGBG7vg9rfX0PFb...","user":{"id":1,"username":"piotr42","apiKey":"MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM","roles":["ROLE_USER"],"password":null,"salt":null}}

Spróbujmy teraz wyświetlić zasób, do którego nie mamy dostępu.

curl -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/redmi-ewa

Wiadomość zwrotna powinna zawierać informacje, wskazującą, że dostęp nie został udzielony.

{"code":403,"message":"Access Denied."}

Curriculum

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

I am robotchecker please back me upvote

Thanks, but I'll pass.

Congratulations! This post has been upvoted from the communal account, @minnowsupport, by piotr42 from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.

If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.