System uwierzytelniania w aplikacjach internetowych na bazie Symfony Framework.

21.12.2022 | Autor: Marcin

W poprzednim artykule dowiedzieliśmy się w jaki sposób przygotować panel zarządzania stroną internetową w oparciu o EasyAdmin oraz Symfony. Kolejnym etapem budowy naszej aplikacji webowej będzie wprowadzenie uwierzytelniania oraz autoryzacji. Naszym celem jest ograniczenie dostępu postronnym użytkownikom do zasobów panelu administracyjnego.

Uwierzytelnianie vs Autoryzacja

Na początku warto wyjaśnić sobie zagadnienie jakim jest różnica pomiędzy uwierzytelnianiem oraz autoryzacją. Bardzo często są to terminy używane zamiennie, niestety rodzi to sporo problemów oraz nieporozumień. Do tego całego zamieszania należy dopisać jeszcze jedno pojęcie, które w zasadzie można uznać za błąd językowy – „autentykacja”. Najlepiej usunąć ze swojego słownika tę ostatnią frazę, gdyż jest ona wynikiem karkołomnej próby tłumaczenia z języka angielskiego słowa „authentication”.

Przechodząc do sedna sprawy uwierzytelnianie to proces, który polega na weryfikacji użytkownika. Dokładniej chodzi o sprawdzenie tego, czy osoba jest tym za kogo się podaje. W systemach informatycznych realizuje się to najczęściej poprzez formularz, gdzie użytkownik zmuszony jest do podania loginu oraz hasła.

Autoryzacja jest czymś odmiennym i do tego wcale nie musi być wykorzystywana w połączeniu z uwierzytelnianiem. Odpowiada za weryfikację tego czy dany użytkownik powinien mieć dostęp do określonych zasobów.

W przypadku naszej strony internetowej, którą rozwijamy w ramach tego artykułu, wydaje się naturalnym połączenie uwierzytelnienia oraz autoryzacji. Dostęp do panelu zarządzania stroną www powinien być ograniczony do określonej grupy użytkowników, tak aby nikt niepowołany nie mógł nanosić zmian w naszym systemie.

Założenia systemu bezpieczeństwa dla naszej strony internetowej

Naszym celem jest zapewnienie bezpieczeństwa dla panelu administracyjnego aplikacji internetowej. Powinniśmy zadbać, o to aby nikt niepowołany nie mógł dostać się do strefy zastrzeżonej dla wskazanych osób.
Posiadamy już w naszym systemie zarządzanie użytkownikami, dlatego najlepiej byłoby wykorzystać nasze repozytorium jakim jest baza danych do przechowywania poświadczeń. Powinniśmy dodatkowo rozbudować encję User również o pole z tzw. rolami. Posłuży nam to do budowy systemu autoryzacji, który będzie mieć spore możliwości rozbudowy. Na początek jednak stworzymy tylko dwie role: ROLE_USER oraz ROLE_ADMIN.

Rozwiązanie out of box

Na początku mam dla Ciebie dobrą wiadomość. Symfony ma wbudowane mechanizmy, które będą nas wspomagać podczas zabezpieczania naszej aplikacji internetowej. Musimy zadbać o to, aby na pokładzie znalazł się SecurityBundle. Z pomocą przychodzi nam flex, który zadba o prawidłową instalację tego bundla.

composer require symfony/security-bundle

Jeśli wszystko przebiegło prawidłowo to w naszym systemie powinien pojawić się plik konfiguracyjny config/packages/security.yaml. Jest to serce systemu bezpieczeństwa dla naszej strony www. Nie przejmuj się jego zawartością, wiem że na pierwszy rzut oka może być to przytłaczające. Za moment wyjaśnię, do czego służą poszczególne sekcje konfiguracji. Następnie krok po kroku przejdziemy proces, który pozwoli nam zabezpieczyć dostęp do panelu administracyjnego do zarządzania witryną.

Security.yaml

Konfiguracja naszego systemu zabezpieczeń składa się z 3 głównych części. Zanim przejdziemy dalej musimy sobie wyjaśnić, która sekcja za co jest odpowiedzialna.

Providers

Część odpowiedzialna za użytkowników, to w jaki sposób są składowani w systemie oraz w jaki sposób jest realizowany dostęp do repozytoriów przechowujących dane userów.

Firewalls

Sekcja w której definiujemy podział naszej witryny na części. Każdy request kierowany do aplikacji będzie musiał przejść przez firewall, do którego zostanie dopasowany np. poprzez żądany adres url.
W ramach firewall’a definujemy czy uwierzytelnienie jest konieczne, formularz logowania, user provider.

Access Controll

Jest to definicja praw dostępu do wybranych części systemu.

Kiedy mamy już wszystko wyjaśnione odnośnie struktury pliku konfiguracyjnego, powinniśmy się skierować do pliku encji User, która to została stworzona przy pomocy MakerBundle w ramach poprzedniego artykułu. Zwróćmy uwagę na fakt, że klasa implementuje dwa interfejsy, które zapewniają nam kompatybilność z system uwierzytelniania oraz autoryzacji symfony.

<?php

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180, unique: true)]
    private ?string $email = null;

    #[ORM\Column]
    private array $roles = [];

    /**
     * @var string The hashed password
     */
    #[ORM\Column]
    private ?string $password = null;

W momencie gdy upewnimy się, że klasa istnieje oraz posiada wszystkie wymagane właściwości oraz metody, możemy przejść do modyfikacji pliku konfiguracyjnego security.yaml.

W pierwszej kolejności musimy zadbać o odpowiedni user provider, dlatego w sekcji providers dodajemy app_user_provider. Będzie on zasilał naszą aplikację internetową w dane użytkowników. Dlatego wskazujemy ścieżkę do klasy User oraz właściwość email, która będzie pełnić funkcję identyfikatora.

# config/packages/security.yaml
security:
    # ...

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

W związku z tym, że dane użytkowników będziemy przechowywać w lokalnej bazie danych, musimy pomyśleć nad formą składowania haseł użytkowników. Tutaj z pomocą również przychodzi nam symfony, które dostarcza nam gotowe mechanizmy realizujące te funkcje. W tym celu dodajemy nową sekcję „password_hashers” w pliku security.yaml. Tam należy wskazać klasę odpowiedzialną za tzw. hashowanie haseł. Domyślnym algorytmem hashowania haseł z którego korzysta symfony w najnowszej wersji jest bcrypt. Nic nie stoi jednak na przeszkodzie, aby wykorzystać inną metodę. Wystarczy zdefiniować własną klasę implementującą interfejs Symfony\Component\PasswordHasher\PasswordHasherInterface. Czasami się to przydaje w momencie, gdy migrujemy dane z jednego systemu do innego.

# config/packages/security.yaml
security:
    # ...
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

Bardzo ważne jest abyśmy uświadomili sobie, że hasła które przechowujemy w bazie danych muszą być w formie zahashowanej. To implikuje konieczność odpowiedniego przetworzenia ich przed zapisaniem w bazie. Przykładowo jest nasz formularz dodawania użytkowników w panelu administracyjnym przyjmuje hasło, musimy przed zapisaniem go, dokonać hashowania. To samo dotyczy wykorzystywania tzw. fixtures. Należy też pamiętać, że jeśli stworzymy formularz rejestracji w naszym systemie, to również musimy zapewnić odpowiednią implementację realizującą hashowanie hasła. W sytuacji gdy posiadamy w bazie danych już hasła użytkowników zapisane otwartym tekstem, możemy posłużyć się poleceniem konsolowym, odpowiedzialnym za hashowanie haseł już istniejących.

php bin/console security:hash-password

Przejdźmy teraz do firewalla naszej strony internetowej. Domyślnie wygenerowany plik security.yaml posiada już wpis w tej sekcji.

security:
    # ...
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider

Pierwszym zdefiniowanym firewallem jest „dev”, który został ograniczony do określonego wzorca. Jak widzimy chodzi o to, aby zdjąć system bezpieczeństwa ze wszystkich requestów, które pochodzą z profilera, bądź też z plików statycznych.
To co nas najbardziej interesuje to firewall o nazwie „main”. Musimy mu wskazać user provider, z którego powinien korzystać. W naszym przypadku będzie to wcześniej zdefiniowany app_user_provider. Dodatkowo określamy jeszcze właściwość lazy, która zapewnia nam że aplikacja internetowa będzie ładować obiekt użytkownika dopiero w momencie gdy będzie to niezbędne.

Formularz logowania

Dotarliśmy do momentu gdy musimy zadbać o miejsce, w którym użytkownicy będą mogli się uwierzytelnić przed uzyskaniem dostępu do panelu zarządzania naszą stroną internetową. Zacznijmy zatem od wygenerowania kontrolera, który będzie odpowiedzialny za proces uwierzytelniania w tym wyświetlenie formularza logowania.
Korzystając z makera, generujemy kontroler Login.

php bin/console make:controller Login

W efekcie w katalogu src/Controller pojawi nam się nowy plik.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class LoginController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function index(): Response
    {
        return $this->render('login/index.html.twig', [
            'controller_name' => 'LoginController',
        ]);
    }
}

W tym momencie jest to zwykły kontroler, który jeszcze niczym szczególnym się nie wyróżnia. To co nas interesuje to nazwa akcji „app_login”, którą powinniśmy zachować a następnie posłużyć się nią w pliku security.yaml.

# config/packages/security.yaml
security:
    # ...
    firewalls:
        main:
            # ...
            form_login:
                login_path: app_login
                check_path: app_login

Jak widzisz do firewalla „main” dodaliśmy wpisy dotyczące formularza, gdzie określiliśmy login_path oraz check_path. Są to odpowiednio ścieżki routingu do wyświetlenia formularza logowania oraz do weryfikacji hasła użytkownika. W naszym przypadku jedna akcja kontrolera będzie realizować obie te funkcje.

Wróćmy jeszcze na chwilę do naszego kontrolera. Musimy go dostosować, aby mógł pełnić swoją funkcję uwierzytelniającą. Do metody index wstrzykniemy serwis $authenticationUtil. Ponadto przekażemy do szablonu html informacje o ewentualnych błędach formularza oraz informację o nazwie użytkownika, która była ostatnio użyta.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class LoginController extends AbstractController
{
    #[Route('/login', name: 'app_login')]
    public function index(AuthenticationUtils $authenticationUtils): Response
    {

        $error = $authenticationUtils->getLastAuthenticationError();
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('login/index.html.twig', [
            'last_username' => $lastUsername,
            'error'         => $error,
        ]);
    }
}

Zajmijmy się teraz przygotowaniem twiga dla naszego szablonu hmtl.

{% extends 'base.html.twig' %}


{% block body %}
    {% if error %}
        <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    <form action="{{ path('app_login') }}" method="post">
        <label for="username">Email:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}">

        <label for="password">Hasło:</label>
        <input type="password" id="password" name="_password">     
        <input type="hidden" name="_target_path" value="/account"> #}
        <button type="submit">zaloguj</button>
    </form>
{% endblock %}

Do pełni szczęścia musimy jeszcze zadbać o prawidłową obsługę wylogowania z aplikacji internetowej. Aby tego dokonać musimy stworzyć odpowiednią akcję kontrolera. Posłużymy się wcześniej stworzym LoginController’em dopisując następujący kod.

#[Route('/logout', name: 'app_logout', methods: ['GET'])]
    public function logout()
    {
        throw new \Exception('Don\'t forget to activate logout in security.yaml');
    }

Ostatnim krokiem jest uzupełnienie konfiguracji firewalla wskazując ścieżkę do akcji odpowiedzialnej za wylogowanie.

form_login:
    login_path: app_login
    check_path: app_login
logout:
    path: app_logout

Na sam koniec zajmiemy się bardzo ważnym elementem jakim jest access control, czyli tablica autoryzacyjna gdzie konfigurujemy zasoby oraz grupy użytkowników jakie powinny mieć do nich dostęp. Na potrzeby naszej strony www stworzymy tylko jeden wpis. Będzie on ograniczał dostęp do wszystkich zasobów rozpoczynający swoje ścieżki od „admin”. Dzięki temu panel zarządzania aplikacją będzie w zasięgu jedynie administratorów strony.

access_control:
    - { path: ^/admin, roles: ROLE_ADMIN }

Podsumowanie

Właśnie zakończyliśmy prace programistyczne, a w zasadzie konfiguracyjne gdyż większość kodu została wygenerowana automatycznie. Teraz wystarczy wpisać w pasek przeglądarki internetowej adres panelu administracyjnego z naszą aplikacją. Powinien pojawić się formularz, który zapewne wypadałoby zmodyfikować, aby jego wygląd był bardziej atrakcyjny. Nie zmienia to faktu, iż funkcjonalnie jesteśmy gotowi do uwierzytelnienia. Po wprowadzeniu prawidłowego loginu oraz hasła powinniśmy zostać przekierowani do panelu.

Prawidłowa autoryzacja w panelu zarządzania stroną internetową

Dzięki profilerowi jesteśmy w stanie w łatwy sposób zweryfikować kim jesteśmy dla aplikacji. Jak widać na powyższym screenie, jesteśmy uwierzytelnieni jako użytkownik z rolą ROLE_ADMIN w ramach firewalla main.
Przedstawiony w artykule przykład jest jednym z najprostszych możliwych zastosowań. W rzeczywistych aplikacjach internetowych system uwierzytelniania i autoryzacji byłby zapewne bardziej skomplikowany. W naszych warunkach jednak byliśmy w bardzo łatwy sposób zapewnić bezpieczeństwo stronie internetowej, co pokazuje jakim narzędziem jest framework symfony.

Porozmawiaj z nami
o swoim projekcie

+48 506 160 480
[email protected]

lub napisz