Autenticazione utente completa e controllo degli accessi: un tutorial sul passaporto Laravel, pt. 1

Pubblicato: 2022-03-11

Quando si sviluppa un'applicazione Web, è generalmente una buona idea dividerla in due livelli. Un'API di livello intermedio interagisce con il database e un livello Web in genere è costituito da una SPA o MPA front-end. In questo modo, un'applicazione Web è accoppiata in modo più lasco, semplificando la gestione e il debug a lungo termine.

Una volta creata l'API, la configurazione dell'autenticazione e dello stato in un contesto API senza stato potrebbe sembrare alquanto problematica.

In questo articolo, vedremo come implementare l'autenticazione utente completa e una semplice forma di controllo dell'accesso in un'API utilizzando Laravel e Passport. Dovresti avere esperienza di lavoro con Laravel poiché questo non è un tutorial introduttivo.

Prerequisiti per l'installazione:

  • PHP 7+, MySQL e Apache (gli sviluppatori che desiderano installarli tutti e tre contemporaneamente possono utilizzare XAMPP.)
  • Compositore
  • Laravel 7
  • Passaporto Laravel. Poiché le API sono generalmente stateless e non utilizzano sessioni, generalmente utilizziamo i token per mantenere lo stato tra le richieste. Laravel utilizza la libreria Passport per implementare un server OAuth2 completo che possiamo utilizzare per l'autenticazione nella nostra API.
  • Postino, cURL o Insomnia per testare l'API: dipende dalle preferenze personali
  • Editor di testo a tua scelta
  • Helper Laravel (per Laravel 6.0 e versioni successive): dopo aver installato Laravel e Passport, esegui semplicemente:
 composer require laravel/helpers

Con quanto sopra installato, siamo pronti per iniziare. Assicurati di configurare la connessione al database modificando il file .env .

Tutorial Laravel Passport, Passaggio 1: aggiungere un controller e un modello per le richieste fittizie

Innanzitutto, creeremo un controller e un modello per le richieste fittizie. Il modello non sarà di grande utilità in questo tutorial, è solo per dare un'idea dei dati che il controller è destinato a manipolare.

Prima di creare il modello e il controller, è necessario creare una migrazione. In una finestra di terminale o cmd.exe , se stai utilizzando Windows, esegui:

 php artisan make:migration create_articles_table --create=articles

Ora vai alla cartella database/migrations e apri il file con un nome simile a xxxx_xx_xx_xxxxxx_create_articles_table.php .

Nella funzione up della classe, scriveremo questo:

 Schema::create('articles', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->string('body'); $table->integer('user_id'); $table->timestamps(); });

Successivamente, creeremo un modello di Article . Per farlo, esegui:

 php artisan make:model Article

Creiamo quindi il controller ArticleController eseguendo:

 php artisan make:controller ArticleController --resource

Successivamente, modificheremo il file app/Providers/AppServiceProvider.php e importeremo la classe Illuminate\Support\Facades\Schema aggiungendo:

 use Illuminate\Support\Facades\Schema

...alla fine delle importazioni nella parte superiore del file.

Quindi, nella funzione di boot , scriveremo:

 Schema::defaultStringLength(191);

Al termine di tutto ciò, possiamo eseguire:

 php artisan migrate

...per applicare la migrazione che abbiamo creato sopra.

Tutorial Laravel Passport, Passaggio 2: crea i pezzi necessari del middleware

Qui aggiungeremo i pezzi di middleware necessari per il funzionamento dell'API.

Risposte JSON

Il primo pezzo necessario è il middleware ForceJsonResponse , che convertirà automaticamente tutte le risposte in JSON.

Per fare ciò, esegui:

 php artisan make:middleware ForceJsonResponse

E questa è la funzione di gestione di quel middleware, in App/Http/Middleware/ForceJsonReponse.php :

 public function handle($request, Closure $next) { $request->headers->set('Accept', 'application/json'); return $next($request); }

Successivamente, aggiungeremo il middleware al nostro file app/Http/Kernel.php nell'array $routeMiddleware :

 'json.response' => \App\Http\Middleware\ForceJsonResponse::class,

Quindi, lo aggiungeremo anche all'array $middleware nello stesso file:

 \App\Http\Middleware\ForceJsonResponse::class,

Ciò assicurerebbe che il middleware ForceJsonResponse venga eseguito su ogni richiesta.

CORS (condivisione delle risorse tra le origini)

Per consentire ai consumatori della nostra API REST Laravel di accedervi da un'origine diversa, dobbiamo configurare CORS. Per farlo, creeremo un middleware chiamato Cors .

In un terminale o prompt dei comandi, cd nella directory principale del progetto ed esegui:

 php artisan make:middleware Cors

Quindi, in app/Http/Middleware/Cors.php , aggiungi il seguente codice:

 public function handle($request, Closure $next) { return $next($request) ->header('Access-Control-Allow-Origin', '*') ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') ->header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, X-Token-Auth, Authorization'); }

Per caricare questo pezzo di middleware, dovremo aggiungere una riga all'array $routeMiddleware di app/Http/Kernel.php :

 'cors' => \App\Http\Middleware\Cors::class,

Inoltre, dovremo aggiungerlo all'array $middleware come abbiamo fatto per il middleware precedente:

 \App\Http\Middleware\Cors::class,

Dopo averlo fatto, aggiungeremo questo gruppo di route a routes/api.php :

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... });

Tutti i nostri percorsi API entreranno in quella funzione, come vedremo di seguito.

Esercitazione Laravel Passport, Fase 3: Creazione di controller di autenticazione utente per l'API

Ora vogliamo creare il controller di autenticazione con le funzioni di login e di register .

Per prima cosa, eseguiremo:

 php artisan make:controller Auth/ApiAuthController

Ora importeremo alcune classi nel file app/Http/Controllers/Auth/ApiAuthController.php . Queste classi verranno utilizzate nella creazione delle funzioni di login e register . Importeremo le classi aggiungendo:

 use App\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str;

...nella parte superiore del controller.

Ora, per aggiungere l'autenticazione API Laravel per i nostri utenti, creeremo le funzioni di login , logout e register (iscrizione) nello stesso file.

La funzione di register sarà simile a questa:

 public function register (Request $request) { $validator = Validator::make($request->all(), [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6|confirmed', ]); if ($validator->fails()) { return response(['errors'=>$validator->errors()->all()], 422); } $request['password']=Hash::make($request['password']); $request['remember_token'] = Str::random(10); $user = User::create($request->toArray()); $token = $user->createToken('Laravel Password Grant Client')->accessToken; $response = ['token' => $token]; return response($response, 200); }

La funzione di login è così:

 public function login (Request $request) { $validator = Validator::make($request->all(), [ 'email' => 'required|string|email|max:255', 'password' => 'required|string|min:6|confirmed', ]); if ($validator->fails()) { return response(['errors'=>$validator->errors()->all()], 422); } $user = User::where('email', $request->email)->first(); if ($user) { if (Hash::check($request->password, $user->password)) { $token = $user->createToken('Laravel Password Grant Client')->accessToken; $response = ['token' => $token]; return response($response, 200); } else { $response = ["message" => "Password mismatch"]; return response($response, 422); } } else { $response = ["message" =>'User does not exist']; return response($response, 422); } }

E infine, la funzione di logout :

 public function logout (Request $request) { $token = $request->user()->token(); $token->revoke(); $response = ['message' => 'You have been successfully logged out!']; return response($response, 200); }

Dopodiché, dobbiamo aggiungere le funzioni login , register e logout alle nostre rotte, cioè all'interno del gruppo di rotte già nell'API:

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... // public routes Route::post('/login', 'Auth\ApiAuthController@login')->name('login.api'); Route::post('/register','Auth\ApiAuthController@register')->name('register.api'); Route::post('/logout', 'Auth\ApiAuthController@logout')->name('logout.api'); // ... });

Infine, dobbiamo aggiungere il tratto HasApiToken al modello User . Passa app/User e assicurati di avere:

 use HasApiTokens, Notifiable;

…in testa alla classe.

Quello che abbiamo finora...

Se avviamo il server delle applicazioni, ovvero eseguiamo php artisan serve , e poi proviamo a inviare una richiesta GET alla route /api/user , dovremmo ricevere il messaggio:

 { "message": "Unauthenticated." }

Questo perché non siamo autenticati per accedere a quel percorso. Per rendere protetti alcuni percorsi di tua scelta, possiamo aggiungerli a routes/api.php subito dopo Route::post lines:

 Route::middleware('auth:api')->group(function () { // our routes to be protected will go in here });

Prima di procedere, aggiungeremo il percorso di logout al middleware auth:api perché Laravel utilizza un token per disconnettere l'utente, un token a cui non è possibile accedere dall'esterno del middleware auth:api . Le nostre rotte pubbliche si presentano così:

 Route::group(['middleware' => ['cors', 'json.response']], function () { // ... // public routes Route::post('/login', 'Auth\ApiAuthController@login')->name('login.api'); Route::post('/register', 'Auth\ApiAuthController@register')->name('register.api'); // ... });

I nostri percorsi protetti , invece, si presentano così:

 Route::middleware('auth:api')->group(function () { // our routes to be protected will go in here Route::post('/logout', 'Auth\ApiAuthController@logout')->name('logout.api'); });

Ora andremo ArticleController che abbiamo creato in app/Http/Controllers/ArticleController.php ed elimineremo i metodi di create e edit in quella classe. Successivamente, aggiungeremo il seguente pezzo di codice, leggermente modificato, a ciascuna funzione rimanente:

 $response = ['message' => '<function name> function']; return response($response, 200);

Compileremo <function name> come appropriato. Ad esempio, la funzione di update avrà questo come corpo:

 $response = ['message' => 'update function']; return response($response, 200);

Un test di autenticazione Laravel manuale: creazione di un utente

Per registrare un utente, invieremo una richiesta POST a /api/register con i seguenti parametri: name , email (che deve essere univoca), password e password_confirmation .

Uno screenshot dell'invio di una richiesta POST a /api/register utilizzando Postman.

Quando l'utente viene creato, l'API restituirà un token, che utilizzeremo in ulteriori richieste come mezzo per l'autenticazione.

Per accedere, invieremo una richiesta POST a /api/login . Se le nostre credenziali sono corrette, otterremo anche un token dalla nostra API di accesso Laravel in questo modo.

Uno screenshot dell'invio di una richiesta POST a /api/login utilizzando Postman.

Il token di autorizzazione che otteniamo restituito da questa richiesta possiamo usarlo quando vogliamo accedere a un percorso protetto. In Postman, la scheda "Autorizzazione" ha un menu a discesa in cui il tipo può essere impostato su "Token al portatore", dopodiché il token può andare nel campo del token.

Il processo è abbastanza simile in Insomnia.

Gli utenti cURL possono fare l'equivalente passando il parametro -H "Authorization: Bearer <token>" , dove <token> è il token di autorizzazione fornito dal login o dalla risposta del registro.

Come con cURL, se gli sviluppatori prevedono di utilizzare l'API utilizzando axios o una libreria di quel tipo, possono aggiungere un'intestazione di Authorization con valore Bearer <token> .

Esercitazione Laravel Passport, Fase 4: Creazione della funzionalità di reimpostazione della password

Ora che l'autenticazione di base è stata eseguita, è il momento di impostare una funzione di reimpostazione della password.

Per fare ciò, possiamo scegliere di creare una directory del controller api_auth , creare nuovi controller personalizzati e implementare la funzione; oppure possiamo modificare i controller di autenticazione che possiamo generare con Laravel. In questo caso, modificheremo i controller di autenticazione, poiché l'intera applicazione è un'API.

Innanzitutto, genereremo i controller di autenticazione eseguendo:

 composer require laravel/ui php artisan ui vue --auth

Modificheremo la classe in app/Http/Controllers/Auth/ForgotPasswordController.php , aggiungendo questi due metodi:

 protected function sendResetLinkResponse(Request $request, $response) { $response = ['message' => "Password reset email sent"]; return response($response, 200); } protected function sendResetLinkFailedResponse(Request $request, $response) { $response = "Email could not be sent to this email address"; return response($response, 500); }

Successivamente, dobbiamo configurare il controller che reimposta effettivamente la password, quindi andremo su app/Http/Controllers/Auth/ResetPasswordController.php e sovrascriveremo le funzioni predefinite in questo modo:

 protected function resetPassword($user, $password) { $user->password = Hash::make($password); $user->save(); event(new PasswordReset($user)); } protected function sendResetResponse(Request $request, $response) { $response = ['message' => "Password reset successful"]; return response($response, 200); } protected function sendResetFailedResponse(Request $request, $response) { $response = "Token Invalid"; return response($response, 401); }

Abbiamo anche bisogno di importare alcune classi nel controller aggiungendo:

 use Illuminate\Auth\Events\PasswordReset; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash;

...nella parte superiore del controller.

Vorremo anche modificare quale notifica e-mail viene utilizzata, perché la notifica e-mail fornita con Laravel non utilizza i token API per l'autorizzazione. Possiamo crearne uno nuovo in app/Notifications eseguendo questo comando:

 php artisan make:notification MailResetPasswordNotification

Avremo bisogno di modificare il file app/Notifications/MailResetPasswordNotification.php in modo che assomigli a questo:

 <?php namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Support\Facades\Lang; class MailResetPasswordNotification extends ResetPassword { use Queueable; protected $pageUrl; public $token; /** * Create a new notification instance. * * @param $token */ public function __construct($token) { parent::__construct($token); $this->pageUrl = 'localhost:8080'; // we can set whatever we want here, or use .env to set environmental variables } /** * Get the notification's delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return ['mail']; } /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { if (static::$toMailCallback) { return call_user_func(static::$toMailCallback, $notifiable, $this->token); } return (new MailMessage) ->subject(Lang::getFromJson('Reset application Password')) ->line(Lang::getFromJson('You are receiving this email because we received a password reset request for your account.')) ->action(Lang::getFromJson('Reset Password'), $this->pageUrl."?token=".$this->token) ->line(Lang::getFromJson('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.users.expire')])) ->line(Lang::getFromJson('If you did not request a password reset, no further action is required.')); } /** * Get the array representation of the notification. * * @param mixed $notifiable * @return array */ public function toArray($notifiable) { return [ // ]; } }

Per utilizzare questa nuova notifica, è necessario sovrascrivere il metodo sendPasswordResetNotification che l' User eredita dalla classe Authenticatable . Tutto quello che dobbiamo fare è aggiungerlo ad app/User.php :

 public function sendPasswordResetNotification($token) { $this->notify(new \App\Notifications\MailResetPasswordNotification($token)); }

Con una configurazione della posta correttamente funzionante, le notifiche dovrebbero funzionare a questo punto.

Tutto ciò che resta ora è il controllo dell'accesso degli utenti.

Tutorial Laravel Passport, Passaggio 5: creare un middleware di controllo degli accessi

Prima di creare il middleware di controllo dell'accesso, dovremo aggiornare la tabella user in modo che abbia una colonna denominata type , che verrà utilizzata per determinare il livello utente: il tipo 0 è un utente normale, il tipo 1 è un amministratore e il tipo 2 è un super amministratore.

Per aggiornare la tabella user , dobbiamo creare una migrazione eseguendo questo:

 php artisan make:migration update_users_table_to_include_type --table=users

Nel file appena creato del modulo database/migrations/[timestamp]_update_users_table.php , dovremo aggiornare le funzioni up e down per aggiungere e rimuovere la colonna del type , rispettivamente:

 public function up() { Schema::table('users', function (Blueprint $table) { $table->integer('type'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropIfExists('type'); }); }

Successivamente, eseguiremo php artisan migrate . Fatto ciò, dobbiamo modificare la nostra funzione di register nel file ApiAuthController.php , aggiungendolo appena prima della riga con $user = User::create($request->toArray()); :

 $request['type'] = $request['type'] ? $request['type'] : 0;

Inoltre, dovremo aggiungere questa riga all'array $validator :

 'type' => 'integer',

La prima di queste due modifiche renderà tutti gli utenti registrati “utenti normali” per impostazione predefinita, cioè se non viene inserito alcun tipo di utente.

Il middleware di controllo degli accessi stesso

Ora siamo in grado di creare due pezzi di middleware da utilizzare per il controllo degli accessi: uno per gli amministratori e uno per i super amministratori.

Quindi eseguiremo:

 php artisan make:middleware AdminAuth php artisan make:middleware SuperAdminAuth

Innanzitutto, andremo su app/Http/Middleware/AdminAuth.php e importeremo Illuminate\Support\Facades\Auth , quindi modificheremo la funzione handle in questo modo:

 public function handle($request, Closure $next) { if (Auth::guard('api')->check() && $request->user()->type >= 1) { return $next($request); } else { $message = ["message" => "Permission Denied"]; return response($message, 401); } }

Dovremo anche modificare la funzione handle in app/Http/Middleware/SuperAdminAuth.php :

 public function handle($request, Closure $next) { if (Auth::guard('api')->check() && $request->user()->type >= 2) { return $next($request); } else { $message = ["message" => "Permission Denied"]; return response($message, 401); } }

Dovresti anche importare la classe Auth nella parte superiore di entrambi i file aggiungendo:

 use Illuminate\Support\Facades\Auth;

...fino al fondo delle importazioni trovate lì.

Per utilizzare il nostro nuovo middleware, faremo riferimento a entrambe le classi nel kernel, ovvero in app/Http/Kernel.php aggiungendo le seguenti righe $routeMiddleware :

 'api.admin' => \App\Http\Middleware\AdminAuth::class, 'api.superAdmin' => \App\Http\Middleware\SuperAdminAuth::class,

Se gli sviluppatori vogliono utilizzare il middleware in un determinato percorso, tutto ciò che devi fare è aggiungerlo alla funzione del percorso in questo modo:

 Route::post('route','Controller@method')->middleware('<middleware-name-here>');

<middleware-name-here> in questo caso può essere api.admin , api.superAdmin , ecc., a seconda dei casi.

Questo è tutto ciò che serve per creare il nostro middleware.

Mettere tutto insieme

Per verificare che la nostra autenticazione e il controllo dell'accesso funzionino, ci sono alcuni passaggi aggiuntivi da eseguire.

Testare l'autenticazione e il controllo degli accessi Laravel: passaggio 1

Dobbiamo modificare la funzione di index di ArticleController e registrare il percorso. (Nei progetti del mondo reale, useremmo PHPUnit e lo faremmo come parte di un test automatico. Qui stiamo aggiungendo manualmente un percorso a scopo di test, che può essere rimosso in seguito.)

Passeremo al controller ArticleController su app/Http/Controllers/ArticleController e modificheremo la funzione di index in modo che assomigli a questa:

 public function index() { $response = ['message' => 'article index']; return response($response, 200); }

Successivamente, registreremo la funzione in un percorso andando al file routes/api.php e aggiungendo questo:

 Route::middleware('auth:api')->group(function () { Route::get('/articles', 'ArticleController@index')->name('articles'); });

Testare l'autenticazione e il controllo degli accessi Laravel: passaggio 2

Ora possiamo provare ad accedere al percorso senza un token di autenticazione. Dovremmo ricevere un errore di autenticazione.

Uno screenshot dell'invio di una richiesta GET a /api/articles utilizzando Postman, ricevendo in risposta un messaggio "Non autenticato".

Testare l'autenticazione e il controllo degli accessi Laravel: passaggio 3

Possiamo anche provare ad accedere allo stesso percorso con un token di autorizzazione (quello che abbiamo ottenuto dalla registrazione o dall'accesso in precedenza in questo articolo).

A volte, questo potrebbe causare un errore simile a questo:

 Unknown column 'api_token' in 'where clause' (SQL: select * from `users` where `api_token` = ... 

Uno screenshot dell'errore della colonna sconosciuta da GETting the API/articles route using Postman.

Se ciò accade, gli sviluppatori dovrebbero assicurarsi di aver eseguito una migrazione Passport e avere ['guards']['api']['driver'] impostato su passport in config/auth.php :

 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ 'driver' => 'passport', 'provider' => 'users', ], ],

Successivamente, anche la cache di configurazione deve essere aggiornata.

Una volta risolto, dovremmo avere accesso al percorso.

Uno screenshot dell'invio di una richiesta GET a /api/articles utilizzando Postman, con una normale risposta JSON.

Testare l'autenticazione e il controllo degli accessi Laravel: passaggio 4

È ora di testare il controllo degli accessi. ->middleware('api.admin') al percorso degli articoli, quindi appare così:

 Route::get('/articles', 'ArticleController@index')->middleware('api.admin')->name('articles');

Abbiamo fatto in modo che a un utente appena creato venga assegnato automaticamente il tipo 0, come possiamo vedere tramite il percorso api/user .

Uno screenshot dell'invio di una richiesta GET a /api/user utilizzando Postman. La risposta include un ID, nome, e-mail, timestamp di verifica e-mail nullo, timestamp creati e aggiornati compilati e un tipo.

Per questo motivo, dovremmo ricevere un errore nel tentativo di accedere all'endpoint degli articles come tale utente.

Uno screenshot dell'invio di una richiesta GET a /api/articles utilizzando Postman, con una risposta JSON di autorizzazione negata.

Ai fini del test, modifichiamo l'utente nel database in modo che abbia un type 1. Dopo aver verificato nuovamente la modifica tramite la route api/user , siamo pronti per riprovare a GET la route /articles/ .

Uno screenshot dell'invio di una richiesta GET a /api/articles come utente autenticato da Laravel utilizzando Postman, identico a una richiesta precedente, con l'eccezione che il tempo di risposta di questa era di 1.280 ms invece di 890 ms.

Funziona perfettamente.

Gli sviluppatori che stanno realizzando applicazioni più complesse dovrebbero notare che i controlli di accesso adeguati non saranno così semplici. In tal caso, è possibile utilizzare altre applicazioni di terze parti o le porte e le politiche di Laravel per implementare il controllo dell'accesso utente personalizzato. Nella seconda parte di questa serie, esamineremo soluzioni di controllo accessi più robuste e flessibili.

Autenticazione API Laravel: cosa abbiamo imparato

In questo tutorial di Laravel Passport, abbiamo discusso:

  1. Creazione di un controller fittizio e di un modello da utilizzare durante il test del nostro esempio di Laravel Passport.
  2. Creare il middleware necessario per far funzionare la nostra API senza intoppi, indirizzando CORS e costringendo l'API a restituire sempre risposte JSON.
  3. Configurazione dell'autenticazione API Laravel di base: registrazione, accesso e disconnessione.
  4. Impostazione della funzionalità di "reimpostazione della password" in base all'impostazione predefinita di Laravel.
  5. Creazione di un middleware di controllo dell'accesso per aggiungere livelli di autorizzazione dell'utente a percorsi diversi.

Queste sono competenze essenziali per chiunque lavori nel campo dei servizi di sviluppo Laravel. I lettori troveranno il risultato finale in questo repository GitHub e ora dovrebbero essere ben posizionati per implementare l'autenticazione con Laravel. Non vediamo l'ora di commenti qui sotto.