unity wiedza

Czas czytania: 13 minut

Uwierzytelnienie użytkownika z wykorzystaniem tokenów JWT, Express, bazy danych MongoDB oraz użycie ich w kliencie Vue.js

Co zawiera ten przewodnik?

Wracam do was z drugą częścią artykułu na temat uwierzytelnienia z wykorzystaniem naszego API. Pierwszą część możecie przeczytać na naszym blogu. Dzisiaj pokaże wam jak zaimplementować w kliencie Vue.js napisane przez nas interfejsy API. Prace te wykonywaliśmy w Unity kilka tygodni temu. Zakładam, że jeżeli udało Ci się tutaj trafić szukasz odpowiedzi na konkretne pytania. Dowiesz się jak wykorzystać nasze API i zaimplementować je w kliencie Vue. Jeżeli poszukujesz chociaż częściowo odpowiedzi na temat uwierzytelnienia z wykorzystaniem tokenów JWT oraz uwierzytelnienia w kliencie napisanym we frameworku Vue, mam nadzieje, że dobrze trafiłeś.

Zanim zaczniemy stawiać sobie konkretne pytania oraz na nie odpowiadać, pamiętaj, że musiałem poczynić pewne założenia. Zakładam, drogi czytelniku że posiadasz podstawową wiedzę na temat Vue.js, Vuex oraz JavaScript. Pamiętaj że wzorując się na tym artykule możesz zintegrować nasze interfejsy API z interfejsem użytkownika stworzonym np. w React, Angular czy jakimkolwiek kliencie którego stworzyłeś. Pytania które postawimy będą uniwersalne.

Pytania na które postaram odpowiedzieć podczas tego poradnika:
– Gdzie przechowywać tokeny?
– Jak skonfigurować Axios?
– Jak powinno wyglądać logowanie, sesja użytkownika oraz przywrócenie jej?
– Kiedy usuwać tokeny?
– Jak obsłużyć brak autoryzacji, czyli błąd 401?
– Jak obsługiwać trasy naszego klienta?

Jednak jeżeli nie widziałeś pierwszej części artykułu odsyłam Cię do niej, będzie łatwiej Ci zrozumieć artykuł który w tym momencie planujesz czytać dalej. Podrzucam link i zapraszam do udanej lektury 😉

A jeszcze jedno, chciałbym zaznaczyć że kompletny projekt z naszym API oraz Klientem znajdziesz na repozytorium GitHub. Jeżeli moje wyjaśnienia nie będą wystarczające spróbuj zmierzyć się z kodem. Powodzenia!

Obsługa uwierzytelnienia w kliencie Vue.js

Kiedy to ja pracując nad jednym z projektów, musiałem zmierzyć się z uwierzytelnieniem opartym o tokeny JWT i zaimplementować je w kliencie Vue, stawiałem sobie konkretne pytania, które przedstawiłem wcześniej. To dzięki nim mogłem określić dobry kierunek realizacji zadania. Przechodząc przez proces implementacji uwierzytelnienia w naszym kliencie, stopniowo będę odpowiadał na pytania oraz pokazywał kod który odpowiada za dane zachowanie.

Gdzie przechowywać tokeny?

Gdzie przechowywać dane po stronie użytkownika? Mamy możliwość wyboru kilku magazynów do przechowywania danych np. localStorage, SessionStorage czy Cookies. Magazyn localStorage charakteryzuje się długotrwałym przechowywaniem danych w przeglądarce. Dane istnieją dopóki nie zostaną usunięte oraz są dostępne jedynie w tej samej domenie w której zostały utworzone. SessionStorage jest podobny do localsgtorage, jedyną różnicą jaką posiada jest trwałość danych. Zostają one usunięte wraz z opuszczeniem domeny przez użytkownika np. zamknięciem karty, czy przeglądarki. A Cookies? Cookie jest magazynem który jest wstanie przechowywać bardzo małe ilości danych, mamy jednak możliwość ustawienia ważności każdego pliku cookies. Dodatkowo dane muszą być wysyłane na wszystkich żądaniach HTTP, co zwiększa ruch między klientem a serwerem. My nasze tokeny będziemy przechowywać w magazynie localStorage, ponieważ nasza aplikacja potrzebuje tokeny po stronie klienta, jedynie token odświeżający będziemy wysyłać w zapytaniu. Dodatkowo chcemy, aby dane istniały i nie zostały usunięte w momencie zamknięcia naszego klienta. Ja, w pliku w którym gromadzę zbiór funkcji do obsługi naszego mechanizmu uwierzytelnienia, stworzyłem następujące funkcje które są eksportowane, dlatego mogę z nich korzystać w różnych momentach. Oto jak one wyglądają:

Pierwsze dwie linijki kodu to wyrażenia funkcyjne, które zwracają nam poszczególną wartość odpowiedniego klucza w localStorage. W naszym wypadku są to dwa tokeny accessToken oraz refreshToken.

setLocalStorageTokens to funkcja, która ustawia nam odpowiednie wartości tokenów zwróconych przez backend w localStorage. Tą funkcję z odpowiednim parametrem będziemy wykorzystywać w akcji logowania, czy odświeżania cyklicznego tokenów. Parametrem, jaki przyjmie, będzie obiekt z aktualnymi tokenami autoryzacyjnymi.Tak będzie wyglądać ten obiekt:

Funkcje removeLocalStorageTokens używać będziemy w momencie kiedy będziemy mieli obowiązek usuwać tokeny, na przykład podczas wylogowywania.

Konfiguracja Axios

Axios, to oparty na obietnicach klient HTTP, który działa w przeglądarce oraz w środowisku Node.js. Axios zapewnia nam pojedynczy interfejs API do obsługi interfejsu XMLHttpRequest dla węzła HTTP. Użyjemy go do komunikacji z naszym API.

Nasza konfiguracja komunikacji klienta z API powinna wyglądać następująco: początkowo musimy zdefiniować zmienną z adresem do naszego serwisu, w moim wypadku apiHost, oraz musimy utworzyć nową instancję Axios z naszą konfiguracją. Dodatkowo skonfigurowałem dwa nagłówki, ponieważ musimy również powiedzieć jaki format odpowiedzi naszego serwera akceptujemy, za co odpowiada „Accept”, oraz ustawić „Content-Type”, który informuje jakiego formatu dane będziemy przesyłać, co determinuje sposób w jaki nasze API powinno je odczytać.

Zmienna API zawiera definicję nowej instancji Axios, natomiast authenticationHeader zawiera odpowiednią konstrukcję naszego nagłówka z tokenem do weryfikacji prawidłowego uwierzytelnia na interfejsach API, które tego wymagają, czyli są chronione.

Teraz w pliku ze zbiorem funkcji dla naszego uwierzytelnienia będziemy mogli dodać odpowiednią definicję enepointów dla naszego API.

Musimy jeszcze zaimportować naszą konfigurację Axios, tak jak poniżej:

Teraz za każdym razem kiedy definiujemy endpoint do naszego API, używamy instancji Axios oraz jeżeli jest on chroniony, czyli mamy wymóg przekazać nagłówek z ważnym tokenem, nie będziemy mieli z tym problemu, ponieważ wyeksportowaliśmy sobie authenticationHeader.

Zauważ, że nasz nagłówek oprócz odpowiedniej konstrukcji, nie jest niczym innym jak getAccessToken.

Czyli w nagłówku przekażemy token, który przechowujemy w localStorage, dlatego naszym obowiązkiem będzie aktualizować stan tokenów zapisanych w magazynie localStorage.

Logowanie, sesja użytkownika oraz przywrócenie jej

Krok po kroku będziemy szli przez proces logowania, weryfikacji tokenów, ustawienia odpowiednich danych w naszym magazynie vuex oraz funkcji która odpowiadać będzie za odnawianie tokenów.

W naszym kliencie korzystam z magazynu Vuex, który mamy możliwość użyć we Vue. Również w nim będę przechowywał kluczowe informacje. Popatrz poniżej jak będzie wyglądała konfiguracja mojego pliku state.js

isAuthorized jest flagą, która będzie ustawiana zależnie od stanu, czy użytkownik jest uwierzytelniony czy nie.

tokenRefreshCounterId przechowywać będzie identyfikator metody do odświeżania tokenu.
auth tu przechowywać będziemy dekodowany obiekt tokenu dostępu.
account tu będą się znajdować np. Lista użytkowników znajdujących się w bazie danych.

Przejdźmy do akcji logowania użytkownika authenticationUser, którą wywołamy na panelu logowania naszego użytkownika.

Przekazujemy email i hasło, a w odpowiedzi dostajemy ten oto obiekt z tokenami:

Kolejnym krokiem jest wywołanie akcji authorize.

Ta akcja ma kilka zadań. Pierwsza funkcja, o której już wspominałem, ustawia w localStorage tokeny, które otrzymaliśmy w odpowiedzi od serwera po pozytywnym uwierzytelnieniu, następnie wywołujemy dwie mutacje. Pierwsza mutacja SET_AUTH_USER ma za zadanie dekodować nasz token dostępu i ustawić odpowiedni obiekt w state.authUser. Do tego używam funkcji jwtDecode biblioteki jwt-decode, którą musiałem zaimplementować w projekcie. Jeżeli przekażemy wartość null to mutacja ustawi wartości zerowe. Oprogramowane zostało to tak przeze mnie ponieważ wykorzystujemy też tą mutację do zerowania stanu state.authUser.

Druga mutacja ma jedno zadanie –  dla state.isAuthorized ma ustawić boolean. Mutacja przypisuje jedynie wartość, ważniejsze jest co robi ta funkcja.

Jako parametr do tej funkcji przekazujemy aktualny token z localStorage, a zadaniem tej funkcji jest zweryfikować token i zwrócić boolean zależnie od tego czy token jest ważny czy może nie.

Funkcja wygląda następująco. Wykorzystałem w niej funkcje dostarczone przez biblioteki date-fns w celu weryfikacji ważności tokenu. Jeżeli czas aktualny jest przed czasem wygaśnięcia naszego tokenu, przejdzie weryfikacja i zostanie zwrócone true. W tym momencie możemy śmiało powiedzieć, że od tego momentu przekazany token jest prawidłowy.

Jednak nasze pytanie zmierza do sesji użytkownika oraz przywrócenia tej sesji. Dla nas sesja użytkownika to ciągle ważny token, no ale przecież jego ważność jest chwilowa (2-3 minuty) i czy w związku z tym użytkownik będzie musiał na nowo się logować? Nie. W tym celu musimy utworzyć cykliczne odnawianie tokenu. Czyli podczas korzystania z naszego klienta, w tle token będzie non stop odnawiany. I do tego właśnie w akcji authorize na sam koniec zwracamy wywołanie akcji refreshToken.

Akcja refreshToken ma za zadanie zweryfikować ile czasu pozostało do wygaśnięcia ważności tokenu accessToken. Jeżeli czas ten jest krótszy niż bufor, token jest odnawiany od razu. Jeżeli do wygaśnięcia zostało więcej czasu to funkcje z opóźnieniem setTimeout ustawiam na tą różnice. Jednocześnie zapisuje identyfikator wywołania tej funkcji w pamięci w naszym magazynie state.remainingTokenTime, aby w każdym momencie, gdy użytkownik postanowi się wylogować lub na nowo spróbować odnowić token, móc wcześniejszą funkcje anulować.

Akcja getNewRefreshToken zawarta wewnątrz powyższej akcji ma za zadanie pozyskać nowe tokeny.

Funkcja getTimeDiff zwraca różnice w czasie na podstawie weryfikacji ważności tokenu oraz aktualnego czasu. Tu również wykorzystałem funkcję biblioteki date-fns.

Powiedzieliśmy o zalogowaniu, o podtrzymywaniu tokenów, a co w sytuacji kiedy użytkownik nie wyloguje się ale zamknie aplikacje? Jak zweryfikować, uruchamiając naszą aplikacje, czy w localStorage znajdują się ważne tokeny i przekierować na odpowiednią podstronę?

Już wyjaśniam. Utworzyłem funkcje inicjalizującą stan naszego klienta, wygląda ona następująco:

Nie robi ona nic specjalnego, czego wcześniej nie omówiliśmy. Weryfikujemy czy token accessToken jest ważny jeżeli tak wywołujemy akcje authorize, która pod spodem ustawia nam na nowo tokeny, stan oraz ustawia odświeżanie tokenu cyklicznie. Jednak jeżeli token dostępu nie istnieje albo jest nieważny, a mamy w pamięci refreshToken, który jest nadal ważny, odnawiamy tokeny i na nowo wywołujemy akcje authorize. Jeżeli obojga tokenów brakuje to funkcja się nie powiedzie i zostanie zwrócony panel logowania.

Aby prawidłowo nam działała funkcja initializationUserAuthentication musimy ją wywołać przed zbudowaniem naszej aplikacji Vue. Dlatego w pliku gdzie definiujemy Vue musimy zmodyfikować kod do takiej struktury.

Kod ten weryfikuje wpierw stan naszego klienta, czy użytkownik jest uwierzytelniony czy nie, a dopiero poźniej buduje naszego klienta.

Powiedzmy sobie jeszcze o akcji do wylogowywania użytkownika.

Początkowo usuwamy tokeny z localStorage, następnie mutacjami zerujemy stan naszego magazynu Vuex oraz zerujemy akcją cleartimeoutboken odnawianie tokenu. Wykonujemy te akcje w taki sposób:

Ostatnim krokiem jest przekierowanie na stronę panelu logowania.

Kiedy usuwać tokeny

Musimy odpowiedzieć sobie na pytanie kiedy tak naprawdę powinniśmy usuwać tokeny? Przecież uwierzytelnienie oparte o JWT opiera się na tokenie i jego ważności. Mam token, który jest ważny? To znaczy, że jestem uwierzytelniony. To prawda, wszystko opiera się w tym wypadku na tokenach.

Wyobraźmy sobie sytuacje, kiedy użytkownik nie wyloguje się, co będzie skutkować pozostawieniem w localStorage tokenów. Dla tokenu dostępu mamy ustawioną ważność na 2 minuty, a token służący do odnawiania tokenów ma ważność jednego miesiąca. Tak więc, jeżeli użytkownik wróci do strony po kilku minutach lub kilku dniach, to i tak wchodząc do aplikacji, aplikacja zweryfikuje ważność tokena do odnawiania naszych tokenów i wygeneruje nowe, więc użytkownik nawet nie zauważy, że był wylogowany.

Dlatego stworzyłem akcje we Vuex dla naszej aplikacji logoutUser, o której mówiłem wyżej. Wywołując ją czyścimy całkowicie stan tokenów w naszym kliencie. To kiedy będziesz odbierał dostęp do serwisu użytkownikowi może zależeć od Ciebie i potrzeby jaką masz. Powyższy problem mógłbyś rozwiązać weryfikując czy użytkownik ma aktywną kartę. Jeżeli karta nie jest aktywna z aplikacją przez więcej niż 15 minut, usuwasz refreshToken, co skutkować będzie niepowodzeniem w odnowieniu tokenów i użytkownik zostanie wylogowany.

Innymi sytuacjami kiedy musimy czyścić tokeny jest niepowodzenie w odnowieniu tokenów, czyli wszystkie zapytania użytkownika, których odpowiedź zwraca błąd 401, no i oczywiście akcja typu Wyloguj.

Obsługa braku autoryzacji, czyli błąd 401

A co w przypadku, jeżeli użytkownik będzie odpytywać serwer o listę użytkowników lub inne dane które wymagają uwierzytelnienia i odpowiedzią będzie błąd 401, czyli braku autoryzacji? Jako twórcy tego mechanizmu musimy być wstanie zareagować na tego typu błędy. Ponieważ moglibyśmy doprowadzić do sytuacji, że użytkownik odpytuje serwer o jakieś informacje chronione, a w odpowiedzi dostaje błąd braku autoryzacji nadal mogąc poruszać się po serwisie.

W naszej prostej aplikacji podczas wystąpienia takiej sytuacji, powinniśmy wylogować całkowicie użytkownika, wyczyścić miejsca, w których przechowujemy tokeny (localStorage, Vuex), usunąć identyfikator metody służącej do odświeżania tokenu oraz przekierować użytkownika na stronę logowania.

I tu przychodzi z pomocą nam Axios. Podczas, gdy defilowaliśmy naszą nową instancje Axios, pominąłem omówienie pewnego fragmentu kodu, dokładnie chodzi mi o ten fragment:

Interceptors pozwala na przechwytywanie żądania lub odpowiedzi, zanim zostaną przetworzone. Dzięki temu jesteśmy w stanie zareagować o wiele wcześniej na zwrócony w odpowiedzi przez serwer błąd. Nasz kod w prosty sposób wychwyci błąd 401 i wywoła akcję logoutUser, która odpowiada za wylogowanie użytkownika, wyczyszczenie localStorage oraz stanu w naszym Vuex.

Obsługa tras

Definiując nasze trasy w aplikacji musimy się skupić na tym do jakich zawartości użytkownik ma mieć dostęp jako nieuwierzytelniony oraz do jakiej powinien mieć dostęp po prawidłowym uwierzytelnieniu. Do takiej weryfikacji posłużymy się middlewarem.

Middleware to oprogramowanie pośrednie wykorzystywane w routingu dla Vue. Dzięki oprogramowaniu pośredniemu jesteśmy wstanie w łatwy sposób zdefiniować jemu funkcje i dzięki jej na przykład wykonać takie czynności, jak wymaganie uwierzytelnienia użytkownika na niektórych trasach.

Ja w katalogu projektu utworzyłem sobie folder ‚middleware’ i zdefiniowałem w nim dwa pliki auth.js oraz noAuth.js. Ich zawartość jest następująca:

Wykorzystuje tutaj funkcje do weryfikacji ważności i zgodności tokenu isValidAccessToken, o której mowa była wcześniej.

Teraz wystarczy dodać do naszej konfiguracji tras w aplikacji pliki które zdefiniowaliśmy powyżej.

Jak możesz zobaczyć w przykładzie, użyliśmy beforeEnter. Są to strażnicy na trasie dostarczeni przez vue-router, podczas próby przejścia na konkretną trasę, wykona on fragment kodu który zostanie do niego przypisany.

Jeżeli kod się powiedzie, czyli token zostanie zweryfikowany prawidłowo, uzyskasz dostęp do odpowiedniego zasobu. Jeżeli weryfikacja się nie powiedzie, zostaniesz przekierowany na stronę logowania.

Podsumowanie

Mam nadzieje, że postawione przeze mnie pytania oraz odpowiedzi uzupełnione kodem aplikacji, którą znajdziesz na GitHub, były wystarczająco wytłumaczone, abyś był wstanie wzorując się na tym artykule podjąć temat implementacji takiego mechanizmu we własnym kliencie. Ewentualnie, abyś mógł wykorzystać zarówno API jak i klienta do stworzenia własnej aplikacji z koniecznością uwierzytelnienia użytkownika. Tak jak mówiłem na początku, musiałem poczynić pewne założenia, dlatego założyłem, że odbiorcą będzie ktoś kto pracował z Vue. Cieszę się że mogłem podzielić się tym tematem z Tobą drogi czytelniku oraz przedstawić Ci własny pomysł na uwierzytelnienie użytkownika oparte o tokeny JWT.

Jeszcze raz polecam zaglądnąć do repozytorium z projektem. Znajdziesz tam kod API oraz Klienta, tematy pierwszej jak i drugiej części artykułu. Pozwoliłem sobie urozmaicić aplikację klienta o wyświetlanie listy użytkowników pobieranych z naszej bazy oraz o tworzenie nowych użytkowników. Życzę Ci miłej zabawy oraz powodzenia w poszerzaniu wiedzy 😉

unity

unity

Dołącz do naszego zespołu już dziś

Sprawdź kogo szukamy!

Wyrażam zgodę na przetwarzanie danych osobowych na zasadach określonych w polityce prywatności. Jeśli nie wyrażasz zgody na wykorzystywanie cookies we wskazanych w niej celach, w tym do profilowania, prosimy o wyłącznie cookies w przeglądarce lub opuszczenie serwisu. więcej

Akceptuj