JSON Web Token Tutorial: przykład w Laravel i AngularJS
Opublikowany: 2022-03-11Wraz z rosnącą popularnością aplikacji jednostronicowych, aplikacji mobilnych i usług RESTful API, sposób, w jaki twórcy stron internetowych piszą kod zaplecza, znacznie się zmienił. Dzięki technologiom takim jak AngularJS i BackboneJS nie spędzamy już dużo czasu na tworzeniu znaczników, zamiast tego budujemy interfejsy API, z których korzystają nasze aplikacje front-endowe. Nasz back-end to bardziej logika biznesowa i dane, podczas gdy logika prezentacji jest przeniesiona wyłącznie do aplikacji front-endowych lub mobilnych. Zmiany te doprowadziły do nowych sposobów implementacji uwierzytelniania w nowoczesnych aplikacjach.
Uwierzytelnianie jest jedną z najważniejszych części każdej aplikacji internetowej. Przez dziesięciolecia najłatwiejszym rozwiązaniem były pliki cookie i uwierzytelnianie na serwerze. Jednak obsługa uwierzytelniania w nowoczesnych aplikacjach mobilnych i jednostronicowych może być trudna i wymagać lepszego podejścia. Najbardziej znanymi rozwiązaniami problemów z uwierzytelnianiem dla interfejsów API są OAuth 2.0 i JSON Web Token (JWT).
Zanim przejdziemy do tego samouczka JSON Web Token, czym dokładnie jest JWT?
Co to jest token sieciowy JSON?
Token sieciowy JSON służy do wysyłania informacji, które można zweryfikować i którym można zaufać za pomocą podpisu cyfrowego. Składa się z kompaktowego i bezpiecznego dla adresu URL obiektu JSON, który jest podpisany kryptograficznie w celu zweryfikowania jego autentyczności i który można również zaszyfrować, jeśli ładunek zawiera poufne informacje.
Ze względu na swoją zwartą strukturę token JWT jest zwykle używany w nagłówkach Authorization
HTTP lub parametrach zapytań URL.
Struktura tokena internetowego JSON
JWT jest reprezentowany jako sekwencja wartości zakodowanych w standardzie base64url, które są oddzielone znakami kropki.
Oto przykład tokena JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0 . yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
nagłówek
Nagłówek zawiera metadane tokena i zawiera minimalnie typ podpisu oraz algorytm szyfrowania. (Możesz użyć narzędzia do formatowania JSON, aby upiększyć obiekt JSON).
Przykładowy nagłówek
{ "alg": "HS256", "typ": "JWT" }
Ten przykładowy nagłówek JWT deklaruje, że zakodowany obiekt jest tokenem sieciowym JSON i że jest podpisany przy użyciu algorytmu HMAC SHA-256.
Po zakodowaniu w base64 mamy pierwszą część naszego JWT.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Ładunek (roszczenia)
W kontekście JWT oświadczenie można zdefiniować jako oświadczenie dotyczące encji (zazwyczaj użytkownika), a także dodatkowe metadane dotyczące samego tokena. Roszczenie zawiera informacje, które chcemy przesłać i których serwer może użyć do prawidłowej obsługi uwierzytelniania JSON Web Token. Istnieje wiele roszczeń, które możemy dostarczyć; obejmują one zarejestrowane nazwy roszczeń, publiczne nazwy roszczeń i prywatne nazwy roszczeń.
Zarejestrowane roszczenia JWT
Są to oświadczenia zarejestrowane w rejestrze IANA JSON Web Token Claims. Te oświadczenia JWT nie mają być obowiązkowe, ale raczej stanowią punkt wyjścia dla zestawu użytecznych, interoperacyjnych oświadczeń.
Obejmują one:
- iss : wystawca tokena
- sub : Temat tokena
- aud : publiczność tokena
- exp : czas wygaśnięcia JWT zdefiniowany w czasie uniksowym
- nbf : „Nie wcześniej” czas określający czas, przed którym token JWT nie może zostać przyjęty do przetwarzania
- iat : czas „wystawiony w”, w czasie uniksowym, w którym token został wystawiony
- jti : żądanie identyfikatora JWT zapewnia unikalny identyfikator dla JWT
Roszczenia publiczne
Oświadczenia publiczne muszą mieć nazwy odporne na kolizje. Dzięki nadaniu nazwy identyfikatorowi URI lub URN unika się kolizji nazw dla tokenów JWT, w których nadawca i odbiorca nie są częścią zamkniętej sieci.
Przykładową nazwą oświadczenia publicznego może być: https://www.toptal.com/jwt_claims/is_admin
, a najlepszą praktyką jest umieszczenie w tej lokalizacji pliku opisującego oświadczenie, aby można było je usunąć w celu dokumentacji.
Prywatne roszczenia
Prywatne nazwy roszczeń mogą być używane w miejscach, w których tokeny JWT są wymieniane tylko w zamkniętym środowisku między znanymi systemami, na przykład wewnątrz przedsiębiorstwa. Są to twierdzenia, które możemy zdefiniować sami, takie jak identyfikatory użytkowników, role użytkowników lub jakiekolwiek inne informacje.
Używanie nazw roszczeń, które mogą mieć sprzeczne znaczenia semantyczne poza systemem zamkniętym lub prywatnym, może kolidować, więc używaj ich ostrożnie.
Należy pamiętać, że chcemy, aby token sieciowy był jak najmniejszy, więc używaj tylko niezbędnych danych w oświadczeniach publicznych i prywatnych.
Przykładowy ładunek JWT
{ "iss": "toptal.com", "exp": 1426420800, "https://www.toptal.com/jwt_claims/is_admin": true, "company": "Toptal", "awesome": true }
Ten przykładowy ładunek ma dwa zarejestrowane roszczenia, jedno publiczne i dwa prywatne. Po zakodowaniu w base64 mamy drugą część naszego JWT.
eyJpc3MiOiJ0b3B0YWwuY29tIiwiZXhwIjoxNDI2NDIwODAwLCJodHRwOi8vdG9wdGFsLmNvbS9qd3RfY2xhaW1zL2lzX2FkbWluIjp0cnVlLCJjb21wYW55IjoiVG9wdGFsIiwiYXdlc29tZSI6dHJ1ZX0
Podpis
Standard JWT jest zgodny ze specyfikacją JSON Web Signature (JWS) w celu wygenerowania końcowego podpisanego tokenu. Jest generowany przez połączenie zakodowanego nagłówka JWT i zakodowanego ładunku JWT oraz podpisanie go przy użyciu silnego algorytmu szyfrowania, takiego jak HMAC SHA-256. Sekretny klucz podpisu jest przechowywany przez serwer, dzięki czemu będzie mógł weryfikować istniejące tokeny i podpisywać nowe.
$encodedContent = base64UrlEncode(header) + "." + base64UrlEncode(payload); $signature = hashHmacSHA256($encodedContent);
To daje nam ostatnią część naszego JWT.
yRQYnWzskCZUxPwaQupWkiUzKELZ49eM7oWxAQK_ZXw
Bezpieczeństwo i szyfrowanie JWT
Bardzo ważne jest, aby używać protokołu TLS/SSL w połączeniu z JWT, aby zapobiec atakom typu man-in-the-middle. W większości przypadków wystarczy to do zaszyfrowania ładunku JWT, jeśli zawiera on poufne informacje. Jeśli jednak chcemy dodać dodatkową warstwę ochrony, możemy zaszyfrować sam ładunek JWT za pomocą specyfikacji JSON Web Encryption (JWE).
Oczywiście, jeśli chcemy uniknąć dodatkowego obciążenia związanego z używaniem JWE, inną opcją jest po prostu przechowywanie poufnych informacji w naszej bazie danych i używanie naszego tokena do dodatkowych wywołań API do serwera, gdy potrzebujemy uzyskać dostęp do poufnych danych.
Dlaczego potrzebne są tokeny internetowe?
Zanim zobaczymy wszystkie korzyści płynące z używania uwierzytelniania JWT, musimy przyjrzeć się temu, w jaki sposób uwierzytelnianie odbywało się w przeszłości.
Uwierzytelnianie na serwerze
Ponieważ protokół HTTP jest bezstanowy, musi istnieć mechanizm przechowywania informacji o użytkowniku oraz sposób uwierzytelniania użytkownika przy każdym kolejnym żądaniu po zalogowaniu. Większość stron internetowych wykorzystuje pliki cookie do przechowywania identyfikatora sesji użytkownika.
Jak to działa
Przeglądarka wysyła do serwera żądanie POST zawierające identyfikator i hasło użytkownika. Serwer odpowiada za pomocą pliku cookie, który jest ustawiany w przeglądarce użytkownika i zawiera identyfikator sesji w celu identyfikacji użytkownika.
Przy każdym kolejnym żądaniu serwer musi znaleźć tę sesję i zdeserializować ją, ponieważ dane użytkownika są przechowywane na serwerze.
Wady uwierzytelniania opartego na serwerze
Trudno skalować : serwer musi utworzyć sesję dla użytkownika i zachować ją gdzieś na serwerze. Można to zrobić w pamięci lub w bazie danych. Jeśli mamy system rozproszony, musimy upewnić się, że korzystamy z oddzielnego magazynu sesji, który nie jest połączony z serwerem aplikacji.
Udostępnianie żądań między źródłami (CORS) : podczas korzystania z wywołań AJAX do pobrania zasobu z innej domeny („cross-origin”) możemy napotkać problemy z zabronionymi żądaniami, ponieważ domyślnie żądania HTTP nie zawierają plików cookie w żądania pochodzenia.
Łączenie z frameworkiem sieciowym : Używając uwierzytelniania opartego na serwerze, jesteśmy związani ze schematem uwierzytelniania naszego frameworka. Udostępnianie danych sesji między różnymi platformami internetowymi napisanymi w różnych językach programowania jest naprawdę trudne, a nawet niemożliwe.
Uwierzytelnianie oparte na tokenach
Uwierzytelnianie oparte na tokenie/JWT jest bezstanowe, więc nie ma potrzeby przechowywania informacji o użytkowniku w sesji. Daje nam to możliwość skalowania naszej aplikacji bez martwienia się, gdzie użytkownik się zalogował. Możemy z łatwością użyć tego samego tokena do pobrania bezpiecznego zasobu z domeny innej niż ta, do której jesteśmy zalogowani.
Jak działają tokeny internetowe JSON
Przeglądarka lub klient mobilny wysyła żądanie do serwera uwierzytelniania, zawierające dane logowania użytkownika. Serwer uwierzytelniania generuje nowy token dostępu JWT i zwraca go do klienta. Przy każdym żądaniu do zasobu z ograniczeniami klient wysyła token dostępu w ciągu zapytania lub nagłówku Authorization
. Następnie serwer sprawdza poprawność tokenu i, jeśli jest poprawny, zwraca klientowi bezpieczny zasób.
Serwer uwierzytelniania może podpisać token przy użyciu dowolnej bezpiecznej metody podpisu. Na przykład algorytm klucza symetrycznego, taki jak HMAC SHA-256, może być używany, jeśli istnieje bezpieczny kanał do udostępniania tajnego klucza wszystkim stronom. Alternatywnie można również zastosować asymetryczny system klucza publicznego, taki jak RSA, eliminując potrzebę dalszego udostępniania kluczy.
Zalety uwierzytelniania opartego na tokenach
Bezstanowy, łatwiejszy do skalowania : token zawiera wszystkie informacje umożliwiające identyfikację użytkownika, eliminując potrzebę stanu sesji. Jeśli korzystamy z load balancera, możemy przekazać użytkownika na dowolny serwer, zamiast być związanym z tym samym serwerem, na którym się zalogowaliśmy.
Ponowne wykorzystanie: możemy mieć wiele oddzielnych serwerów działających na wielu platformach i domenach, ponownie wykorzystując ten sam token do uwierzytelniania użytkownika. Łatwo jest zbudować aplikację, która współdzieli uprawnienia z inną aplikacją.
Zabezpieczenia JWT : Ponieważ nie używamy plików cookie, nie musimy chronić przed atakami typu cross-site request forgering (CSRF). Powinniśmy nadal szyfrować nasze tokeny za pomocą JWE, jeśli musimy umieścić w nich jakiekolwiek poufne informacje, i przesyłać nasze tokeny przez HTTPS, aby zapobiec atakom typu man-in-the-middle.
Wydajność : nie ma wyszukiwania po stronie serwera w celu znalezienia i deserializacji sesji przy każdym żądaniu. Jedyne, co musimy zrobić, to obliczyć HMAC SHA-256, aby zweryfikować token i przeanalizować jego zawartość.
Przykład tokena internetowego JSON przy użyciu Laravel 5 i AngularJS
W tym samouczku JWT zademonstruję, jak zaimplementować podstawowe uwierzytelnianie za pomocą JSON Web Tokens w dwóch popularnych technologiach internetowych: Laravel 5 dla kodu backendu i AngularJS dla przykładu frontend Single Page Application (SPA). (Możesz znaleźć całe demo tutaj, a kod źródłowy w tym repozytorium GitHub, dzięki czemu możesz śledzić wraz z samouczkiem).
Ten przykład tokena internetowego JSON nie będzie używać żadnego rodzaju szyfrowania, aby zapewnić poufność informacji przesyłanych w oświadczeniach. W praktyce jest to często w porządku, ponieważ protokół TLS/SSL szyfruje żądanie. Jeśli jednak token będzie zawierał poufne informacje, takie jak numer PESEL użytkownika, należy go również zaszyfrować za pomocą JWE.
Przykład zaplecza Laravel
Wykorzystamy Laravel do obsługi rejestracji użytkowników, utrwalania danych użytkownika w bazie danych i dostarczania pewnych zastrzeżonych danych, które wymagają uwierzytelnienia, aby aplikacja Angular mogła je wykorzystać. Stworzymy również przykładową subdomenę API do symulacji współdzielenia zasobów między źródłami (CORS).
Instalacja i uruchamianie projektu
Aby korzystać z Laravela, musimy zainstalować na naszym komputerze menedżera pakietów Composer. Podczas tworzenia w Laravel zalecam korzystanie z zapakowanego w Laravel Homestead „pudełka” Vagranta. Zapewnia nam kompletne środowisko programistyczne niezależnie od naszego systemu operacyjnego.
Najłatwiejszym sposobem na załadowanie naszej aplikacji JWT Laravel jest użycie pakietu Composer Laravel Installer.
composer global require "laravel/installer=~1.1"
Teraz wszyscy jesteśmy gotowi do stworzenia nowego projektu Laravel, uruchamiając laravel new jwt
.
Jeśli masz jakiekolwiek pytania dotyczące tego procesu, zapoznaj się z oficjalną dokumentacją Laravela.
Po utworzeniu podstawowej aplikacji Laravel 5 musimy skonfigurować nasz Homestead.yaml
, który skonfiguruje mapowania folderów i konfigurację domen dla naszego lokalnego środowiska.
Przykład pliku Homestead.yaml
:
--- ip: "192.168.10.10" memory: 2048 cpus: 1 authorize: /Users/ttkalec/.ssh/public.psk keys: - /Users/ttkalec/.ssh/private.ppk folders: - map: /coding/jwt to: /home/vagrant/coding/jwt sites: - map: jwt.dev to: /home/vagrant/coding/jwt/public - map: api.jwt.dev to: /home/vagrant/coding/jwt/public variables: - key: APP_ENV value: local
Po uruchomieniu naszego komputera Vagrant za pomocą polecenia vagrant up
i zalogowaniu się do niego za pomocą vagrant ssh
, przechodzimy do wcześniej zdefiniowanego katalogu projektu. W powyższym przykładzie będzie to /home/vagrant/coding/jwt
. Możemy teraz uruchomić polecenie php artisan migrate
, aby utworzyć niezbędne tabele użytkowników w naszej bazie danych.
Instalowanie zależności kompozytora
Na szczęście istnieje społeczność programistów pracujących nad Laravelem i utrzymujących wiele świetnych pakietów, które możemy ponownie wykorzystać i rozszerzyć naszą aplikację. W tym przykładzie użyjemy tymon/jwt-auth
, autorstwa Seana Tymona, do obsługi tokenów po stronie serwera, oraz barryvdh/laravel-cors
, autorstwa Barry'ego vd. Heuvel, do obsługi CORS.
jwt-auth
Wymagaj tymon/jwt-auth
w naszym composer.json
i zaktualizuj nasze zależności.
composer require tymon/jwt-auth 0.5.*
Dodaj JWTAuthServiceProvider
do naszej tablicy dostawców app/config/app.php
.
'Tymon\JWTAuth\Providers\JWTAuthServiceProvider'
Następnie w pliku app/config/app.php
pod tablicą aliases
dodajemy fasadę JWTAuth
.
'JWTAuth' => 'Tymon\JWTAuth\Facades\JWTAuth'
Na koniec będziemy chcieli opublikować konfigurację pakietu za pomocą następującego polecenia: php artisan config:publish tymon/jwt-auth
Tokeny internetowe JSON są szyfrowane przy użyciu tajnego klucza. Możemy wygenerować ten klucz za pomocą polecenia php artisan jwt:generate
. Zostanie on umieszczony w naszym pliku config/jwt.php
. Jednak w środowisku produkcyjnym nigdy nie chcemy, aby nasze hasła lub klucze API znajdowały się w plikach konfiguracyjnych. Zamiast tego powinniśmy umieścić je wewnątrz zmiennych środowiskowych serwera i odwołać się do nich w pliku konfiguracyjnym za pomocą funkcji env
. Na przykład:
'secret' => env('JWT_SECRET')
Możemy dowiedzieć się więcej o tym pakiecie i wszystkich jego ustawieniach konfiguracyjnych na Github.
laravel-cors
Wymagaj barryvdh/laravel-cors
w naszym composer.json
i zaktualizuj nasze zależności.
composer require barryvdh/laravel-cors 0.4.x@dev
Dodaj CorsServiceProvider
do naszej tablicy dostawców app/config/app.php
.
'Barryvdh\Cors\CorsServiceProvider'
Następnie dodaj oprogramowanie pośredniczące do naszego app/Http/Kernel.php
.
'Barryvdh\Cors\Middleware\HandleCors'
Opublikuj konfigurację w lokalnym pliku config/cors.php
za pomocą polecenia php artisan vendor:publish
.

Przykład konfiguracji pliku cors.php
:
return [ 'defaults' => [ 'supportsCredentials' => false, 'allowedOrigins' => [], 'allowedHeaders' => [], 'allowedMethods' => [], 'exposedHeaders' => [], 'maxAge' => 0, 'hosts' => [], ], 'paths' => [ 'v1/*' => [ 'allowedOrigins' => ['*'], 'allowedHeaders' => ['*'], 'allowedMethods' => ['*'], 'maxAge' => 3600, ], ], ];
Routing i obsługa żądań HTTP
W trosce o zwięzłość umieszczę cały mój kod w pliku Routes.php odpowiedzialnym za routing i delegowanie żądań Laravel do kontrolerów. Zwykle tworzymy dedykowane kontrolery do obsługi wszystkich naszych żądań HTTP i utrzymujemy nasz kod w sposób modularny i czysty.
Załadujemy nasz widok AngularJS SPA za pomocą
Route::get('/', function () { return view('spa'); });
Rejestracja Użytkownika
Kiedy wysyłamy żądanie POST
do /signup
z nazwą użytkownika i hasłem, spróbujemy utworzyć nowego użytkownika i zapisać go w bazie danych. Po utworzeniu użytkownika tworzony jest token JWT i zwracany za pośrednictwem odpowiedzi JSON.
Route::post('/signup', function () { $credentials = Input::only('email', 'password'); try { $user = User::create($credentials); } catch (Exception $e) { return Response::json(['error' => 'User already exists.'], HttpResponse::HTTP_CONFLICT); } $token = JWTAuth::fromUser($user); return Response::json(compact('token')); });
Logowanie użytkownika
Gdy wysyłamy żądanie POST
do /signin
z nazwą użytkownika i hasłem, weryfikujemy, czy użytkownik istnieje i zwraca JWT za pośrednictwem odpowiedzi JSON.
Route::post('/signin', function () { $credentials = Input::only('email', 'password'); if ( ! $token = JWTAuth::attempt($credentials)) { return Response::json(false, HttpResponse::HTTP_UNAUTHORIZED); } return Response::json(compact('token')); });
Pobieranie zasobu z ograniczeniami w tej samej domenie
Gdy użytkownik się zaloguje, możemy pobrać ograniczony zasób. Utworzyłem trasę /restricted
, która symuluje zasób, który wymaga uwierzytelnionego użytkownika. W tym celu nagłówek Authorization
żądania lub ciąg zapytania musi zawierać token JWT do weryfikacji przez backend.
Route::get('/restricted', [ 'before' => 'jwt-auth', function () { $token = JWTAuth::getToken(); $user = JWTAuth::toUser($token); return Response::json([ 'data' => [ 'email' => $user->email, 'registered_at' => $user->created_at->toDateTimeString() ] ]); } ]);
W tym przykładzie używam oprogramowania pośredniczącego jwt-auth
dostarczonego w jwt-auth
przy użyciu 'before' => 'jwt-auth'
. To oprogramowanie pośredniczące służy do filtrowania żądania i sprawdzania poprawności tokenu JWT. Jeśli token jest nieprawidłowy, nieobecny lub wygasł, oprogramowanie pośredniczące zgłosi wyjątek, który możemy przechwycić.
W Laravelu 5 wyjątki możemy łapać za pomocą pliku app/Exceptions/Handler.php
. Korzystając z funkcji render
możemy tworzyć odpowiedzi HTTP na podstawie wyrzuconego wyjątku.
public function render($request, Exception $e) { if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenInvalidException) { return response(['Token is invalid'], 401); } if ($e instanceof \Tymon\JWTAuth\Exceptions\TokenExpiredException) { return response(['Token has expired'], 401); } return parent::render($request, $e); }
Jeśli użytkownik jest uwierzytelniony, a token jest ważny, możemy bezpiecznie zwrócić zastrzeżone dane do frontendu za pomocą JSON.
Pobieranie zasobów z ograniczeniami z poddomeny API
W następnym przykładzie tokena internetowego JSON przyjmiemy inne podejście do walidacji tokenu. Zamiast używać oprogramowania pośredniczącego jwt-auth
, będziemy obsługiwać wyjątki ręcznie. Gdy wysyłamy żądanie POST
do serwera API api.jwt.dev/v1/restricted
, tworzymy żądanie cross-origin i musimy włączyć CORS na zapleczu. Na szczęście skonfigurowaliśmy już CORS w pliku config/cors.php
.
Route::group(['domain' => 'api.jwt.dev', 'prefix' => 'v1'], function () { Route::get('/restricted', function () { try { JWTAuth::parseToken()->toUser(); } catch (Exception $e) { return Response::json(['error' => $e->getMessage()], HttpResponse::HTTP_UNAUTHORIZED); } return ['data' => 'This has come from a dedicated API subdomain with restricted access.']; }); });
Przykład interfejsu AngularJS
Używamy AngularJS jako front-end, opierając się na wywołaniach API do serwera uwierzytelniania zaplecza Laravel w celu uwierzytelnienia użytkownika i przykładowych danych, a także serwera API dla przykładowych danych z różnych źródeł. Gdy przejdziemy do strony głównej naszego projektu, backend będzie obsługiwał resources/views/spa.blade.php
, który załaduje aplikację Angular.
Oto struktura folderów aplikacji Angular:
public/ |-- css/ `-- bootstrap.superhero.min.css |-- lib/ |-- loading-bar.css |-- loading-bar.js `-- ngStorage.js |-- partials/ |-- home.html |-- restricted.html |-- signin.html `-- signup.html `-- scripts/ |-- app.js |-- controllers.js `-- services.js
Uruchamianie aplikacji Angular
spa.blade.php
zawiera podstawowe elementy potrzebne do uruchomienia aplikacji. Do stylizacji użyjemy Twitter Bootstrap, a także niestandardowego motywu z Bootswatch. Aby uzyskać wizualne informacje zwrotne podczas wykonywania wywołania AJAX, użyjemy skryptu angular-loading-bar, który przechwytuje żądania XHR i tworzy pasek ładowania. W sekcji nagłówka mamy następujące arkusze stylów:
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/bootstrap.superhero.min.css"> <link rel="stylesheet" href="/lib/loading-bar.css">
Stopka naszego znacznika zawiera odniesienia do bibliotek, a także nasze niestandardowe skrypty dla modułów, kontrolerów i usług Angular.
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-route.min.js"></script> <script src="/lib/ngStorage.js"></script> <script src="/lib/loading-bar.js"></script> <script src="/scripts/app.js"></script> <script src="/scripts/controllers.js"></script> <script src="/scripts/services.js"></script> </body>
Używamy biblioteki ngStorage
dla AngularJS, aby zapisywać tokeny w lokalnej pamięci przeglądarki, dzięki czemu możemy wysyłać je przy każdym żądaniu za pośrednictwem nagłówka Authorization
.
W środowisku produkcyjnym oczywiście zminimalizujemy i połączymy wszystkie nasze pliki skryptów i arkusze stylów w celu poprawy wydajności.
Stworzyłem pasek nawigacyjny za pomocą Bootstrapa, który zmieni widoczność odpowiednich linków w zależności od statusu zalogowania użytkownika. Status logowania jest określany przez obecność zmiennej token
w zasięgu kontrolera.
<div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">JWT Angular example</a> </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav navbar-right"> <li data-ng-show="token"><a ng-href="#/restricted">Restricted area</a></li> <li data-ng-hide="token"><a ng-href="#/signin">Sign in</a></li> <li data-ng-hide="token"><a ng-href="#/signup">Sign up</a></li> <li data-ng-show="token"><a ng-click="logout()">Logout</a></li> </ul> </div>
Rozgromienie
Mamy plik o nazwie app.js
, który jest odpowiedzialny za konfigurację wszystkich naszych tras frontonu.
angular.module('app', [ 'ngStorage', 'ngRoute', 'angular-loading-bar' ]) .constant('urls', { BASE: 'http://jwt.dev:8000', BASE_API: 'http://api.jwt.dev:8000/v1' }) .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $routeProvider. when('/', { templateUrl: 'partials/home.html', controller: 'HomeController' }). when('/signin', { templateUrl: 'partials/signin.html', controller: 'HomeController' }). when('/signup', { templateUrl: 'partials/signup.html', controller: 'HomeController' }). when('/restricted', { templateUrl: 'partials/restricted.html', controller: 'RestrictedController' }). otherwise({ redirectTo: '/' });
Tutaj widzimy, że zdefiniowaliśmy cztery trasy, które są obsługiwane przez HomeController
lub RestrictedController
. Każda trasa odpowiada częściowemu widokowi HTML. Zdefiniowaliśmy również dwie stałe, które zawierają adresy URL dla naszych żądań HTTP do zaplecza.
Poproś o przechwytywanie
Usługa $http AngularJS pozwala nam komunikować się z backendem i wykonywać żądania HTTP. W naszym przypadku chcemy przechwycić każde żądanie HTTP i wstrzyknąć mu nagłówek Authorization
zawierający nasz JWT, jeśli użytkownik jest uwierzytelniony. Możemy również użyć interceptora do stworzenia globalnego modułu obsługi błędów HTTP. Oto przykład naszego przechwytywacza, który wstrzykuje token, jeśli jest dostępny w lokalnej pamięci przeglądarki.
$httpProvider.interceptors.push(['$q', '$location', '$localStorage', function ($q, $location, $localStorage) { return { 'request': function (config) { config.headers = config.headers || {}; if ($localStorage.token) { config.headers.Authorization = 'Bearer ' + $localStorage.token; } return config; }, 'responseError': function (response) { if (response.status === 401 || response.status === 403) { $location.path('/signin'); } return $q.reject(response); } }; }]);
Kontrolery
W pliku controllers.js
zdefiniowaliśmy dwa kontrolery dla naszej aplikacji: HomeController
i RestrictedController
. HomeController
obsługuje funkcje logowania, rejestracji i wylogowania. Przekazuje nazwę użytkownika i hasło z formularzy logowania i rejestracji do usługi Auth
, która wysyła żądania HTTP do zaplecza. Następnie zapisuje token w pamięci lokalnej lub wyświetla komunikat o błędzie, w zależności od odpowiedzi z zaplecza.
angular.module('app') .controller('HomeController', ['$rootScope', '$scope', '$location', '$localStorage', 'Auth', function ($rootScope, $scope, $location, $localStorage, Auth) { function successAuth(res) { $localStorage.token = res.token; window.location = "/"; } $scope.signin = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signin(formData, successAuth, function () { $rootScope.error = 'Invalid credentials.'; }) }; $scope.signup = function () { var formData = { email: $scope.email, password: $scope.password }; Auth.signup(formData, successAuth, function () { $rootScope.error = 'Failed to signup'; }) }; $scope.logout = function () { Auth.logout(function () { window.location = "/" }); }; $scope.token = $localStorage.token; $scope.tokenClaims = Auth.getTokenClaims(); }])
RestrictedController
zachowuje się w ten sam sposób, tylko pobiera dane za pomocą funkcji getRestrictedData
i getApiData
w usłudze Data
.
.controller('RestrictedController', ['$rootScope', '$scope', 'Data', function ($rootScope, $scope, Data) { Data.getRestrictedData(function (res) { $scope.data = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted content.'; }); Data.getApiData(function (res) { $scope.api = res.data; }, function () { $rootScope.error = 'Failed to fetch restricted API content.'; }); }]);
Backend jest odpowiedzialny za udostępnianie danych objętych ograniczeniami tylko wtedy, gdy użytkownik jest uwierzytelniony. Oznacza to, że aby odpowiedzieć z danymi objętymi ograniczeniami, żądanie dotyczące tych danych musi zawierać poprawny token JWT w nagłówku Authorization
lub ciągu zapytania. Jeśli tak nie jest, serwer odpowie kodem stanu 401 Nieautoryzowany błąd.
Usługa uwierzytelniania
Usługa Auth jest odpowiedzialna za logowanie i rejestrację żądań HTTP do zaplecza. Jeśli żądanie się powiedzie, odpowiedź zawiera podpisany token, który jest następnie dekodowany w standardzie base64, a dołączone informacje o oświadczeniach tokenu są zapisywane w zmiennej tokenClaims
. Jest to przekazywane do kontrolera za pomocą funkcji getTokenClaims
.
angular.module('app') .factory('Auth', ['$http', '$localStorage', 'urls', function ($http, $localStorage, urls) { function urlBase64Decode(str) { var output = str.replace('-', '+').replace('_', '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw 'Illegal base64url string!'; } return window.atob(output); } function getClaimsFromToken() { var token = $localStorage.token; var user = {}; if (typeof token !== 'undefined') { var encoded = token.split('.')[1]; user = JSON.parse(urlBase64Decode(encoded)); } return user; } var tokenClaims = getClaimsFromToken(); return { signup: function (data, success, error) { $http.post(urls.BASE + '/signup', data).success(success).error(error) }, signin: function (data, success, error) { $http.post(urls.BASE + '/signin', data).success(success).error(error) }, logout: function (success) { tokenClaims = {}; delete $localStorage.token; success(); }, getTokenClaims: function () { return tokenClaims; } }; } ]);
Usługa danych
Jest to prosta usługa, która wysyła żądania do serwera uwierzytelniania, a także do serwera API, w celu uzyskania niektórych danych z ograniczeniami fikcyjnymi. Tworzy żądanie i deleguje wywołania zwrotne sukcesu i błędów do kontrolera.
angular.module('app') .factory('Data', ['$http', 'urls', function ($http, urls) { return { getRestrictedData: function (success, error) { $http.get(urls.BASE + '/restricted').success(success).error(error) }, getApiData: function (success, error) { $http.get(urls.BASE_API + '/restricted').success(success).error(error) } }; } ]);
Poza tym samouczkiem JSON Web Token
Uwierzytelnianie oparte na tokenach umożliwia nam konstruowanie systemów oddzielonych od siebie, które nie są powiązane z określonym schematem uwierzytelniania. Token może być generowany w dowolnym miejscu i używany w dowolnym systemie, który używa tego samego tajnego klucza do podpisywania tokena. Są przystosowane do urządzeń mobilnych i nie wymagają od nas używania plików cookie.
Tokeny internetowe JSON działają we wszystkich popularnych językach programowania i szybko zyskują na popularności. Są wspierani przez firmy takie jak Google, Microsoft i Zendesk. Ich standardowa specyfikacja opracowana przez Internet Engineering Task Force (IETF) jest nadal w wersji roboczej i może ulec niewielkim zmianom w przyszłości.
Nadal jest wiele do omówienia na temat tokenów JWT, takich jak obsługa szczegółów zabezpieczeń i odświeżanie tokenów po ich wygaśnięciu, ale samouczek JSON Web Token powinien zademonstrować podstawowe użycie i, co ważniejsze, zalety korzystania z tokenów JWT.
Dalsza lektura na blogu Toptal Engineering:
- Budowanie interfejsu API REST Node.js/TypeScript, część 3: MongoDB, uwierzytelnianie i testy automatyczne