Czego nauczę się w tym poradniku?
- Jak utworzyć mechanizm uwierzytelniający w RESTful API
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 przedstawione zostały m.in. zagadnienia związane z tworzeniem formularza dla encji, i dodawaniem do niego mechanizmu walidacji. Ponadto przedstawiony został koncept kontrolera stworzonego we wzorcu RESTful API.
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 uwierzytelniającego użytkownika w RESTful API, który pozwoli na zabezpieczenie ścieżek przed nieautoryzowanym dostępnem z zewnątrz. W ramach wspomnianego procesu, opisane zostaną czynności związane z tworzeniem encji użytkownika, dostawcy użytkowników, czyli usługi odpowiedzialnej za zwrócenie obiektu użytkownika dla podanej nazwy oraz konfigurowaniem zapory sieciowej, w celu osiągnięcia żądanych efektów.
Przykłady zastosowania funkcjonalności utworzonych w tym poradniku znajdują się na jego końcu.
Jak utworzyć mechanizm uwierzytelniający w RESTful API?
Poprzednio aplikacja serwerowa została rozszerzona o funkcjonalność RESTful API, jednocześnie umożliwiając tworzenie i usuwanie encji urządzeń oraz pozwalając na wyświetlanie szczegółów dotyczących poszczególnych urządzeń. Na tę chwilę interfejs dostępny jest bez konieczności autoryzowania się w aplikacji, a więc zasoby mogą zostać wyświetlone przez osobę z zewnątrz, co nie koniecznie może być pożądanym rozwiązaniem.
Mechanizm uwierzytelniający, który zostanie utworzony w tym poradniku, pozwoli na autoryzację w aplikacji serwerowej za pomocą klucza api key, który przekazywany będzie w nagłówku żądania. Następnie klucz ten zostanie wykorzystany w celu odnalezienia korespondującego obiektu encji użytkownika. Jeżeli takowa będzie istnieć, dostęp do zasobów zostanie umożliwiony.
Klasa encji użytkownika
Nim użytkownik będzie mógł być autoryzowany, konieczne będzie utworzenie klasy encji, aby dane identyfikujące dla danego użytkownika mogły zostać zapisane w bazie danych. Wspomniana encja posiadać będzie trzy właściwości: User::$id
, User::$name
oraz User::$apiKey
.
Warstwa abstrakcyjna
Komponent Security domyślnie dostarczony z frameworkiem Symfony posiada już prosty interfejs dla obiektu użytkownika, jednak brakuje w nim deklaracji dla metod User::setUsername(string $username)
oraz User::setApiKey(string $apiKey)
, które potrzebne będą aby umożliwić ustawienie adekwatnych własności.
Należy rozpocząć od utworzenia interfejsu UserInterface
, rozszerzającego bazowy interfejs komponentu Security, w katalogu src/Ptrio/MessageBundle/Model
.
<?php
// src/Ptrio/MessageBundle/Model/UserInterface.php
namespace App\Ptrio\MessageBundle\Model;
use Symfony\Component\Security\Core\User\UserInterface as BaseUserInterface;
interface UserInterface extends BaseUserInterface
{
/**
* @param string $username
*/
public function setUsername(string $username);
/**
* @param string $apiKey
*/
public function setApiKey(string $apiKey);
}
Klasa bazowa zawierać będzie deklaracje definiujące jak obsługiwane mają być metody zawarte w interfejsie UserInterface
.
W katalogu src/Ptrio/MessageBundle/Model
należy utworzyć plik o nazwie User.php
.
<?php
// src/Ptrio/MessageBundle/Model/User.php
namespace App\Ptrio\MessageBundle\Model;
abstract class User implements UserInterface
{
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $username;
/**
* @var string
*/
protected $apiKey;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getUsername(): string
{
return $this->username;
}
/**
* @param string $username
*/
public function setUsername(string $username)
{
$this->username = $username;
}
/**
* @return string
*/
public function getApiKey(): string
{
return $this->apiKey;
}
/**
* @param string $apiKey
*/
public function setApiKey(string $apiKey)
{
$this->apiKey = $apiKey;
}
/**
* @return array
*/
public function getRoles(): array
{
return ['ROLE_USER'];
}
/**
* @return void
*/
public function getPassword(): void
{
}
/**
* @return void
*/
public function getSalt(): void
{
}
/**
* @return void
*/
public function eraseCredentials(): void
{
}
}
Metoda User::getRoles()
zwraca tablicę zawierającą role jakie będzie posiadał obiekt użytkownika. W związku z tym, że mechanizm autoryzacji na tę chwilę nie umożliwia zaawansowanego zarządzania uprawnieniami, jedynym elementem zawartym we wspomnianej tablicy jest ROLE_USER
.
Metody User::getPassword()
i User::getSalt()
nie zwracają żadnych wartości, ponieważ w opisywanym mechanizmie uwierzytelniającym nie będzie możliwa autoryzacja za pomocą formularza.
W przypadku metody User::eraseCredentials()
, nie potrzebujemy funkcjonalności umożliwiającej wymazanie danych autoryzujących, dlatego wnętrze metody pozostaje puste.
Klasa właściwa
W klasie właściwej dla encji użytkownika znajdować się będą informacje dotyczące mapowania ORM.
Do katalogu src/Ptrio/MessageBundle/Entity
należy dodać plik User.php
.
<?php
// src/Ptrio/MessageBundle/Entity/User.php
namespace App\Ptrio\MessageBundle\Entity;
use App\Ptrio\MessageBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Class User
* @package App\Ptrio\MessageBundle\Entity
*
* @ORM\Entity
*/
class User extends BaseUser
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", unique=true)
* @Assert\NotBlank()
*/
protected $username;
/**
* @ORM\Column(type="string", unique=true)
* @Assert\NotBlank()
*/
protected $apiKey;
}
Następnie należy wykonać migracje bazy danych, aby zaktualizować strukturę tabel.
Klasa managera użytkowników
W celu umożliwienia zarządzania obiektami użytkowników, potrzebna będzie usługa managera użytkowników. Zasada tworzenia usługi managera użytkowników jest taka sama jak w przypadku usług managerów wiadomości i urządzeń, które utworzone zostały w poprzednich artykułach z serii Web Development z Symfony.
Warstwa abstrakcyjna
Pierwszym krokiem będzie utworzenie warstwy abstrakcyjnej.
W katalogu src/Ptrio/MessageBundle/Model
należy utworzyć plik interfejsu o nazwie UserManagerInterface.php
.
<?php
// src/Ptrio/MessageBundle/Model/UserManagerInterface.php
namespace App\Ptrio\MessageBundle\Model;
interface UserManagerInterface
{
/**
* @return UserInterface
*/
public function createUser();
/**
* @param UserInterface $user
*/
public function updateUser(UserInterface $user);
/**
* @param UserInterface $user
*/
public function removeUser(UserInterface $user);
/**
* @param array $criteria
* @return null|UserInterface
*/
public function findUserBy(array $criteria): ?UserInterface;
/**
* @param string $username
* @return null|UserInterface
*/
public function findUserByUsername(string $username): ?UserInterface;
/**
* @param string $apiKey
* @return null|UserInterface
*/
public function findUserByApiKey(string $apiKey): ?UserInterface;
/**
* @return string
*/
public function getClass(): string;
}
W następnym kroku, plik klasy bazowej managera użytkowników UserManager.php
powinien zostać dodany do tego samego katalogu.
<?php
// src/Ptrio/MessageBundle/Model/UserManager.php
namespace App\Ptrio\MessageBundle\Model;
abstract class UserManager implements UserManagerInterface
{
/**
* {@inheritdoc}
*/
public function createUser()
{
$class = $this->getClass();
return new $class;
}
/**
* {@inheritdoc}
*/
public function findUserByUsername(string $username): ?UserInterface
{
return $this->findUserBy(['username' => $username]);
}
/**
* {@inheritdoc}
*/
public function findUserByApiKey(string $apiKey): ?UserInterface
{
return $this->findUserBy(['apiKey' => $apiKey]);
}
}
Klasa właściwa
Plik klasy właściwej powinien znajdować się w katalogu src/Ptrio/MessageBundle/Doctrine
.
<?php
// src/Ptrio/MessageBundle/Doctrine/UserManager.php
namespace App\Ptrio\MessageBundle\Doctrine;
use App\Ptrio\MessageBundle\Model\UserManager as BaseUserManager;
use Doctrine\Common\Persistence\ObjectManager;
use App\Ptrio\MessageBundle\Model\UserInterface;
class UserManager extends BaseUserManager
{
/**
* @var ObjectManager
*/
private $objectManager;
/**
* @var \Doctrine\Common\Persistence\ObjectRepository
*/
private $repository;
/**
* @var string
*/
private $class;
/**
* UserManager constructor.
* @param ObjectManager $objectManager
* @param string $class
*/
public function __construct(
ObjectManager $objectManager,
string $class
)
{
$this->objectManager = $objectManager;
$this->repository = $objectManager->getRepository($class);
$metadata = $objectManager->getClassMetadata($class);
$this->class = $metadata->getName();
}
/**
* {@inheritdoc}
*/
public function updateUser(UserInterface $user)
{
$this->objectManager->persist($user);
$this->objectManager->flush();
}
/**
* {@inheritdoc}
*/
public function removeUser(UserInterface $user)
{
$this->objectManager->remove($user);
$this->objectManager->flush();
}
/**
* {@inheritdoc}
*/
public function findUserBy(array $criteria): ?UserInterface
{
/** @var UserInterface|null $user */
$user = $this->repository->findOneBy($criteria);
return $user;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return $this->class;
}
}
Konfiguracja usług
Deklarację dla usługi managera ptrio_message.user_manager
należy dodać do pliku services.yaml
.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.user_manager:
class: 'App\Ptrio\MessageBundle\Doctrine\UserManager'
arguments:
- '@doctrine.orm.entity_manager'
- 'App\Ptrio\MessageBundle\Entity\User'
Usługa dostawcy użytkowników
Usługa dostawcy użytkowników, jak sama nazwa wskazuje, odpowiadać będzie za dostarczenie obiektu użytkownika mechanizmowi uwierzytelniającemu. Posiadać będzie ona definicje pozwalające na odnalezienie obiektu użytkownika za pomocą przekazanego klucza api key.
Nim przejdziemy dalej, oprócz samego komponentu Security, konieczna będzie instalacja modułu SecurityBundle
, aby aplikacja mogła korzystać z usług zapory sieciowej.
$ composer require symfony/security-bundle
Warstwa abstrakcyjna
Komponent Security dostarczony jest z bazowym interfejsem UserProviderInterface
. W celu lepszej organizacji tworzonego kodu, wskazane jest utworzenie deklaracji dla metody odpowiedzialnej za odnalezienie obiektu użytkownika i rozszerzenie wspomnianego interfejsu.
Należy utworzyć plik UserProviderInterface.php
w katalogu src/Ptrio/MessageBundle/Security/Core/User
.
<?php
// src/Ptrio/MessageBundle/Security/Core/User/UserProviderInterface.php
namespace App\Ptrio\MessageBundle\Security\Core\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface as BaseUserProviderInterface;
interface UserProviderInterface extends BaseUserProviderInterface
{
/**
* @param string $apiKey
* @return null|UserInterface
*/
public function findUser(string $apiKey): ?UserInterface;
}
Klasa właściwa
Klasa właściwa ApiKeyUserProvider
definiować będzie jak metody zadeklarowane w interfejsie UserProviderInterface
mają być obsługiwane.
<?php
// src/Ptrio/MessageBundle/Security/Core/User/ApiKeyUserProvider.php
namespace App\Ptrio\MessageBundle\Security\Core\User;
use App\Ptrio\MessageBundle\Model\UserManagerInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* @var UserManagerInterface
*/
private $userManager;
/**
* EntityUserProvider constructor.
* @param UserManagerInterface $userManager
*/
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
/**
* {@inheritdoc}
*/
public function loadUserByUsername($username)
{
$user = $this->findUser($username);
if ($user instanceof UserInterface) {
return $user;
}
throw new UsernameNotFoundException('User cannot be found.');
}
/**
* {@inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if ($user instanceof UserInterface) {
return $this->loadUserByUsername($user->getUsername());
}
throw new UnsupportedUserException('Unsupported user instance.');
}
/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return $this->userManager->getClass() === $class;
}
/**
* {@inheritdoc}
*/
public function findUser(string $apiKey): ?UserInterface
{
return $this->userManager->findUserByApiKey($apiKey);
}
}
Omówmy teraz pokrótce poszczególne elementy klasy ApiKeyUserProvider
.
Obiekt użytkownika wyszukiwany jest za pomocą usługi managera użytkowników, której instancja przekazywana jest jako argument konstruktora.
Metoda ApiKeyUserProvider::loadUserByUsername($username)
odpowiedzialna jest za dostarczenie obiektu użytkownika. W przypadku gdy obiekt nie zostanie znaleziony, podniesiony zostanie wyjątek UsernameNotFoundException
z adekwatną informacją.
Metoda ApiKeyUserProvider::refreshUser(UserInterface $user)
odpowiada za odświeżenie obiektu użytkownika przy każdym żądaniu.
Metoda ApiKeyUserProvider::supportsClass($class)
wskazuje czy dana deklaracja dla obiektu użytkownika jest wspierana przez usługę dostawcy.
Konfiguracja usługi
Do pliku services.yaml
koniecznym jest dodanie definicji dla usługi dostawcy ptrio_message.user_provider.api_key
aby odpowiednie zależności zostały wstrzyknięte.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.user_provider.api_key:
class: 'App\Ptrio\MessageBundle\Security\Core\User\ApiKeyUserProvider'
arguments:
- '@ptrio_message.user_manager'
Do pliku config/packages/security.yaml
, w sekcji providers
należy dodać definicję dostawcy utworzonego w poprzednich krokach, aby usługa uwierzytelniająca mogła go odnaleźć.
# config/packages/security.yaml
security:
providers:
user_provider:
id: 'ptrio_message.user_provider.api_key'
Klasa obiektu uwierzytelniającego
Klasa obiektu uwierzytelniającego zawierać będzie logikę potrzebną do pobrania danych o kluczu api key z odpowiedniego nagłówka i pobranie obiektu encji użytkownika przy zastosowaniu usługi dostawcy użytkowników. Następnie, w zależności czy korespondujący użytkownik został odnaleziony lub nie, obiekt klasy uwierzytelniającej umożliwi dostęp do zasobów, bądź zwróci odpowiednią odpowiedź, wskazującą, że dostęp nie może zostać przyznany.
Klasa uwierzytelniająca rozszerzać będzie klasę bazową AbstractGuardAuthenticator
, będącą częścią komponentu Security Guard, w związku z czym, tworzenie warstwy abstrakcyjnej nie będzie konieczne. Komponent można zainstalować za pomocą narzędzia Composer.
$ composer require symfony/security-guard
Klasa właściwa
Plik klasy właściwej TokenAuthenticator.php
powinien zostać umieszczony w katalogu src/Ptrio/MessageBundle/Security/Guard
.
<?php
// src/Ptrio/MessageBundle/Security/Guard/TokenAuthenticator.php
namespace App\Ptrio\MessageBundle\Security\Guard;
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\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
/**
* {@inheritdoc}
*/
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get('X-AUTH-TOKEN'),
];
}
/**
* {@inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiKey = $credentials['token'];
if (null === $apiKey) {
return null;
}
return $userProvider->loadUserByUsername($apiKey);
}
/**
* {@inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
/**
* {@inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response($exception->getMessage(), Response::HTTP_FORBIDDEN);
}
/**
* {@inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new Response('Authentication is required to access this resource.', Response::HTTP_UNAUTHORIZED);
}
/**
* {@inheritdoc}
*/
public function supports(Request $request)
{
return $request->headers->has('X-AUTH-TOKEN');
}
/**
* {@inheritdoc}
*/
public function supportsRememberMe()
{
return false;
}
}
Metoda TokenAuthenticator::getCredentials(Request $request)
odpowiedzialna jest za pobranie nagłówka X-AUTH-TOKEN
z obiektu żądania i zwrócenie tablicy, która następnie przekazana będzie jako pierwszy argument metody TokenAuthenticator::getUser($credentials, UserProviderInterface $userProvider)
.
Informacja: Metoda
TokenAuthenticator::getCredentials(Request $request)
wywoływana jest przy każdym żądaniu.
Metoda TokenAuthenticator::getUser($credentials, UserProviderInterface $userProvider)
odpowiada za przekazanie danych autoryzujących znajdujących się w tablicy $credentials
do obiektu dostawcy użytkowników i za zwrócenie obiektu użytkownika, który z kolei dostarczany jest przez usługę dostawcy.
Metoda TokenAuthenticator::checkCredentials($credentials, UserInterface $user)
wywoływana jest jedynie gdy metoda TokenAuthenticator::getUser($credentials, UserProviderInterface $userProvider)
zwróci obiekt użytkownika typu UserInterface
. W związku z tym, że mechanizm uwierzytelniający autoryzuje użytkowników za pomocą klucza api key nie jest koniecznym umieszczanie dodatkowej logiki wewnątrz wspomnianej metody. Zwracana wartość to true
, wskazująca, że autoryzacja powiodła się.
Metoda TokenAuthenticator::onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
odpowiada za zwrócenie obiektu typu Response
lub kontynuowanie żądania (zwracana wartość to wtedy null
). Jest ona wywoływana po pomyślnej autoryzacji, czyli w momencie gdy metoda TokenAuthenticator::checkCredentials($credentials, UserInterface $user)
zwróci wartość true
. W tym przypadku żądanie chcemy kontynuować, więc wartość null
jest zwracana.
Metoda TokenAuthenticator::onAuthenticationFailure(Request $request, AuthenticationException $exception)
zawiera logikę, która zostanie wykonana w momencie, gdy autoryzacja nie powiedzie się. W tym przypadku zwracany jest obiekt typu Response
z kodem HTTP 403
.
Metoda TokenAuthenticator::start(Request $request, AuthenticationException $authException = null)
wywoływana jest w momencie gdy żądanie do źródła zabezpieczonego autoryzacją nie posiada nagłówka X-AUTH-TOKEN
. Podobnie jak w przypadku metody TokenAuthenticator::onAuthenticationFailure(Request $request, AuthenticationException $exception)
, do klienta zwracany jest obiekt Response
, z jedną różnicą - kod HTTP odpowiedzi to 401
.
Metoda TokenAuthenticator::supports(Request $request)
, wywoływana przy każdym żądaniu, zwraca wartość boolean
, na której podstawie algorytm może zdecydować czy autoryzacja powinna zostać wykorzystana przy danym żądaniu.
Metoda TokenAuthenticator::supportsRememberMe()
wskazuje czy funkcjonalność remember_me
powinna zostać włączona. W przypadku RESTful API wspomniania funkcjonalność nie będzie konieczna.
Konfiguracja zapory sieciowej
Aby algorytm uwierzytelniający klasy TokenAuthenticator
był stosowany do zabezpieczenia tworzonego RESTful API, należy skonfigurować zaporę sieciową w pliku config/packages/security.yaml
.
# config/packages/security.yaml
firewalls:
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- 'App\Ptrio\MessageBundle\Security\Guard\TokenAuthenticator'
Jak widać na powyższym przykładzie, nazwa zapory sieciowej to api
.
Parametr pattern
pozwala zdefiniować wzorzec, za pomocą wyrażeń regularnych, dla ścieżek, które mają zostać objęte autoryzacją. W tym przypadku będą to wszystkie żądania i odpowiedzi znajdujące się za endpointem API /api
.
Informacja: Wartość
stateless
powinna zostać ustawiona natrue
, ponieważ w RESTful API usługi nie powinny przechowywać informacji o obecnym stanie klienta API.
Klasa generatora tokenów
Aby usprawnić proces tworzenia kluczy api key, wskazane jest utworzenie osobnej usługi odpowiedzialnej za generowanie wspomnianych kluczy. Następnie usługa ta może zostać wykorzystana podczas tworzenia nowego obiektu użytkownika.
Warstwa abstrakcyjna
Aby uniknąć tworzenia sztywnych powiązań pomiędzy klasami, należy utworzyć abstrakcyjny typ (interfejs) TokenGeneratorInterface
w katalogu src/Ptrio/MessageBundle/Util
.
<?php
// src/Ptrio/MessageBundle/Util/TokenGeneratorInterface.php
namespace App\Ptrio\MessageBundle\Util;
interface TokenGeneratorInterface
{
/**
* @return string
*/
public function generateToken(): string;
}
Każda klasa właściwa generatora tokenów implementująca powyższy interfejs będzie musiała również zaimplementować metodę generateToken()
.
Klasa właściwa
Następnie plik klasy właściwej TokenGenerator.php
powinien zostać dodany do tego samego katalogu.
<?php
// src/Ptrio/MessageBundle/Util/TokenGenerator.php
namespace App\Ptrio\MessageBundle\Util;
class TokenGenerator implements TokenGeneratorInterface
{
/**
* {@inheritdoc}
*/
public function generateToken(): string
{
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
}
}
Jak widać na powyższym przykładzie, metoda TokenGenerator::generateToken()
korzysta w pierwszej kolejności z funkcji random_bytes
w celu utworzenia kryptograficznie bezpiecznego ciągu znaków o długości 32, który następnie kodowany jest za pomocą funkcji base64_encode
. W kolejnym kroku znaki +/
w uzyskanym stringu zamieniane są na -_
za pomocą funkcji strtr
. Na końcu funkcja rtrim
usuwa z ciągu znaków pozycje =
. Przyjęte rozwiązanie umożliwia utworzenia unikalnego klucza api key.
Informacja: Do generowania kluczy może zostać zastosowany dowolny inny algorytm.
Konfiguracja usługi
Definicję dla usługi generatora tokenów należy dodać do pliku services.yaml
.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.util.token_generator:
class: 'App\Ptrio\MessageBundle\Util\TokenGenerator'
Klasy komend do zarządzania użytkownikami
Na tym etapie wciąż nie istnieje implementacja pozwalająca na dodanie lub usunięcie użytkownika. Można to zmienić, tworząc komendy AddUserCommand
oraz RemoveUserCommand
. Proces tworzenia wspomnianych komend jest podobny do procesu przedstawionego w pierwszym poradniku z serii Web Development z Symfony, gdzie opisywana była implementacja usług komend AddDeviceCommand
i RemoveDeviceCommand
.
Klasa właściwa komendy odpowiedzialnej za dodawanie użytkowników
Komenda odpowiedzialna za dodawanie użytkowników przyjmować będzie nazwę użytkownika jako wymagany argument. Podczas tworzenia nowego obiektu użytkownika, zostanie wykorzystany obiekt typu TokenGeneratorInterface
do utworzenia klucza api key. Obiekt typu UserManagerInterface
wykorzystany będzie do sprawdzenia czy obiekt użytkownika o podanej nazwie istnieje oraz za zapisanie nowego obiektu w sytuacji gdy ten nie istnieje. W przypadku dodania nowego użytkownika, klucz api key zwracany jest do klienta.
Do katalogu src/Ptrio/MessageBundle/Command
należy dodać plik AddUserCommand
.
<?php
// src/Ptrio/MessageBundle/Command/AddUserCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\UserManagerInterface;
use App\Ptrio\MessageBundle\Util\TokenGeneratorInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AddUserCommand extends Command
{
/**
* @var string
*/
public static $defaultName = 'ptrio:message:add-user';
/**
* @var UserManagerInterface
*/
private $userManager;
/**
* @var TokenGeneratorInterface
*/
private $tokenGenerator;
/**
* AddUserCommand constructor.
* @param UserManagerInterface $userManager
* @param TokenGeneratorInterface $tokenGenerator
*/
public function __construct(
UserManagerInterface $userManager,
TokenGeneratorInterface $tokenGenerator
)
{
$this->userManager = $userManager;
$this->tokenGenerator = $tokenGenerator;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setDefinition([
new InputArgument('username', InputArgument::REQUIRED),
]);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$username = $input->getArgument('username');
if ($user = $this->userManager->findUserByUsername($username)) {
$output->writeln('User with a given username already exists.');
} else {
$user = $this->userManager->createUser();
$user->setUsername($username);
$apiKey = $this->tokenGenerator->generateToken();
$user->setApiKey($apiKey);
$this->userManager->updateUser($user);
$output->writeln('User created successfully with the following api key: ' . $apiKey);
}
}
}
Klasa właściwa komendy odpowiedzialnej za usuwanie użytkowników
Klasa komendy odpowiedzialnej za usuwanie użytkowników będzie również korzystać z obiektu typu UserManagerInterface
.
Do katalogu src/Ptrio/MessageBundle/Command
powinien zostać dodany plik AddUserCommand.php
.
<?php
// src/Ptrio/MessageBundle/Command/AddUserCommand.php
namespace App\Ptrio\MessageBundle\Command;
use App\Ptrio\MessageBundle\Model\UserManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RemoveUserCommand extends Command
{
/**
* @var string
*/
public static $defaultName = 'ptrio:message:remove-user';
/**
* @var UserManagerInterface
*/
private $userManager;
/**
* RemoveUserCommand constructor.
* @param UserManagerInterface $userManager
*/
public function __construct(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setDefinition([
new InputArgument('username', InputArgument::REQUIRED),
]);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$username = $input->getArgument('username');
if ($user = $this->userManager->findUserByUsername($username)) {
$this->userManager->removeUser($user);
$output->writeln('User removed successfully.');
} else {
$output->writeln('User with a given username does not exist.');
}
}
}
Konfiguracja usług
Usługi poprzednio utworzonych komend należy zdefiniować w pliku src/Ptrio/MessageBundle/Resources/config/services.yaml
.
# src/Ptrio/MessageBundle/Resources/config/services.yaml
ptrio_message.add_user_command:
class: 'App\Ptrio\MessageBundle\Command\AddUserCommand'
arguments:
- '@ptrio_message.user_manager'
- '@ptrio_message.util.token_generator'
tags:
- { name: console.command }
ptrio_message.remove_user_command:
class: 'App\Ptrio\MessageBundle\Command\RemoveUserCommand'
arguments:
- '@ptrio_message.user_manager'
tags:
- { name: console.command }
Przykłady
Poniżej znajdują się przykłady przedstawiające procesy tworzenia nowego użytkownika, usunięcia istniejącego oraz wykonywania zapytań do API zabezpieczonego mechanizmem uwierzytelniającym.
Dodawanie użytkowników
Nowy użytkownik może zostać dodany za pomocą komendy ptrio:message:add-user
.
$ php bin/console ptrio:message:add-user piotr42
Odpowiedź z serwera w przypadku, gdy proces tworzenia użytkownika zakończył się pomyślnie, znajduje się poniżej.
User created successfully with the following api key: yJsLCNsSc_gOeTsMVWlhF1pBuLV-Ic3KBbS_WnEqbRw
Usuwanie użytkowników
Istniejącego użytkownika usunąć można za pomocą komendy ptrio:message:remove-user
jako argument podając nazwę użytkownika.
$ php bin/console ptrio:message:remove-user piotr42
Odpowiedź z serwera w przypadku, gdy proces usuwania użytkownika zakończył się pomyślnie, znajduje się poniżej.
User removed successfully.
Testy mechanizu uwierzytelniającego
W pierwszej kolejności sprawdźmy co wydarzy się, gdy do API prześlemy żądanie o szczegóły urządzenia o nazwie iphone-piotr
bez podawania klucza api key w nagłówku X-AUTH-TOKEN
.
$ curl http://localhost/api/v1/devices/iphone-piotr
Serwer powinien zwrócić odpowiedź informującą, że autoryzacja jest wymagana aby wyświetlić zasób.
Authentication is required to access this resource.
Spróbujmy teraz przesłać żądanie po ten sam zasób, tym razem podając nieistniejący klucz api key.
$ curl -H 'X-AUTH-TOKEN: Non-existing-api-key' http://localhost/api/v1/devices/iphone-piotr
Rezultat znajduje się poniżej.
User cannot be found for a given api key.
Teraz czas na żądanie z istniejącym kluczem api key.
$ curl -H 'X-AUTH-TOKEN: MMYKUE63gCyFc9blOwcrdr0EP9ghb7yiWZ2lr4fRauM' http://localhost/api/v1/devices/iphone-piotr
W tym przypadku dostęp do zasobu powinien zostać udzielony i obiekt JSON zwrócony do klienta (domyślny format).
{"id":1,"name":"iphone-piotr","token":"d1KeQHgkIoo:APA91bGBG7vg9rfX0PFbtn..."
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.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit