Проблемы с аутентификацией между Next.js 15 и Laravel 11 Sanctum (401 Unauthenticated)

Я работаю над приложением с фронтендом на Next.js и бэкендом на Laravel 11, используя Laravel Sanctum для аутентификации. Столкнулся с проблемой: после успешного входа пользователя я не могу получить доступ к защищённым маршрутам, и сервер возвращает статус 401 Unauthenticated при запросах из middleware Next.js.

Описание проблемы:

После входа на странице /login я получаю данные пользователя, и в консоли браузера вижу:

Сессия активна, пользователь:
{ id: 1, name: 'Admin', email: '[email protected]', ... } 

Однако при переходе на /admin происходит перенаправление обратно на /login, и в консоли Next.js (серверная часть) появляется:

Статус ответа: 401
Данные ответа: { message: 'Unauthenticated.' }
Сессия недействительна. Перенаправление на /login.

В логах Laravel вижу успешный вход пользователя:

[2024-10-30 19:16:26] local.INFO: Попытка входа {"email":"[email protected]"}
[2024-10-30 19:16:26] local.INFO: Успешный вход: [email protected]

Что я использую:

Next.js на фронтенде Laravel 11 на бэкенде Laravel Sanctum для аутентификации Сессионная аутентификация (не токены API)

Что я пробовал:

Передача cookies из запроса в middleware Next.js:

В middleware.ts я попытался передать cookies из исходного запроса в запрос к Laravel API:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const laravelSession = request.cookies.get('laravel_session');

  if (!laravelSession || !laravelSession.value) {
    console.log('Сессия не найдена. Перенаправление на /login.');
    return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
  }

  try {
    const cookie = request.headers.get('cookie') || '';

    const response = await fetch('http://localhost:8000/api/user', {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Cookie': cookie,
      },
    });

    console.log('Статус ответа:', response.status);
    const data = await response.json();
    console.log('Данные ответа:', data);

    if (response.status !== 200) {
      console.log('Сессия недействительна. Перенаправление на /login.');
      return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
    }

    console.log('Сессия подтверждена.');
  } catch (error) {
    console.error('Ошибка проверки сессии:', error);
    return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin'],
};

Обновление SANCTUM_STATEFUL_DOMAINS и CORS:

В файле .env добавил localhost:

SANCTUM_STATEFUL_DOMAINS=localhost:3000,localhost
SESSION_DOMAIN=localhost

В config/sanctum.php:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000,localhost')),

В config/cors.php:

'paths' => ['api/*', 'login', 'logout', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:3000', 'http://localhost'],
'allowed_headers' => ['*'],
'supports_credentials' => true,

Проверка сессии и cookies:

Cookies laravel_session и XSRF-TOKEN устанавливаются после входа. При запросах из браузера (useEffect на клиенте) я могу получить данные пользователя с /api/user.

Проверка middleware в Laravel:

В routes/api.php маршрут /user настроен так:

Route::middleware([
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'auth:sanctum',
])->get('/user', function (Request $request) {
    return response()->json($request->user());
});

Очистка кэша и перезапуск серверов:

Выполнил php artisan config:clear и php artisan cache:clear. Перезапустил сервер Laravel и Next.js. Результат:

При запросе из middleware Next.js к http://localhost:8000/api/user всё ещё получаю статус 401 Unauthenticated.

В консоли Next.js вижу:

Статус ответа: 401
Данные ответа: { message: 'Unauthenticated.' }
Сессия недействительна. Перенаправление на /login.

Дополнительная информация:

Запросы из браузера работают корректно и возвращают данные пользователя. Проблема возникает только при запросах из middleware Next.js на серверной стороне. Похоже, что сервер Laravel не распознаёт сессию при серверных запросах из Next.js.

Файлы bootstrap app.php

<?php

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(
        web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        commands: __DIR__ . '/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        // Глобальные middleware
        $middleware->append([
            \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
            \Illuminate\Http\Middleware\TrustProxies::class,
            \Illuminate\Http\Middleware\HandleCors::class,
            \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
            \Illuminate\Http\Middleware\ValidatePostSize::class,
            \Illuminate\Foundation\Http\Middleware\TrimStrings::class,
            \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        ]);

        // Группа middleware 'web'
        $middleware->web(append: [
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ]);

        // Группа middleware 'api'
        $middleware->group('api', [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

routes web.php

<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;

Route::post('/login', function (Request $request) {
    Log::info('Попытка входа', ['email' => $request->input('email')]);

    $credentials = $request->only('email', 'password');

    if (Auth::attempt($credentials)) {
        $request->session()->regenerate();
        Log::info('Успешный вход: ' . $request->input('email'));
        return response()->json(['message' => 'Login successful']);
    }

    Log::warning('Неверные учетные данные', ['email' => $request->input('email')]);
    return response()->json(['message' => 'Invalid credentials'], 401);
})->name('login');

Route::post('/logout', function (Request $request) {
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();

    Log::info('Пользователь вышел');
    return response()->json(['message' => 'Logout successful']);
});

api.php

<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;

Route::middleware([
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'auth:sanctum',
])->get('/user', function (Request $request) {
    return $request->user()
        ? response()->json($request->user())
        : response()->json(['message' => 'Unauthorized'], 401);
});

Route::get('/check-session', function (Request $request) {
    return response()->json([
        'session_id' => $request->session()->getId(),
        'user' => $request->user(),
        'session_data' => $request->session()->all()
    ]);
});

next js login page.tsx

"use client";
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const router = useRouter();
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSessionActive, setIsSessionActive] = useState(false);

  useEffect(() => {
    const checkSession = async () => {
      try {
        const response = await fetch('http://localhost:8000/api/user', {
          method: 'GET',
          credentials: 'include',
        });

        if (response.ok) {
          const user = await response.json();
          console.log('Сессия активна, пользователь:', user);
          setIsSessionActive(true);
          router.push('/admin');
        }
      } catch (error) {
        console.log('Активная сессия не найдена.');
      }
    };

    checkSession();
  }, [router]);

  const getCsrfToken = async () => {
    const response = await fetch('http://localhost:8000/sanctum/csrf-cookie', {
      method: 'GET',
      credentials: 'include',
    });

    if (!response.ok) {
      throw new Error(`Не удалось получить CSRF-токен: ${response.status}`);
    }
  };

  const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (isSubmitting || isSessionActive) return;

    setIsSubmitting(true);

    try {
      await getCsrfToken();

      const xsrfToken = document.cookie
        .split('; ')
        .find((row) => row.startsWith('XSRF-TOKEN='))
        ?.split('=')[1];

      if (!xsrfToken) {
        alert('Ошибка: отсутствует XSRF-токен. Пожалуйста, попробуйте снова.');
        return;
      }

      const response = await fetch('http://localhost:8000/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-XSRF-TOKEN': decodeURIComponent(xsrfToken || ''),
        },
        credentials: 'include',
        body: JSON.stringify({ email, password }),
      });

      if (response.ok) {
        router.push('/admin');
      } else {
        alert('Неверные учетные данные или ошибка сессии.');
      }
    } catch (error) {
      alert('Произошла ошибка во время входа. Пожалуйста, попробуйте снова.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
      <form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <input
          type="password"
          placeholder="Пароль"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <button type="submit" disabled={isSubmitting}>Войти</button>
      </form>
    </div>
  );
}

middleware.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const laravelSession = request.cookies.get('laravel_session');

  if (!laravelSession || !laravelSession.value) {
    console.log('Сессия не найдена. Перенаправление на /login.');

    // Перенаправление на страницу входа фронтенда
    return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
  }

  try {
    // Извлекаем cookies из исходного запроса
    const cookie = request.headers.get('cookie') || '';

    const response = await fetch('http://localhost:8000/api/user', {
      method: 'GET',
      headers: {
        'Accept': 'application/json',
        'Cookie': cookie, // Передаём cookies в запросе
      },
    });

    console.log('Статус ответа:', response.status);
    const data = await response.json();
    console.log('Данные ответа:', data);

    if (response.status !== 200) {
      console.log('Сессия недействительна. Перенаправление на /login.');
      return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
    }

    console.log('Сессия подтверждена.');
  } catch (error) {
    console.error('Ошибка проверки сессии:', error);
    return NextResponse.redirect(new URL('/login', request.nextUrl.origin));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/admin'],
};

Вопрос:

Как правильно настроить аутентификацию между Next.js и Laravel Sanctum, чтобы middleware Next.js мог успешно проверять сессию пользователя при серверных запросах?

Почему Laravel возвращает статус 401 при запросе из middleware, хотя сессия активна и cookies передаются?

Может ли проблема быть связана с тем, что middleware Next.js выполняется на сервере, и Laravel Sanctum не распознаёт его как "stateful" запрос?

Буду очень благодарен за любую помощь или подсказки по решению этой проблемы. Если нужна дополнительная информация или код, я готов предоставить.

Спасибо!


Ответы (0 шт):