
REST API аутентифікація з використанням Laravel Passport
13.01.2019 17:25 | Laravel
У цій статті розглянемо як в Laravel створити API аутентифікацію за допомогою офіційного пакета Passport.
Для початку кілька слів про те, як все відбувається. Як відомо, між запитами REST-додатки не зберігають інформацію про стан клієнта. Для аутентифікації зазвичай використовуються токени. Іншими словами:
- Користувач відправляє серверу логін і пароль
- Якщо все ок, сервер генерує токен і відправляє клієнту
- Клієнт (зазвичай в хедері) відправляє даний токен з кожним запитом
- На стороні сервера перевіряється токен, і знову ж таки, якщо все ок - відправляється відповідь
Пакет laravel / passport забезпечує повну реалізацію сервера OAuth2, і якщо Ви не знайомі з цією темою, після прочитання статті рекомендую ознайомитися з наступними матеріалами: JSON Web Tokens, Laravel Passport, OAuth 2.0 Server.
Для даної статті я використовую Laravel 5.7.19 та PostgreSQL 10.5
Установка та конфігурація
Крок 1
Встановлюємо пакет:
composer require laravel/passport
Крок 2
Виконуємо міграції:
php artisan migrate
Крок 3
Створюємо ключі шифрування, які необхідні для генерування безпечних токенов:
php artisan passport:install
Ця команда також створить клієнтів "personal access" і "password grant", які будуть використовуватися при генерації токенів.
Крок 4
Додамо трейт HasApiToken
в модель User
:
<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
...
}
Крок 5
Далі пропишемо виклик роутів у методі boot()
провайдера AuthServiceProvider
(app/Providers/AuthServiceProvider.php
):
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Passport::routes();
}
}
Шаг 6
Нарешті, у файлі конфігурації config/auth.php
у якості параметра драйвера для захисту api аутентифікації встановимо значення passport
:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
Раджу очистити кеш конфігурації, тим більше, якщо проект не новий:
php artisan config:clear
Роути та контролери
В приципі всі методи можна було б помістити в AuthController
, але я пропоную зробити структуру аналогічну web - тобто створити окремі контролери для логіна, реєстрації і т.і. та розташувати їх в каталозі Api/Auth
.Також, оскільки кожен з контролерів буде містити тільки один метод, будемо використовувати Single Action Controllers.
RegisterController і RegisterFormRequest
Валідацію пропоную винести з контролера в окремий клас. Для цього виконаємо:
php artisan make:request Api/Auth/RegisterFormRequest
Відткриваємо тільки що створений файл app/Http/Requests/Api/Auth/RegisterFormRequest.php
та вставляємо наступний код:
<?php
namespace App\Http\Requests\Api\Auth;
use Illuminate\Foundation\Http\FormRequest;
class RegisterFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:6', 'confirmed'],
];
}
}
Розбирати код не бачу сенсу, оскільки тема була раніше розкрита тут. Щоб створити контролер з єдиним методом слід використовувати флаг -i (скорочення від --invokable):
php artisan make:controller -i Api/Auth/RegisterController
Замість звичайного Request
робимо ін'єкцію RegisterFormRequest
, після чого залишається тільки створити користувача і повернути відповідь. Не будемо віддавати токен, а лише зазначимо, що реєстрація була успішною, і тепер можна увійти в систему, використовуючи свою поштову адресу і пароль:
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Auth\RegisterFormRequest;
use App\User;
class RegisterController extends Controller
{
public function __invoke(RegisterFormRequest $request)
{
$user = User::create(array_merge(
$request->only('name', 'email'),
['password' => bcrypt($request->password)],
));
return response()->json([
'message' => 'You were successfully registered. Use your email and password to sign in.'
], 200);
}
}
LoginController
Аналогічним чином створюємо контролер для входу:
php artisan make:controller -i Api/Auth/LoginController
Оскільки код контролера вимагає пояснень, розберемо його по частинах. Отже, беремо із запиту email і пароль і пробуємо увійти. Якщо, дані не вірні - відправимо відповідне повідомлення і код помилки:
$credentials = $request->only('email', 'password');
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'You cannot sign with those credentials',
'errors' => 'Unauthorised'
], 401);
}
Якщо ж все пройшло "як треба", згенеруємо токен:
$token = Auth::user()->createToken(config('app.name'));
І ось тут будьте уважні - в змінній $token
знаходиться не безпосередньо jwt-токен, а об'єкт, який в моєму випадку в Postman виглядає так:
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6Ijk2YWYzNDBlZjVlNTMxZmY0MTc4NmJkM2NiN2QwZjk4Yzh...",
"token": {
"id": "96af340ef5e531ff41786bd3cb7d0f98c8c323b30c1a4857a027aa064b1c40adb50a5a0e6668ae46",
"user_id": 3,
"client_id": 1,
"name": "Test App",
"scopes": [],
"revoked": false,
"created_at": "2019-01-15 19:49:37",
"updated_at": "2019-01-15 19:49:37",
"expires_at": "2020-01-15 19:49:37"
}
}
Тепер, коли об'єкт токена перед очима, стануть зрозумілі подальші дії. Будемо виходити з припущення, що в формі логіна міг бути відзначений чекбокс "Запам'ятати мене". Якщо це так, встановимо термін життя токена в один місяць, якщо ні - один день, і збережемо зміни:
$token->token->expires_at = $request->remember_me ?
Carbon::now()->addMonth() :
Carbon::now()->addDay();
$token->token->save();
Повний код контролера:
<?php
namespace App\Http\Controllers\Api\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class LoginController extends Controller
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function __invoke(Request $request)
{
$credentials = $request->only('email', 'password');
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'You cannot sign with those credentials',
'errors' => 'Unauthorised'
], 401);
}
$token = Auth::user()->createToken(config('app.name'));
$token->token->expires_at = $request->remember_me ?
Carbon::now()->addMonth() :
Carbon::now()->addDay();
$token->token->save();
return response()->json([
'token_type' => 'Bearer',
'token' => $token->accessToken,
'expires_at' => Carbon::parse($token->token->expires_at)->toDateTimeString()
], 200);
}
}
LogoutController
Тут простіше нікуди - "відкликаємо" токен:
<?php
namespace App\Http\Controllers\Api\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class LogoutController extends Controller
{
public function __invoke(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'You are successfully logged out',
]);
}
}
Роуты
Увага: якщо Ви визначите маршрути перед створенням Single action controllers, в процесі створення контролерів в консолі буде вилітати помилка з вимогою вказати метод в роуті. Тому рекомендую дотримуватися послідовності, що описана в цій статті (або піти стандартним шляхом і розмістити всі методи в AuthController).
Відкриємо файл routes/api.php
та пропишемо маршрути:
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Route::group(['namespace' => 'Api'], function () {
Route::group(['namespace' => 'Auth'], function () {
Route::post('register', 'RegisterController');
Route::post('login', 'LoginController');
Route::post('logout', 'LogoutController')->middleware('auth:api');
});
});
Невелике пояснення: слід враховувати, що крім аутентифікації в каталозі Api
будут розташовуватимуться і інші контролери або групи контролерів, тому логічніше в групу із загальним неймспейсом вкласти інші підгрупи (наприклад, categories, products і т.д.)
На цьому на сьогодні все. Успіхів!