Создание безопасного Laravel API с JWT-аутентификацией
Аутентификация JWT (JSON Web Token) в Laravel предоставляет механизм без сохранения состояния с криптографической подписью для проверки потребителей API без серверного хранилища сессий. JWT кодирует полезную нагрузку — как правило, идентификатор пользователя и утверждения — в компактную строку, безопасную для URL, подписанную секретным или RSA-ключом, что позволяет любому сервису, владеющему ключом проверки, независимо валидировать токен.
Это руководство охватывает полную реализацию JWT-аутентификации в Laravel API с использованием пакета `tymon/jwt-auth`, включая настройку, конфигурацию модели, логику контроллера, защиту маршрутов, стратегию обновления токенов и усиление безопасности для продакшена. Каждый шаг включает технический контекст, выходящий за рамки поверхностных руководств.
Предварительные требования и допущения о среде
Перед началом убедитесь в следующем:
- PHP 8.1 или выше (рекомендуется PHP 8.2 для Laravel 11)
- Laravel 10 или 11, установленный через Composer
- Composer 2.x
- MySQL 8.0, PostgreSQL 15 или любая совместимая с PDO база данных
- Настроенный файл `.env` с действительными учётными данными `DB_*`
- Базовое знакомство с сервисным контейнером Laravel, middleware и Eloquent ORM
Если вы развёртываете этот API в продакшен-среде, план VPS Хостинга с полным root-доступом даёт вам контроль, необходимый для настройки PHP-FPM, безопасного управления переменными среды и правильной установки прав доступа к файлам — всё это критически важно для управления JWT-секретом.
Шаг 1: Создание нового проекта Laravel
“`bash
composer create-project laravel/laravel laravel-jwt-api
cd laravel-jwt-api
“`
Проверьте установку:
“`bash
php artisan –version
“`
Установите ключ приложения, если он не был сгенерирован автоматически:
“`bash
php artisan key:generate
“`
Параметр `APP_KEY` в `.env` отличается от JWT-секрета. Оба необходимы и служат разным криптографическим целям — `APP_KEY` защищает зашифрованные куки и данные сессий, тогда как `JWT_SECRET` подписывает токены.
Шаг 2: Установка пакета JWT-аутентификации
Пакет `tymon/jwt-auth` является де-факто стандартом для JWT в Laravel. Установите его:
“`bash
composer require tymon/jwt-auth
“`
Опубликуйте конфигурацию пакета:
“`bash
php artisan vendor:publish –provider="TymonJWTAuthProvidersLaravelServiceProvider"
“`
Это создаёт `config/jwt.php`, который управляет TTL токена, TTL обновления, алгоритмом, поведением чёрного списка и обязательными утверждениями. Внимательно изучите этот файл — значения по умолчанию разумны для разработки, но требуют настройки для продакшена.
Ключевые параметры конфигурации в `config/jwt.php`:
| Параметр | По умолчанию | Рекомендация для продакшена |
|---|
| — | — | — |
|---|
| `ttl` | 60 минут | 15–30 минут |
|---|
| `refresh_ttl` | 20160 минут (2 недели) | 1440–10080 минут |
|---|
| `algo` | `HS256` | `RS256` для распределённых систем |
|---|
| `blacklist_enabled` | `true` | Должно быть `true` |
|---|
| `blacklist_grace_period` | 0 секунд | 10–30 секунд для параллельных запросов |
|---|
| `required_claims` | `['iss','iat','exp','nbf','sub','jti']` | Сохранить все; добавить `aud` для мультитенантных API |
|---|
Шаг 3: Генерация секретного ключа JWT
“`bash
php artisan jwt:secret
“`
Это добавляет `JWT_SECRET` в ваш файл `.env`. Это значение используется в качестве ключа подписи HMAC-SHA256 для всех токенов при использовании алгоритма `HS256` по умолчанию.
Критически важные замечания по безопасности:
- Никогда не фиксируйте `.env` в системе контроля версий. Немедленно добавьте его в `.gitignore`.
- В общем конвейере развёртывания внедряйте `JWT_SECRET` как переменную среды через вашу систему CI/CD, а не храните его в файле.
- При ротации секрета все существующие токены немедленно аннулируются. Планируйте окна ротации соответствующим образом и сообщайте о них потребителям API.
- Для микросервисных архитектур, где несколько сервисов должны проверять токены, переключитесь на `RS256`. Сгенерируйте пару RSA-ключей, храните закрытый ключ на сервисе аутентификации и распространяйте только открытый ключ потребляющим сервисам.
Шаг 4: Настройка защитника аутентификации
Откройте `config/auth.php` и обновите разделы defaults и guards:
“`php
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
“`
Это указывает системе аутентификации Laravel использовать JWT-драйвер при разрешении защитника `api`. Middleware `auth:api` теперь будет делегировать валидацию токенов `tymon/jwt-auth` вместо Laravel Passport или стандартного драйвера токенов.
Не удаляйте защитник `web`. Многие внутренние компоненты Laravel зависят от него, и его удаление вызывает неожиданные сбои в консольных командах и обработчиках очередей, взаимодействующих с системой аутентификации.
Шаг 5: Создание модели пользователя и миграции
Если стандартная модель `User` и миграция уже существуют (они есть в свежей установке Laravel), вы можете изменить их напрямую. Если начинаете с нуля:
“`bash
php artisan make:model User -m
“`
Откройте файл миграции в `database/migrations/` и определите схему:
“`php
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
“`
Запустите миграцию:
“`bash
php artisan migrate
“`
Замечание для продакшена: Всегда запускайте миграции с флагом `–force` в продакшен-средах, где `APP_ENV=production`, так как иначе Laravel запросит подтверждение:
“`bash
php artisan migrate –force
“`
Шаг 6: Реализация интерфейса JWTSubject в модели пользователя
Пакет `tymon/jwt-auth` требует, чтобы ваша аутентифицируемая модель реализовывала контракт `JWTSubject`. Откройте `app/Models/User.php`:
“`php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateFoundationAuthUser as Authenticatable;
use IlluminateNotificationsNotifiable;
use TymonJWTAuthContractsJWTSubject;
class User extends Authenticatable implements JWTSubject
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
/**
- Get the identifier that will be stored in the JWT subject claim.
*/
public function getJWTIdentifier(): mixed
{
return $this->getKey();
}
/**
- Return a key-value array of arbitrary claims to add to the JWT payload.
*/
public function getJWTCustomClaims(): array
{
return [];
}
}
“`
О пользовательских утверждениях: Метод `getJWTCustomClaims()` — это место, где вы встраиваете специфичные для приложения данные непосредственно в полезную нагрузку токена. Распространённые варианты использования включают встраивание `role`, `tenant_id` или `permissions`, чтобы нижестоящие сервисы могли принимать решения об авторизации без обращения к базе данных. Тщательно выбирайте, что встраивать — каждое утверждение увеличивает размер токена и доступно для чтения любому, кто декодирует полезную нагрузку через base64. Никогда не встраивайте конфиденциальные данные, такие как пароли или персональные данные.
Шаг 7: Создание контроллера аутентификации
“`bash
php artisan make:controller AuthController
“`
Заполните `app/Http/Controllers/AuthController.php` полной логикой аутентификации:
“`php
<?php
namespace AppHttpControllers;
use AppModelsUser;
use IlluminateHttpJsonResponse;
use IlluminateHttpRequest;
use IlluminateSupportFacadesAuth;
use IlluminateSupportFacadesHash;
use IlluminateValidationValidationException;
use TymonJWTAuthExceptionsJWTException;
use TymonJWTAuthFacadesJWTAuth;
class AuthController extends Controller
{
/**
- Register a new user and return a JWT.
*/
public function register(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
$token = JWTAuth::fromUser($user);
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
], 201);
}
/**
- Authenticate a user and return a JWT.
*/
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
]);
if (!$token = Auth::guard('api')->attempt($credentials)) {
return response()->json(['error' => 'Invalid credentials'], 401);
}
return $this->respondWithToken($token);
}
/**
- Invalidate the current token (logout).
*/
public function logout(): JsonResponse
{
try {
Auth::guard('api')->logout();
} catch (JWTException $e) {
return response()->json(['error' => 'Failed to invalidate token'], 500);
}
return response()->json(['message' => 'Successfully logged out']);
}
/**
- Refresh the current token.
*/
public function refresh(): JsonResponse
{
try {
$token = Auth::guard('api')->refresh();
} catch (JWTException $e) {
return response()->json(['error' => 'Token cannot be refreshed'], 401);
}
return $this->respondWithToken($token);
}
/**
- Return the authenticated user's profile.
*/
public function me(): JsonResponse
{
return response()->json(Auth::guard('api')->user());
}
/**
- Format the token response consistently.
*/
protected function respondWithToken(string $token): JsonResponse
{
return response()->json([
'token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60,
]);
}
}
“`
Почему важно явно указывать защитник: Вызов `Auth::attempt()` без указания защитника возвращается к защитнику по умолчанию. Если вы изменили значение по умолчанию на `api`, это работает — но ненадёжно. Всегда явно вызывайте `Auth::guard('api')->attempt()`, чтобы избежать скрытых ошибок при изменении защитника по умолчанию в процессе рефакторинга.
Шаг 8: Определение маршрутов API
Откройте `routes/api.php` и определите маршруты аутентификации и защищённые маршруты:
“`php
<?php
use AppHttpControllersAuthController;
use IlluminateSupportFacadesRoute;
// Public authentication routes
Route::prefix('auth')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
});
// Protected routes — require a valid JWT
Route::middleware('auth:api')->prefix('auth')->group(function () {
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
Route::get('me', [AuthController::class, 'me']);
});
// Example protected resource routes
Route::middleware('auth:api')->group(function () {
Route::apiResource('posts', AppHttpControllersPostController::class);
});
“`
Стратегия префиксов маршрутов: Группировка конечных точек аутентификации под `/api/auth/` — широко принятое соглашение, которое делает документацию API чище и упрощает правила ограничения частоты запросов — вы можете применять более строгое ограничение к `/api/auth/login` независимо от конечных точек ресурсов.
Шаг 9: Защита маршрутов и обработка сбоев middleware
Middleware `auth:api` Laravel вернёт ответ `401 Unauthenticated`, когда токен отсутствует, истёк или недействителен. Чтобы возвращать согласованный JSON-ответ вместо HTML-перенаправления, переопределите метод `unauthenticated` в `app/Exceptions/Handler.php`:
“`php
use IlluminateAuthAuthenticationException;
use IlluminateHttpRequest;
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
return redirect()->guest(route('login'));
}
“`
Без этого переопределения API-клиенты получают HTML-перенаправление 302 на страницу входа, которой не существует в чистом API-приложении — распространённый источник путаницы при интеграционном тестировании.
Шаг 10: Стратегия обновления токенов и чёрный список
Природа JWT без сохранения состояния создаёт противоречие: токены самодостаточны и действительны до истечения срока, но вам нужен способ их отзыва при выходе из системы. Пакет `tymon/jwt-auth` решает это с помощью чёрного списка токенов, поддерживаемого драйвером кэша Laravel.
Как работает чёрный список:
- При выходе из системы утверждение `jti` (JWT ID) токена сохраняется в кэше с TTL, соответствующим оставшемуся времени жизни токена.
- При каждом аутентифицированном запросе middleware проверяет чёрный список перед принятием токена.
- Параметр `blacklist_grace_period` допускает короткое окно, в течение которого обновляемый токен всё ещё может использоваться, предотвращая состояния гонки в клиентах, выполняющих параллельные запросы.
Убедитесь, что ваш драйвер кэша поддерживает это. Стандартный драйвер `file` работает для развёртываний на одном сервере. Для горизонтально масштабируемых API, работающих на нескольких узлах — что характерно при использовании Выделенных серверов в конфигурации с балансировкой нагрузки — переключитесь на Redis или Memcached:
“`env
CACHE_DRIVER=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
“`
Процесс обновления токена:
“`
Client API Server
| — POST /api/auth/refresh ——> |
|---|
| Authorization: Bearer <old> |
|---|
| — Validate old token |
|---|
| — Blacklist old token's jti |
|---|
| — Issue new token |
|---|
| <– 200 { token: <new> } ——– |
|---|
“`
Старый токен немедленно добавляется в чёрный список при обновлении. Клиенты должны сохранить новый токен и отбросить старый. Реализуйте это как перехватчик Axios или аналогичный механизм во фронтенде для прозрачной обработки обновления токенов.
Шаг 11: Тестирование API
Используйте `curl` или Postman для проверки каждой конечной точки.
Регистрация пользователя:
“`bash
curl -X POST https://your-domain.com/api/auth/register
-H "Content-Type: application/json"
-d '{
"name": "Jane Smith",
"email": "jane@example.com",
"password": "SecurePass123!",
"password_confirmation": "SecurePass123!"
}'
“`
Ожидаемый ответ (`201 Created`):
“`json
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"token_type": "bearer",
"expires_in": 3600
}
“`
Вход в систему:
“`bash
curl -X POST https://your-domain.com/api/auth/login
-H "Content-Type: application/json"
-d '{"email": "jane@example.com", "password": "SecurePass123!"}'
“`
Доступ к защищённому маршруту:
“`bash
curl -X GET https://your-domain.com/api/auth/me
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
Обновление токена:
“`bash
curl -X POST https://your-domain.com/api/auth/refresh
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
Выход из системы:
“`bash
curl -X POST https://your-domain.com/api/auth/logout
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…"
“`
JWT против сессий, Laravel Sanctum и Passport
Понимание того, когда JWT является правильным выбором, требует сравнения с альтернативами, доступными в экосистеме Laravel.
| Критерий | JWT (`tymon/jwt-auth`) | Laravel Sanctum | Laravel Passport | На основе сессий |
|---|
| — | — | — | — | — |
|---|
| **Сохранение состояния** | Без состояния | Без состояния (токены SPA) / С состоянием (куки) | С состоянием (токены в БД) | С состоянием |
|---|
| **Хранение токенов** | На стороне клиента | На стороне клиента или в куки | База данных | Серверная сессия |
|---|
| **Отзыв** | Чёрный список (кэш) | Немедленный (удаление из БД) | Немедленный (удаление из БД) | Немедленный |
|---|
| **Масштабируемость** | Отличная (нет обращений к БД на запрос) | Хорошая | Умеренная (обращение к БД на каждый запрос) | Низкая (требуется синхронизация сессий) |
|---|
| **Поддержка OAuth2** | Нет | Нет | Да (полный OAuth2-сервер) | Нет |
|---|
| **Сложность** | Средняя | Низкая | Высокая | Низкая |
|---|
| **Лучше всего для** | Stateless API, микросервисы | SPA, мобильные приложения | Сторонние OAuth-клиенты | Традиционные веб-приложения |
|---|
| **Интроспекция токена** | Декодирование полезной нагрузки на стороне клиента | Требует вызова API | Требует вызова API | Н/П |
|---|
Когда выбирать JWT вместо Sanctum: Если ваш API потребляется сторонними клиентами, мобильными приложениями или микросервисами, которые не могут использовать общий сессионный куки или подключение к базе данных с вашим Laravel-приложением, самодостаточная природа JWT является значительным преимуществом. Если вы создаёте собственное SPA на том же домене, Sanctum с аутентификацией на основе куки проще и полностью избавляет от проблем безопасности хранения токенов.
Усиление безопасности для продакшена
Работающая реализация JWT не является безопасной по умолчанию. Примените следующие меры усиления безопасности перед запуском в продакшен:
1. Безусловно применяйте HTTPS
JWT-токены, передаваемые по HTTP, легко перехватить. Применяйте TLS на уровне веб-сервера и перенаправляйте весь HTTP-трафик. Дополните это SSL-сертификатом, чтобы обеспечить зашифрованную передачу для каждого API-запроса.
2. Устанавливайте агрессивные TTL токенов
Краткосрочные токены доступа (15–30 минут) в сочетании с долгосрочными токенами обновления (7–14 дней) ограничивают ущерб от кражи токена. Обновите `config/jwt.php`:
“`php
'ttl' => 15,
'refresh_ttl' => 10080,
“`
3. Применяйте ограничение частоты запросов к конечным точкам аутентификации
Встроенный middleware throttle Laravel предотвращает атаки методом перебора:
“`php
Route::middleware(['throttle:10,1'])->group(function () {
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/register', [AuthController::class, 'register']);
});
“`
Это ограничивает каждый IP до 10 запросов в минуту на конечных точках аутентификации.
4. Валидируйте утверждение `aud` для мультитенантных API
Если ваш API обслуживает несколько клиентских приложений, встраивайте и валидируйте утверждение `audience`, чтобы предотвратить повторное использование токенов между сервисами:
“`php
// In getJWTCustomClaims()
return [
'aud' => config('app.jwt_audience'),
];
“`
5. Защищайте JWT_SECRET на уровне ОС
Установите ограничительные права доступа к файлу `.env`:
“`bash
chmod 640 .env
chown www-data:www-data .env
“`
На правильно настроенном VPS с cPanel вы можете управлять правами доступа и владельцем файлов через Файловый менеджер или SSH без риска раскрытия секретов другим пользователям системы.
6. Ведите журнал событий аутентификации
Интегрируйте систему событий Laravel для записи неудачных попыток входа, обновлений токенов и выходов из системы в централизованный сервис логирования. Это необходимо для обнаружения аномалий.
Добавление управления доступом на основе ролей
Пользовательские утверждения позволяют легко встраивать роли непосредственно в токен:
“`php
// In User model
public function getJWTCustomClaims(): array
{
return [
'role' => $this->role, // e.g., 'admin', 'editor', 'viewer'
];
}
“`
Создайте middleware для применения требований к ролям:
“`php
<?php
namespace AppHttpMiddleware;
use Closure;
use IlluminateHttpRequest;
use TymonJWTAuthFacadesJWTAuth;
class RoleMiddleware
{
public function handle(Request $request, Closure $next, string $role): mixed
{
$payload = JWTAuth::parseToken()->getPayload();
if ($payload->get('role') !== $role) {
return response()->json(['error' => 'Forbidden'], 403);
}
return $next($request);
}
}
“`
Зарегистрируйте его в `app/Http/Kernel.php` (Laravel 10) или `bootstrap/app.php` (Laravel 11) и примените к маршрутам:
“`php
Route::middleware(['auth:api', 'role:admin'])->group(function () {
Route::apiResource('admin/users', AdminUserController::class);
});
“`
Предостережение: Данные о ролях, встроенные в токен, актуальны лишь на момент его выдачи. Если роль пользователя изменится, старый токен продолжит предоставлять старую роль до истечения срока действия или обновления. Для высокозащищённых изменений ролей (например, немедленного отзыва прав администратора) сочетайте добавление токена в чёрный список с коротким TTL или выполняйте проверку роли в базе данных в middleware наряду с проверкой утверждения.
Соображения по развёртыванию Laravel JWT API
При переходе от локальной разработки к продакшен-серверу несколько специфичных для среды факторов влияют на поведение JWT:
- Согласованность часовых поясов: Утверждения JWT `iat`, `nbf` и `exp` являются Unix-временными метками. Убедитесь, что часовой пояс вашего сервера установлен на UTC (`date.timezone = UTC` в `php.ini`), чтобы предотвратить отклонение токенов из-за расхождения часов.
- OPcache: Включите PHP OPcache для снижения накладных расходов на загрузку файлов библиотеки JWT при каждом запросе. Это особенно важно для высоконагруженных API.
- Обработчики очередей для очистки чёрного списка токенов: Если вы реализуете пользовательские задания по очистке чёрного списка токенов, убедитесь, что обработчик очереди запущен как управляемый процесс (Supervisor или systemd).
- Управление переменными среды: На Панелях управления VPS используйте менеджер переменных среды панели, а не редактируйте `.env` напрямую в продакшене, чтобы избежать случайной перезаписи при развёртываниях.
Контрольный список перед запуском в продакшен
Используйте этот контрольный список для проверки готовности реализации к продакшену:
- `JWT_SECRET` содержит не менее 32 символов, сгенерирован случайным образом и не зафиксирован в системе контроля версий
- `blacklist_enabled` установлен в `true` в `config/jwt.php`
- TTL токена составляет 30 минут или менее для токенов доступа
- TTL обновления установлен в соответствии с вашей политикой сессий
- Все конечные точки API обслуживаются исключительно по HTTPS
- Ограничение частоты запросов применено к конечным точкам `/login` и `/register`
- Обработчик исключений `unauthenticated` возвращает JSON, а не HTML-перенаправление
- Пользовательские утверждения не содержат паролей, секретов или конфиденциальных персональных данных
- Драйвер кэша — Redis или Memcached (не `file`) в многосерверных развёртываниях
- События аутентификации регистрируются и отслеживаются
- Изменения ролей, требующие немедленного вступления в силу, обрабатываются через добавление в чёрный список, а не только через истечение срока действия утверждений
- Права доступа к файлу `.env` ограничены пользователем веб-сервера
Часто задаваемые вопросы
В чём разница между `JWT_SECRET` и `APP_KEY` в Laravel?
`APP_KEY` используется сервисом шифрования Laravel для шифрования куки, данных сессий и значений, передаваемых через `Crypt::encrypt()`. `JWT_SECRET` используется исключительно пакетом `tymon/jwt-auth` для подписи и проверки JSON Web Token. Они криптографически независимы и служат совершенно разным целям. Оба должны храниться в тайне.
Почему мой JWT-токен продолжает возвращать 401, хотя срок его действия не истёк?
Наиболее распространённые причины: токен добавлен в чёрный список (например, после выхода из системы или обновления), `JWT_SECRET` был ротирован после выдачи токена, драйвер кэша, хранящий чёрный список, недоступен, или существует расхождение часов между сервером выдачи и сервером проверки, превышающее параметр `leeway` в `config/jwt.php`. Проверяйте каждую из этих причин по порядку.
Можно ли использовать JWT-аутентификацию с очередями Laravel или консольными командами?
JWT предназначен для аутентификации HTTP-запросов. Внутри заданий очереди или команд Artisan нет контекста HTTP-запроса, поэтому вы не можете разрешить пользователя из токена через middleware. Вместо этого передайте первичный ключ пользователя в качестве параметра задания и загружайте модель напрямую с помощью `User::find($userId)`.
Как обрабатывать параллельные запросы во время обновления токена без получения ошибок 401?
Установите `blacklist_grace_period` в `config/jwt.php` в значение от 10 до 30 секунд. В течение этого окна токен, который только что был обновлён (и технически добавлен в чёрный список), всё ещё будет приниматься. Это предотвращает состояния гонки в клиентах, отправляющих несколько одновременных запросов во время выполнения обновления.
Совместим ли `tymon/jwt-auth` с Laravel 11?
По состоянию на текущий цикл выпусков, версия `tymon/jwt-auth` `2.x` поддерживает Laravel 10 и 11 через ветку `dev-develop` или теговые релизы, декларирующие совместимость. Всегда проверяйте ограничения `composer.json` пакета и трекер задач на GitHub перед обновлением версий Laravel в проекте, зависящем от этого пакета. Рассмотрите возможность фиксации версии пакета в `composer.json`, чтобы избежать неожиданных критических изменений при выполнении `composer update`.
