Если ты в 2025 году ты всё ещё руками собираешь авторизацию на Laravel для своей админки — у меня для тебя плохие новости. Ты либо получаешь удовольствие от страданий (либо просто не знаешь про Admiral).
А если нет желания быть заложником бесконечного копипаста и конфигурационного ада, то есть один очень простой способ перестать тратить часы на одни и те же ритуалы.
В этом гайде я покажу, как за пару движений поставить работающую связку Laravel + фронт + нормальную авторизацию и забыть про вечный треш с настройками.
Установка Laravel
Создаём основную директорию проекта:
mkdir admiral-laravel-init && cd admiral-laravel-init
Ставим актуальную версию Laravel — 12:
composer global require laravel/installer
Теперь создаём проект:
laravel new backend
Для базы данных берём SQLite (но вы можете выбрать любую, если вам скучно жить).
Заходим в папку и запускаем сервер:
cd backend
composer run dev
В консоли появится:
APP_URL: http://localhost:8000
Переходим по ссылке и убеждаемся, что Laravel жив.
Установка Admiral
Чтобы поднять админку, запускаем:
npx create-admiral-app@latest
Выбираем Install the template without backend setting
, а в project name
вводим admin
.
Дальше:
cd admin
npm i
В .env
указываем адрес бэкенда Laravel:
VITE_API_URL=http://localhost:8000/admin
Запускаем админку:
npm run build && npm run dev
В консоли появится что-то вроде:
Local: http://localhost:3000/
Заходим по адресу — вас сразу перекинет на /login
.
Авторизация
Теперь, когда Laravel и Admiral подняты, займёмся авторизацией. Используем Sanctum:
php artisan install:api
Правим config/auth.php
, добавляем новый guard admin
:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
В модель User.php
добавляем трейт HasApiTokens
— он нужен для работы с API-токенами.
AuthController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\LoginRequest;
use App\Services\Admin\Auth\AuthService;
use Illuminate\Validation\ValidationException;
use App\Http\Resources\AuthUserResource;
use App\Services\Admin\Auth\LimitLoginAttempts;
class AuthController
{
use LimitLoginAttempts;
public function __construct(
private readonly AuthService $auth,
) {
}
public function getIdentity(Request $request): array
{
$user = $request->user();
return [
'user' => AuthUserResource::make($user),
];
}
public function checkAuth(Request $request): \Illuminate\Http\JsonResponse
{
return response()->json('ok', 200);
}
public function logout(Request $request): void
{
$request->user()->currentAccessToken()->delete();
}
public function login(LoginRequest $request): array
{
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
$this->sendLockoutResponse($request);
}
try {
$user = $this->auth->login($request->email(), $request->password());
} catch (ValidationException $e) {
$this->incrementLoginAttempts($request);
throw $e;
}
catch (\Throwable $e) {
$this->incrementLoginAttempts($request);
throw ValidationException::withMessages([
'email' => [__('auth.failed')],
]);
}
$token = $user->createToken('admin');
return [
'user' => AuthUserResource::make($user),
'token' => $token->plainTextToken,
];
}
}
LoginRequest.php
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
final class LoginRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => [
'required',
'email',
],
'password' => [
'required',
],
];
}
public function email(): string
{
return $this->input('email');
}
public function password(): string
{
return $this->input('password');
}
}
AuthUserResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class AuthUserResource extends JsonResource
{
public function toArray($request): array
{
$this->resource = [
'id' => $this->resource->id,
'name' => $this->resource->name,
'email' => $this->resource->email,
];
return parent::toArray($request);
}
}
AuthService.php
<?php
declare(strict_types = 1);
namespace App\Services\Admin\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
final class AuthService
{
public function __construct()
{
}
/**
* @throws \Throwable
*/
public function login(string $email, string $password): User
{
$user = $this->findByEmail($email);
throw_if(
!$user || !Hash::check($password, $user->password),
ValidationException::withMessages([
'password' => __('auth.failed'),
])
);
return $user;
}
public function findByEmail(string $email): User|null
{
/** @var \App\Models\User $user */
$user = User::query()
->where('email', $email)
->first();
if (!$user) {
return null;
}
return $user;
}
}
LimitLoginAttempts.php
<?php
declare(strict_types=1);
namespace App\Services\Admin\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
trait LimitLoginAttempts
{
public function maxAttempts(): int
{
return property_exists($this, 'maxAttempts') ? $this->maxAttempts : 5;
}
public function decayMinutes(): int
{
return property_exists($this, 'decayMinutes') ? $this->decayMinutes : 1;
}
protected function hasTooManyLoginAttempts(Request $request): bool
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request),
$this->maxAttempts()
);
}
protected function incrementLoginAttempts(Request $request): void
{
$this->limiter()->hit(
$this->throttleKey($request),
$this->decayMinutes() * 60
);
}
protected function sendLockoutResponse(Request $request): void
{
$seconds = $this->limiter()->availableIn(
$this->throttleKey($request)
);
throw ValidationException::withMessages([
$this->loginKey() => [__('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
])],
])->status(Response::HTTP_TOO_MANY_REQUESTS);
}
protected function clearLoginAttempts(Request $request): void
{
$this->limiter()->clear($this->throttleKey($request));
}
protected function limiter(): RateLimiter
{
return app(RateLimiter::class);
}
protected function fireLockoutEvent(Request $request): void
{
event(new Lockout($request));
}
protected function throttleKey(Request $request): string
{
return Str::transliterate(Str::lower($request->input($this->loginKey())) . '|' . $request->ip());
}
protected function loginKey(): string
{
return 'email';
}
}
Роутинг
routes/admin.php
:
<?php
declare(strict_types = 1);
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
Route::group(['prefix' => 'auth',], function () {
Route::post('login', [AuthController::class, 'login'])->name('login');
Route::group(['middleware' => ['auth:admin']], function () {
Route::post('logout', [AuthController::class, 'logout']);
Route::get('/get-identity', [AuthController::class, 'getIdentity']);
Route::get('/check-auth', [AuthController::class, 'checkAuth']);
});
});
Подключение в bootstrap/app.php
:
<?php
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Routing\Middleware\SubstituteBindings;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
using: function () {
Route::middleware('admin')
->prefix('admin')
->group(base_path('routes/admin.php'));
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
},
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php'
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->group('admin', [SubstituteBindings::class]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();
Сид первого пользователя
DatabaseSeeder.php
:
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
'password' => '12345678',
]);
}
}
Выполняем:
php artisan db:seed
composer run dev
Если CORS решил испортить день
php artisan config:publish cors
В config/cors.php
в paths
добавляем:
'paths' => ['api/*', 'sanctum/csrf-cookie', 'admin/*'],
Итог
Запускай бекенд, фронт — и залогинься. Если что-то не работает, погугли CORS или попробуй мой лайфхак с config/cors.php
.
Вот и вся магия. Патчи, костыли и лишние библиотеки — в топку.
Если хочешь прокачать CRUD или вписать роли — пиши, расскажу, как сделать круто и быстро.
Вот так просто можно перестать страдать с авторизацией и заняться нормальным кодом.
Мнения?
Комментарии (7)
undersunich
08.08.2025 08:17Почему сразу не писать что это для Реакта? А если не нравится Реакт - какие еще есть варианты ?Vue ?
FanatPHP
08.08.2025 08:17Каша из топора. Сказка
Шёл солдат с похода, день-другой идёт, притомился, проголодался. Видит избушка стоит. Зашёл, а там бабушка сидит возле печи, старенькая-престаренькая. — Здравствуй, бабушка! Дай мне чего-нибудь поесть! — говорит солдат. — Ох, сынок, да вон там на гвоздике повесь! — отвечает старуха. — Ты что же это, совсем глуха, не чуешь? — Где хочешь, там и заночуешь. — Ах ты хитрая какая! Подавай на стол! — кричит солдат. — Да нечего, родимый! — Вари кашу! — Да не из чего, родимый! — Давай топор; я из топора сварю! — Что за диво! — думает старушка. — Дай посмотрю, как из топора солдат кашу сварит. Пошла в закуток и принесла служивому топор. А тот положил его в горшок, налил воды и давай себе варить. Варил, варил, попробовал и говорит: — Всем бы каша хороша, только бы чуточку крупы добавить! Старушка принесла ему крупы. Опять варил, варил, попробовал и говорит: — Совсем бы готово, только бы маслицем сдобрить! Старушка принесла ему и масла. Солдат тогда говорит: — Ну, теперь подавай хлеб да соль, да принимайся за ложку — станем кашу есть. Стали есть они вдвоём кашу. Наелись досыта. Старушка и спрашивает: — Служивый! Когда же топор будем есть? — Да видишь, он ещё не уварился, — отвечал солдат, — где-нибудь по дороге доварю и позавтракаю. Тотчас припрятал топор в свою сумку, простился с хозяйкой и пошёл в другую деревню. Вот так-то солдат и каши поел, и топор унёс!
Как не собирать авторизацию на Ларавле руками. Быль
Шаг 1. Ставим Laravel
Шаг 2. Ставим Admiral
Шаг 3. Ставим Laravel Sanctum, ставим и настраиваем:
Шаг 4. php artisan install:api
Шаг 5. Добавляем guard admin в config/auth.php:
Шаг 6. Добавляем в User.php
Шаг 7. Контроллер для входа, выхода и проверки
Шаг 8. Формы и ресурсы
Шаг 9. Сервисы и защита от дурака
Шаг 10. Роуты
Шаг 11. Сидируем тестового юзера
Gippsland
И в чём прикол, чем он лучше? Что то как то пусто. Где итог? Нейросеть плохо потрудилась
И репозиторий не работает.