Każda osoba zaczynająca swoją przygodę z Symfony dochodzi do momentu, w którym potrzebuje stworzyć system logowania oraz zarządzania użytkownikami. Tutaj najczęściej pojawia się problem, ponieważ pisanie takiego systemu wydaje się czasochłonne i stosunkowo trudne. Dlatego wielu programistów wybiera prostszą drogę instalacji gotowego pakietu, który załatwi wszystkie formalności. Przeważnie wybór pada na najpopularniejszy pakiet FOS\UserBundle. Jest to kombajn do zarządzania użytkownikami. No właśnie – kombajn. Wielka kobyła, która sama w sobie stanowi dość sporą aplikację. Większość z nas nie wykorzysta nawet kilku procent jej możliwości, a sama konfiguracja zajmie nam kilkanaście minut. Więc pytanie – czy na pewno jest nam to potrzebne? Czy nie da się prościej? Oczywiście, że tak i w tym artykule pokażę Wam jak zrobić to w łatwy i przyjemny sposób.
Przygotowanie projektu
Zabawę zaczynamy od stworzenia nowego, czystego projektu Symfony. Wykorzystujemy do tego Composer’a i polecenia:
composer create-project symfony/skeleton auth
W ten sposób stworzymy czystą instalację Symfony 4, zawierającego wyłącznie najpotrzebniejsze pliki. Ostatni parametr (auth) to nazwa folderu, w jakim zostanie zainstalowany framework. Wchodzimy do naszego projektu i instalujemy kilka potrzebnych elementów, takich jak pakiet security, annotations oraz twig. Tak przygotowany projekt umożliwi nam stworzenie prostego systemu logowania.
composer require twig composer require security composer require annotations
Konfiguracja pakietu security
Pierwszym krokiem, w przygotowaniu naszego systemu, będzie odpowiednia konfiguracja pakietu security. Otwieramy plik security.yml, z katalogu config/packages. Wewnątrz znajduje się podstawowa konfiguracja naszego systemu autoryzacji. Naszym celem na początek jest dodanie użytkowników do pamięci systemu (na razie nie będziemy korzystać z bazy danych). Aby to zrobić w sekcji providers.in_memory.memory dodajemy strukturę users, która zawiera nazwę użytkownika oraz dwa pola password i roles. Następnie do głównej gałęzi security dorzucamy konfigurację encoders, jako plain text. Trzecim elementem, który musimy ustawić jest sekcja firewalls.main. W tym miejscu konfigurujemy dwa elementy. Form_login i logout, w których ustawiamy kolejno ścieżki do logowania i uwierzytelniania (w przypadku form_login) oraz ścieżkę wylogowania i przekierowania po wylogowaniu (dla logout). Przykładowa konfiguracja przedstawiona jest poniżej.
security: encoders: Symfony\Component\Security\Core\User\User: plaintext # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: in_memory: memory: users: malvor: password: security roles: 'ROLE_USER' admin: password: password roles: 'ROLE_ADMIN' firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: anonymous: true form_login: login_path: login check_path: login logout: path: /logout target: / # activate different ways to authenticate # http_basic: true # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate # form_login: true # https://symfony.com/doc/current/security/form_login_setup.html # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER }
Uwagę należy zwrócić przede wszystkim, w jaki sposób dodawani są kolejni użytkownicy. Kluczem w strukturze, jest username zawierający dwa najważniejsze pola: password i roles. Jak możemy zauważyć, hasło to czysty plaintext, który ustawiamy w sekcji encoders. Podajemy tutaj nazwę obiektu (w tym przypadku jest to domyślny obiekt symfony) oraz metodę, w kodowania hasła. Na chwilę obecną nie będziemy używać żadnego encodera do ukrycia hasła, ale jeśli chcesz wiedzieć, jak zrobić to w kodzie, możecie znaleźć w dokumentacji (https://symfony.com/doc/current/security.html). Taka konfiguracja pozwoli nam obsłużyć już prosty system uwierzytelniania.
System logowania
Mamy już pełną konfigurację, więc teraz czas na dopisanie logiki aplikacji. Stwórzmy sobie kontroler LoginController w folderze src/Controller. Wewnątrz kontrolera dodajemy akcję login, która przyjmuje dwa parametry. Pierwszym z nich jest obiekt Request, drugim natomiast AuthenticationUtils z pakietu Symfony\Component\Security\Http\Authentication. Ustawiamy odpowiednią ścieżkę, do akcji (tę samą, którą ustawiliśmy w form_login do login_path.
<?php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; /** * Class LoginController * @package App\Controller */ class LoginController extends Controller { /** * @Route("/login", name="login") */ public function login(Request $request, AuthenticationUtils $authenticationUtils) : Response { $errors = $authenticationUtils->getLastAuthenticationError(); $lastUsername = $authenticationUtils->getLastUsername(); return $this->render('User/login.html.twig', [ 'errors' => $errors, 'username' => $lastUsername ]); } /** * @Route("/logout", name="logout") */ public function logout() : Response {} }
Wewnątrz akcji robimy 3 rzeczy. Pierwszą z nich jest pobranie ostatnich błędów logowania. Robimy to przy pomocy metody getLastAuthenticationError z obiektu AuthenticationUtils. Ten obiekt pozwala nam także na pobranie ostatnio używanego loginu przez użytkownika. Aby tego dokonać używamy metody getLastUsername. Ostatnim krokiem jest przekazanie obu zmiennych do widoku. Tworzymy prosty formularz w pliku templates/User/login.html.twig.
Dodajemy także drugą akcję logout, która może zostać całkowicie pusta. Symfony sama zrobi magię wylogowania użytkownika, potrzebuje jedynie tego routingu.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %}{% endblock %} <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous"> </head> <body> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> {% if app.user %} <a class="navbar-brand" href="#">{{ app.user.username }}</a> {% endif %} <div class="collapse navbar-collapse" id="navbarResponsive"> <ul class="navbar-nav ml-auto"> {% if app.user %} <li class="nav-item"> <a class="nav-link" href="{{ path('logout') }}">Logout</a> </li> {% else %} <li class="nav-item"> <a class="nav-link" href="{{ path('login') }}">Login</a> </li> {% endif %} </ul> </div> </div> </nav> <div class="container"> {% block body %}{% endblock %} </div> {% block javascripts %}{% endblock %} </body> </html>
Używamy rozszerzenia podstawowej templatki jaką dostarcza nam rozszerzenie Twig’a. Następie w bloku body wrzucamy nasz formularz oraz listujemy wszystkie błędy, jeśli jakieś istnieją. Najprostszym sposobem będzie użycie wbudowanej funkcjonalności errors.messageKey (errors to zmienna, którą przekazaliśmy do widoku).
Sprawdźmy czy to działa
Teraz otwieramy nasz projekt ze ścieżką /login. Powinniśmy zobaczyć prosty formularz, który po wpisaniu danych z pliku security zaloguje naszego użytkownika. Teraz musimy tylko sprawdzić czy wszystko działa poprawnie. W celu sprawdzenia, czy cały proces przebiegł bez zarzutu zainstalujemy profiler dla środowiska developerskiego.
composer require profiler --dev
Jeśli użyliśmy standardowego szablonu twig, wszystkie potrzebne pliki js i css zostaną dołączone automatycznie. Teraz po odświeżeniu naszej strony, zobaczymy piękną belkę na dole strony. Dla nas najważniejsza będzie 5 sekcja od lewej strony (ta z ludzikiem). Jeżeli nakierujemy kursorem myszki na ten element, zobaczymy czy udało nam się poprawnie zalogować, a nasz użytkownik ma poprawną strukturę. Zobaczymy także link do wylogowania. Jeśli go użyjemy, nasz użytkownik zostanie wylogowany.

I w ten oto sposób udało nam się stworzyć działający system autoryzacji.
Dynamiczni użytkownicy – dodajemy bazę obsługę bazy danych
Ok, działa. Ale to nie jest zbyt funkcjonalne. Żeby dodać kolejnego użytkownika, musielibyśmy ingerować w kod aplikacji. To trochę czasochłonne i uniemożliwi nam stworzenie np. rejestracji w serwisie. Czego więc potrzebujemy? Bazy danych. Więc ją dodajmy, to nic trudnego.
Do obsługi bazy danych użyjemy ORM’a Doctrine. Zacznijmy od zainstalowania dwóch elementów. Pierwszy z nich to sam Doctrine.
composer require doctrine
Jedyne co musimy skonfigurować to nasze połączenie z bazą danych. Na potrzeby tego projektu, zrobimy to bezpośrednio w pliku .env, który znajduje się w głównym katalogu projektu. Szukamy tam linijki zaczynającej się od DATABASE_URL (jeśli jej nie ma, to musimy dodać całość ręcznie). Przykładowe połączenie z bazą danych powinno wyglądać następująco:
DATABASE_URL=mysql://username:password@server_address/database_name
Teraz potrzebujemy wygenerować encję użytkownika. Instalujemy bibliotekę maker’a dla Doctrine
composer require doctrine maker
Teraz musimy odpalić komendę do stworzenia encji.
./bin/console make:entity
Jedynym parametrem, jaki musimy podać jest nazwa encji (w naszym przypadku jest to User).
The class name of the entity to create (e.g. AgreeablePizza): User > User created: src/Entity/User.php created: src/Repository/UserRepository.php Success! Next: Add more fields to your entity and start using it. Find the documentation at https://symfony.com/doc/current/doctrine.html#creating-an-entity-class
Wygenerowane zostały dwa pliki. Pierwszy z nich to encja naszego użytkownika, a drugi to repozytorium (nim na razie nie będziemy się przejmować). Otwieramy plik src/Entity/User.php, w którym znajdziemy klasę User z jednym atrybutem id. Aby zachować spójność naszej encji z frameworkiem, implementujmy do niej UserInterface (Symfony\Component\Security\Core\User\UserInterface). Do poprawnego działania potrzebujemy zaimplementować pięć metod getRoles(), getPassword(), getUsername(), eraseCredentials(), getSalt(). Dwie ostatnie zostawiamy póki co puste. Najważniejsze są getRoles, która musi zwracać tablicę, getUsername oraz getPassword. Dodajemy dwa atrybuty: username, password. Ostatnim krokiem jest zaimplementowanie getterów dla obu parametrów i settera dla pola password. Gotowe. Nasza klasa jest przygotowana do pracy z Symfony.
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; /** * @ORM\Entity(repositoryClass="App\Repository\UserRepository") */ class User implements UserInterface { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="string", length=128) */ private $username; /** * @ORM\Column(type="string", length=128) */ private $email; /** * @ORM\Column(type="string", length=4096) */ private $password; /** * User constructor. * @param string $username * @param string $email */ public function __construct(string $username, string $email) { $this->username = $username; $this->email = $email; } public function getRoles() { return ['ROLE_USER']; } public function getPassword() : string { return $this->password; } public function getSalt() { // TODO: Implement getSalt() method. } public function getUsername() { return $this->username; } public function getEmail() { return $this->email; } public function eraseCredentials() { // TODO: Implement eraseCredentials() method. } /** * @param string $password */ public function setPassword(string $password) { $this->password = $password; } }
W ten sposób mamy przygotowaną encję użytkownika. Musimy jeszcze stworzyć tabelę w bazie danych. Użyjemy do tego konsoli i prostego polecenia Doctrine.
./bin/console doctrine:schema:update --force
Teraz wystarczy podpiąć ją do systemu. Wracamy do pliku security.yml znajdującego się w config/packages. Zaczynamy od ustawienia provider. Usuwamy in_memory, którego używaliśmy i dodajemy nasz własny. Nazwa jest dowolna (w moim przypadku to db_provider). Wewnątrz struktury deklarujemy metodę przechowywania użytkowników (entity). Następnie jako kolejny poziom zagnieżdżenia podajemy dwa parametry. Pierwszym z nich to klasa naszej encji, a drugi to property (parametr, po którym będziemy identyfikować użytkownika). W naszym przypadku klasą jest App\Entity\User, property natomiast to username. Na tym etapie, moglibyśmy ustawić np. identyfikację po adresie email.
db_provider: entity: class: App\Entity\User property: username
Teraz ostatnim elementem jest dodanie encodera dla naszej encji. W sekcji encoders usuwamy wartość, która była wcześniej, a w jej miejsce dodajemy konfigurację dla naszej klasy. Chcemy, aby nasze hasło było szyfrowane. W tym miejscu musimy zadeklarować jakiego rodzaju algorytmu Symfony powinno używać.
App\Entity\User: algorithm: bcrypt
Cały kod naszej konfiguracji prezentuje się w następujący sposób:
security: encoders: App\Entity\User: algorithm: bcrypt # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: db_provider: entity: class: App\Entity\User property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: anonymous: true form_login: login_path: login check_path: login logout: path: /logout target: / # activate different ways to authenticate # http_basic: true # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate # form_login: true # https://symfony.com/doc/current/security/form_login_setup.html # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER }
Teraz musimy tylko dodać naszego użytkownika do bazy danych. W tym celu dla usprawnienia całego procesu skorzystamy z dodatkowej biblioteki Doctrine. Dokładniej chodzi o DoctrineFixtures. Instalujemy ją następującą komendą:
composer require doctrine/doctrine-fixtures-bundle --dev
Następnie wystarczy tylko dodać nowy plik w src\DataFixtures o nazwie UserFixtures.php. Wewnątrz tworzymy klasę o nazwie UserFixtures, która rozszerza Doctrine\Bundle\FixturesBundle\Fixture. Implementujemy dwie metody. Pierwszą z nich, to konstruktor, który przyjmuje dwa argumenty: Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface $encoder oraz Doctrine\ORM\EntityManagerInterface $entityManager. Oba argumenty przypisujemy do atrybutów naszej klasy. Drugą metodę nazywamy load. Metoda ta przyjmuje jeden argument Doctrine\Common\Persistence\ObjectManager $manager. Implementujemy w niej kod dodawania nowego użytkownika.
Tworzymy obiekt klasy App\User\Entity podając jako argument nazwę naszego użytkownika. Następnie do zmiennej $password przypisujemy wynik metody encodePassword. Metoda ta wymaga podania dwóch parametrów. Pierwszy to instancja obiektu naszej encji użytkownika, a drugi to hasło w formie plain text. Następnie hasło to ustawiamy dla obiektu użytkownika (używamy metody setPassword) i zapisujemy do bazy danych przy pomocy entity managera.
<?php namespace App\DataFixtures; use App\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; /** * Class UserFixtures * @package App\DataFixtures */ class UserFixtures extends Fixture { /** * @var UserPasswordEncoderInterface */ private $encoder; /** * @var EntityManager */ private $entityManager; /** * UserFixtures constructor. * @param UserPasswordEncoderInterface $encoder Password encoder instance */ public function __construct(UserPasswordEncoderInterface $encoder, EntityManagerInterface $entityManager) { $this->encoder = $encoder; $this->entityManager = $entityManager; } /** * @param ObjectManager $manager Object manager instance * * @return void */ public function load(ObjectManager $manager) : void { $user = new User('user', 'user@test.com'); $password = $this->encoder->encodePassword($user, 'secret'); $user->setPassword($password); $this->entityManager->persist($user); $this->entityManager->flush(); } }
Teraz wystarczy załadować nasze dane do bazy przy pomocy polecenia
./bin/console doctrine:fixtures:load
Jeśli sprawdzimy teraz jak wyglądają dane w naszej tabli.

Jak widać rekord dodał się poprawnie, a hasło jest zakodowane. Teraz wystarczy sprawdzić przy pomocy naszego formularza, czy możemy zalogować się przy pomocy danych, które dodaliśmy w naszym fixture.
Podsumowanie
Artykuł ten miał za zadanie pokazać w jaki sposób stworzyć prosty system autoryzacji używając tylko komponentów Symfony. Projekt zrealizowany został dla frameworka w wersji 4, ale w tej kwestii nie zmienia się nic od wersji 2 (poza samą strukturą Symfony).
Mam nadzieję, że uda mi się przekonać Was do rzadszego stosowania ciężkiego narzędzia, jakim jest FOSUserBundle, którego większość z Was nie będzie potrzebować. W następnym artykule przedstawię jak rozwiązać problem rejestracji nowego użytkownika.
Pobierz gotowy przykład: kliknij tutaj. (wystarczy tylko zmienić dane do bazy danych oraz zainstalować wszystkie zależności przy pomocy composer install)
Lub sprawdź projekt na githubie.
Witam,
wszystko fajnie i pięknie. Ale możesz jeszcze dopisać np. jak zabezpieczyć widok/stronę przed wejściem niezalogowanego użytkownika.
Jasne. W ciągu tygodnia pojawi się post o rejestracji i security.