Если ты в 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)


  1. Gippsland
    08.08.2025 08:17

    И в чём прикол, чем он лучше? Что то как то пусто. Где итог? Нейросеть плохо потрудилась

    И репозиторий не работает.


  1. radioIT
    08.08.2025 08:17

    Если ты в 2025 собираешь свою админку ты лох)


    1. dev_family Автор
      08.08.2025 08:17

      В точку!


  1. undersunich
    08.08.2025 08:17

    Почему сразу не писать что это для Реакта? А если не нравится Реакт - какие еще есть варианты ?Vue ?


    1. dev_family Автор
      08.08.2025 08:17

      И правда - добавили тэг!


  1. 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. Сидируем тестового юзера


  1. 12VLad12
    08.08.2025 08:17

    Вайбкодеры на каждую простую функцию ставят библиотеку