Как стать автором
Обновить
564.32
OTUS
Цифровые навыки от ведущих экспертов

Аутентификация на основе cookies с помощью Laravel Sanctum

Уровень сложностиСредний
Время на прочтение18 мин
Количество просмотров2.1K
Автор оригинала: Roduan Kareem Aldeen

Представляю вашему вниманию подробное пошаговое руководство по настройке аутентификации на основе cookies с помощью Laravel Sanctum. В процессе мы объясним логику каждого шага и покажем, как настроить Postman. Кроме того, мы рассмотрим наиболее распространенные проблемы, связанные с CORS.

Предыстория

Однажды, просматривая YouTube, я наткнулся на следующее видео:

Хранение токенов аутентификации в localStorage — большая ошибка

Его посыл был максимально простым и понятным: «НЕ ХРАНИТЕ ТОКЕНЫ АУТЕНТИФИКАЦИИ В LOCALSTORAGE», и он наглухо засел в моей голове.

Спустя некоторое время, когда я собирался реализовать аутентификацию в своем новом личном проекте, как вы думаете, что начало прорываться из моего подсознания? Вы абсолютно правы — то самое предостережение из видео: «НЕ ХРАНИТЕ ТОКЕНЫ АУТЕНТИФИКАЦИИ В LOCALSTORAGE».

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

  • CORS

    • PreflightWildcardOriginNotAllowed

    • PreflightAllowedOriginMismatch

    • PreflightInvalidAllowCredentials

  • “Session not set on request”

  • 401 — Unauthenticated (при том, что я только что успешно авторизовался)

  • Настройка Postman

В этой статье вы (и я в будущем) найдете подробное пошаговое руководство по настройке Laravel Sanctum с аутентификацией на основе куки. Я надеюсь, что оно поможет вам разобраться в каждом шаге и избежать тех проблем, с которыми столкнулся я.

Пара слов об использовании куки для аутентификации 

Зачем это делать?

В двух словах, это более безопасно, поскольку JavaScript не может получить доступ к httponly куки, каковыми laravel_session куки являются по умолчанию. Если вас интересует более подробная информация, то советую вам посмотреть видео, о котором я говорил в разделе «Предыстория» выше.

Как это работает?

При первом запросе браузера сервер создает новый сеанс и отправляет его ID браузеру через куки. Обычно это {APP_NAME_FROM_ENV_FILE}_session, который в нашем случае будет laravel_session, если только вы не изменили APP_NAME в вашем .env‑файле.

Беремся за дело

Создание нового проекта

laravel new sanctum-cookie

Установка Laravel Sanctum

Если вы используете Laravel 10 или ниже (пропустите этот шаг, если вы используете Laravel 11):

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Далее давайте добавим middleware Sanctum в группу middleware api:

'api' => [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],

Пользователям Laravel 11 достаточно выполнить следующую команду, которая сама все за нас сделает:

php artisan install:api

Касательно подготовки middleware, нам просто нужно вызвать его метод statefulApi в файле bootstrap/app.php нашего приложения:

->withMiddleware(function (Middleware $middleware) {
    $middleware->statefulApi();
})

И последнее! По умолчанию Sanctum регистрирует маршрут для CSRF‑токена /sanctum/csrf‑token. Но поскольку мы будем использовать SPA, а фронтенд‑разработчики, вероятно, установят базовый URL /api, URL Sanctum не будет работать. Чтобы исправить это, мы добавим новый ключ в config/sanctum.php:

   /*
    |--------------------------------------------------------------------------
    | Префикс маршрута Sanctum
    |--------------------------------------------------------------------------
    |
    */

    'prefix' => 'api',

Теперь путь станет /api/csrf‑token, а фронтенд‑разработчики будут рады избавиться от одной дополнительной задачи. Здесь мы закончили!

Указываем домен фронтенд

TLDR: Добавьте новую переменную в .env‑файл: SANCTUM_STATEFUL_DOMAINS={YOUR_FRONTEND_URLS_COMMA_SEPARATED} (разделенные запятыми URL вашего фронтенд‑приложения). Например, если ваше фронтенд‑приложение будет доступно по адресам localhost:5173 для локальной разработки и frontend.madewithlove.com для продакшена, то переменная должна выглядеть следующим образом:

SANCTUM_STATEFUL_DOMAINS=localhost:5173,frontend.madewithlove.com

Бонусный совет: Не забудьте обновить .env.example, поскольку .env игнорируется в системе контроля версий. Для создания .env при клонировании проекта другим разработчиком или при развертывании в рамках DevOps будет использоваться .env.example.

Бонусный совет № 2: Вам НЕ НАДО указывать схему (http:// или https://) и завершающую косую черту /. Вам нужно прописать только хост (домен или IP) и порт (если он есть).

Бонусный совет № 3: Фронтенд и API должны находиться в одном домене верхнего уровня. Если ваш фронтенд доступен по адресу madewithlove.com, то API должен находиться либо в том же домене/поддомене.

Бонусный совет № 4: Переменная SESSION_DOMAIN отвечает за определение доменов и поддоменов, для которых доступен куки сеанса. По умолчанию куки будет доступен для домена верхнего уровня и всех поддоменов.

Объяснение

Во‑первых, давайте ответим на важный вопрос: зачем нам нужно указывать домен фронтенда? Из‑за того, что middleware, которое мы добавили при установке Sanctum, не будет аутентифицировать сеанс, если запрос не будет поступать из указанного нами домена или доменов. Такое ограничение необходимо для защиты приложения от несанкционированного доступа извне. Для получения более подробной информации вы можете ознакомиться с классом EnsureFrontendRequestsAreStateful.

Если мы посмотрим на ключ stateful в файле config/sanctum.php, то увидим, что он получает свое значение из переменной окружения SANCTUM_STATEFUL_DOMAINS. В случае, если эта переменная не задана, он откатится к localhost и его алиасам в дополнение к домену, указанному в APP_URL.

Реализация конечной точки логина

Теперь, когда все приготовления закончены, мы можем начать работу на маршрутом /login. Вы можете реализовать этот маршрут так, как вам угодно. В этом примере я буду использовать реализацию, описанную в официальной документации Laravel. Это позволит вам легко адаптировать ее под свои нужды. Если вы используете какой‑либо стартовый комплект, например, Breeze, то сможете легко повторить эти шаги, поскольку я уверен, что эта статья поможет вам понять, что скрывается под капотом, и вы сами сможете легко справится с аутентификацией.

php artisan make:controller Api/Auth/Spa/LoginController --invokable

// LoginController

namespace App\Http\Controllers\Api\Auth\Spa;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    public function __invoke(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return response()->json(['message' => __('Welcome!')]);
        }

        throw ValidationException::withMessages([
            'email' => __('The provided credentials do not match our records.'),
        ]);
    }
}

Здесь нет чего‑либо необычного, я просто использую метод Auth::attempt для аутентификации пользователя.

Конечно, нам еще нужно определить маршрут логина. Я добавлю к маршрутам аутентификации префикс auth/spa на случай, если в будущем нам понадобится реализовать маршруты аутентификации для мобильного приложения. Вы можете определить для него отдельный набор маршрутов в auth/app.

// routes/api.php
use App\Http\Controllers\Api\Auth\Spa\LoginController;

Route::prefix('auth/spa')->group(function (){
    Route::post('login', LoginController::class)->middleware('guest');
});

Создаем тестового пользователя

Чтобы протестировать нашу новую конечную точку логина, нам понадобится хотя бы один пользователь в базе данных. Существует множество способов его создать, поэтому вы можете выбрать тот, который вам больше всего нравится. В этом руководстве я просто воспользуюсь стандартным сидером, который содержит код для создания одного тестового пользователя.

<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Заполнение базы данных приложения.
     */
    public function run(): void
    {
        // User::factory(10)->create();

        User::factory()->create([
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
}

Примечание: В Laravel 10 код по умолчанию закомментирован. Не забудьте раскомментировать его перед запуском команды.

php artisan db:seed

Создание Postman-коллекции

Для начала давайте просто попробуем залогиниться в конечную точку, которую мы создали ранее, и посмотрим на результат.

К сожалению, результат оказался не таким, как мы ожидали. Для начала мы получили 500 Internal Server Error, а затем — ответ вернулся в формате HTML, а не JSON.

Решение второй проблемы довольно простое: нам просто нужно сообщить Laravel, чтобы он возвращал ответ в формате JSON. Мы можем сделать это, установив заголовок Accept в значение application/json.

Возвращаясь к первому вопросу, вы можете увидеть в первой строке комментария следующее: ReuntimeException: Session store not set on request..
Если мы погуглим эту ошибку, то найдем разные ответы в зависимости от контекста. Эта ошибка возникает из‑за того, что middleware StartSession не было запущено для данного запроса. По‑умолчанию оно применяется только к стандартным web ‑маршрутам. В интернете советуют перенести маршрут в файл web.php или добавить веб‑middleware к маршруту. Однако мы уже делали кое‑что при настройке Sanctum: добавляли middleware в группу маршрутов api. Это middleware должно было запустить StartSession, но почему‑то оно не сработало.

Если мы посмотрим на код EnsureFrontendRequestsAreStateful (middleware Sanctum), то увидим, что оно не будет выполнять никаких действий, ЕСЛИ ТОЛЬКО в запрос не будет включен заголовок referer или origin И его значение не будет добавлено в ключ конфига sanctum.stateful, на что на любезно указывает сообщение выше 👆🏻.

class EnsureFrontendRequestsAreStateful
{
    public function handle($request, $next)
    {
        $this->configureSecureCookieSessions();

        return (new Pipeline(app()))->send($request)->through(
            static::fromFrontend($request) ? $this->frontendMiddleware() : [] // 👈🏻👈🏻👈🏻
        )->then(function ($request) use ($next) {
            return $next($request);
        });
    }

    // ...

    public static function fromFrontend($request)
    {
        $domain = $request->headers->get('referer') ?: $request->headers->get('origin'); // 👈🏻👈🏻👈🏻

        if (is_null($domain)) {
            return false; // 👈🏻👈🏻👈🏻
        }

        $domain = Str::replaceFirst('https://', '', $domain);
        $domain = Str::replaceFirst('http://', '', $domain);
        $domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";

        $stateful = array_filter(config('sanctum.stateful', []));

        return Str::is(Collection::make($stateful)->map(function ($uri) {
            return trim($uri).'/*';
        })->all(), $domain); // 👈🏻👈🏻👈🏻
    }
}

Теперь, когда мы разгадали эту загадку, нам известно, что нужно сделать дальше:

  • Установить заголовок Accept как application/json

  • Установить заголовок Referer как localhost:5173

Давайте попробуем снова и посмотрим, как все пойдет сейчас.

Появилась новая ошибка, что свидетельствует о нашем продвижении вперед. Что такое CSRF‑токен и почему его несоответствие вызывает проблемы? Возможность ответить на этот вопрос, я предоставлю Джеффри Уэй (Jeffery Way), который прекрасно объяснит, что здесь происходит:

Итак, нам нужно сделать GET‑запрос к /api/csrf‑token, чтобы получить XSRF‑TOKEN‑куки, а затем включить его в запрос в качестве X‑XSRF‑TOKEN.

Как вы можете видеть, мы получили куки. Давайте скопируем его значение в запрос и попробуем снова!

Примечание

Обратите внимание, что в конце значения куки присутствует дополнительный символ%3D. Я не знаю, почему он появляется, но, вероятно, это баг в Postman.

eyJpdiI6ImdZb0s4eHQ2MERhOGM5VU0yMVhxUlE9PSIsInZhbHVlIjoiUnJpU2VHVVB2VnE2TEJ4YWMyczJKVnZJNEtzemJvZTg2SDlvSERxREtmKy80T2lpdUFwdE9ZK0lIdkZ6OUhmUUVLWFY2Q24zMEJ3TXdSQnErR0ErRWJuSUNXWHh5M2tYR2svbVlXd3F2RUUvZVpFb1ViNS9ua1FWZUh0akVrcjMiLCJtYWMiOiJhNWY1YTJkMTI0YjNiYzQ0NzM2MWI2M2NhYjRiNjhkYjEwNjljYTA3OWY4NzVhMDNjMmM0YjQ1YjQ5NWQ3NWRlIiwidGFnIjoiIn0%3D

Наконец‑то мы залогинились!

Теперь давайте попробуем сделать GET‑запрос на /api/user, который по умолчанию определен в routes/api.php.

У нас все получилось, поскольку Postman отслеживает файлы куки!

Давайте очистим куки и попробуем еще раз!

Выглядит неплохо!

Но не слишком ли много работы — каждый раз копировать‑вставлять заголовки и вручную вызывать конечную точку csrf‑cookie для получения XSRF‑TOKEN куки? Да, много, но вы привыкнете к этому.

Ладно, я пошутил. Чтобы автоматизировать весь процесс, нам просто нужно написать несколько скриптов Postman, которые будут выполнять следующие действия:

  • По умолчанию добавлять заголовки Accept и Referer для каждого запроса

  • Получать CSRF‑токен перед отправкой POST‑запросов

  • Добавлять заголовок XSRF‑TOKEN только к POST‑запросам

Но сначала давайте создадим несколько переменных, которые помогут нам упростить процесс выполнения новых запросов.

Мы можем использовать base_url для обновления URL‑адресов созданных нами запросов.

И это все еще работает.

Теперь настало время немного размять пальцы. Мы добавим следующий код в качестве Pre‑request скрипта в нашу коллекцию:

pm.request.headers.add({key: 'accept', value: 'application/json' })
pm.request.headers.add({key: 'referer', value: pm.collectionVariables.get('referer')})

if (pm.request.method.toLowerCase() !== 'get') {
    const baseUrl = pm.collectionVariables.get('base_url')

    pm.sendRequest({
        url: `${baseUrl}/csrf-cookie`,
        method: 'GET'
    }, function (error, response, {cookies}) {
        if (!error) {
            pm.request.headers.add({key: 'X-XSRF-TOKEN', value: cookies.get('XSRF-TOKEN')})
        }
    })
}

Чтобы просмотреть свойства совершите дойной клик.

Теперь мы можем удалить заголовки, которые добавили в запрос вручную, и попробовать снова.

Отлично! Мы готовы к работе!

Тестируем нашу работу в браузере

Для демонстрации мы создадим небольшое приложение на Vue 3, которое поможет убедиться, что все работает правильно.

Чтобы создать новое Vue‑приложение, выполните команду yarn create vue. Она запросит некоторые настройки. Вот мои ответы, если вы хотите следовать им:

√ Project name: ... sanctum-cookie-front
√ Add TypeScript? ... Yes
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes
√ Add Vue DevTools 7 extension for debugging? (experimental) ... No

Done. Now run:

  cd cookie-sanctum
  yarn
  yarn format
  yarn dev

Теперь давайте установим необходимые нам зависимости: yarn add axios js‑cookies и yarn add @types/js‑cookies ‑D.

Создайте файл src/plugins/axios.ts со следующим содержимым:

import axiosLib from 'axios'
import Cookies from 'js-cookie'

const axios = axiosLib.create({
  baseURL: import.meta.env.VITE_BACKEND_URL,
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
    'Accept': 'application/json',
  },
})

axios.defaults.withCredentials  = true // разрешить отправку куки

axios.interceptors.request.use(async (config) => {
  if ((config.method as string).toLowerCase() !== 'get') {
    await axios.get('/csrf-cookie').then()
    config.headers['X-XSRF-TOKEN'] = Cookies.get('XSRF-TOKEN')
  }

  return config
})

export default axios

Нам нужно определить переменную VITE_BACKEND_URL:

VITE_BACKEND_URL=http://localhost:8000/api

И последнее, но не менее важное: обновите файл src/App.vue, чтобы отправить запрос login и вывести результат. Помните, что порядок запросов будет таким: GET /api/csrf‑cookie, POST /auth/spa/login и GET /api/user. Мы уже знаем, чего ожидать от каждого из этих запросов, поскольку ранее выполняли их в Postman.

<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
import { onBeforeMount } from 'vue'
import axios from '@/plugins/axios'

onBeforeMount(async () => {
  await axios.post('/auth/spa/login', {
    email: 'test@email.com',
    password: 'password'
  })

  const { data } = await axios.post('/user')

  console.log(data)
})
</script>

Пока все выглядит хорошо. Давайте протестируем это в браузере и посмотрим, что получится!

CORS error. Как мы ее получили?
Хотя все работает на Postman, поведение в браузере немного отличается. Вот более полное объяснение:
Vue‑приложение приходит с localhost:5173 и оно пытается отправить запрос на localhost:8000, который является другим host«ом. Браузер отправит preflight запрос на сервер, чтобы проверить, разрешен ли следующий запрос (GET /api/csrf-cookie) настройками CORS сервера. Если разрешение получено, браузер успешно выполняет запрос, а если нет, то выдает CORS error.

Теперь вот в чем фокус: вы должны ВСЕГДА наводить курсор на сообщение CORS error, чтобы получить подробную информацию, и в нашем случае оно гласит: PreflightWildcardOriginNotAllowed, что означает, что сервер имеет подстановочный знак в качестве значения заголовка Access‑Control‑Allow‑Origin. Чтобы исправить это в Laravel, давайте попробуем обновить ключ allowed_origins в config/cors.php на [env('SANCTUM_STATEFUL_DOMAINS', '')].

return [
    // ...

    'allowed_origins' => [env('SANCTUM_STATEFUL_DOMAINS', '*')],

   // ...
];

Давайте попробуем еще раз...

Еще одна CORS error, но на этот раз по другой причине — PreflightAllowedOriginMismatch. Это связано с тем, что если мы посмотрим на примеры Access‑Control‑Allow‑Origin мы увидим, что origin состоит из схемы (https:// / http://) и хоста (localhost:5173 / madewithlove.com). Это означает, что значение SANCTUM_STATEFUL_URL здесь не подходит. Поэтому мы создадим новую переменную FRONTEND_URL=http://localhost:5173 и добавим ее в .env и .env.example и обновим файл config/cors.php.

return [
    // ...

    'allowed_origins' => [env('FRONTEND_URL', '*')],

   // ...
];

Давайте попробуем еще раз!

Итак, осталась еще одна ошибка, которая требует внимания: PreflightInvalidAllowCredentials. Чтобы исправить ее, нам нужно установить параметр supports_credentials в true в config/cors.php. Этот параметр отвечает за заголовок ответа Access‑Control‑Allow‑Credentials, который информирует браузеры о том, разрешает ли сервер HTTP‑запросам из разных источников включать учетные данные. Учетные данные могут быть представлены в виде куки или заголовков аутентификации, содержащих имя пользователя и пароль (то есть, заголовок Authorization ).

return [
    // ...

    'allowed_origins' => [env('FRONTEND_URL', '*')],

    // ...

    'supports_credentials' => true,

];

Давайте попробуем еще раз!

Ну наконец‑то!

Но что произойдет, если уже аутентифицированный пользователь попытается повторно залогиниться (возможно, по ошибке)? Давайте обновим страницу и посмотрим, что у нас получится...

Упс! Написано PreflightMissingAllowOriginHeader! Но мы же уже разобрались с этим, так что же происходит?
На самом деле, проблема связана с гостевым middleware, которое применяется к маршруту логина, который включает middleware RedirectIfAuthenticated, и когда пользователь снова логинится, это middleware перенаправляет его на маршрут /. Нам необходимо отключить его, так как мы не хотим никаких перенаправлений в нашем API.

Если вы используете Laravel 10 или ниже, откройте файл app/Http/Middleware/RedirectIfAuthenticated.php и обновите метод handle, чтобы он возвращал ответ JSON вместо перенаправления:

class RedirectIfAuthenticated
{
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                if ($request->expectsJson()) {
                    return response()->json(['message' => __('Already Authenticated')], 403);
                }
                
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
}

Пользователям Laravel 11 необходимо внести в файл bootstrap/app.php следующие изменения:

use App\Exceptions\AlreadyAuthenticatedException;
// ... 
use Illuminate\Http\Request;

    // ...

    ->withMiddleware(function (Middleware $middleware) {
        $middleware->statefulApi();

        $middleware->redirectUsersTo(function (Request $request) {
            if ($request->expectsJson()) {
                throw new AlreadyAuthenticatedException();
            }

            return '/';
        });
    })

// ...

Как видите, мы пробросим пользовательское исключение, когда пользователь попытается залогиниться, если он уже авторизован. Теперь давайте создадим это пользовательское исключение:

php artisan make:exception AlreadyAuthenticatedException

Вот реализация:

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Http\JsonResponse;

class AlreadyAuthenticatedException extends Exception
{
    public function render(): JsonResponse
    {
        return response()->json(['message' => __('Already Authenticated')], 403);
    }
}

Да, я понимаю, о чем вы могли подумать. Зачем нам все это? Почему мы добавили оператор return непосредственно в файл?

Отличный вопрос! Значение, возвращаемое методом redirectUsersTo, будет использоваться в middleware RedirectIfAuthenticated следующим образом:

    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if (Auth::guard($guard)->check()) {
                return redirect($this->redirectTo($request)); // 👈🏻👈🏻👈🏻 используется в качестве аргумента для функции `redirect`
            }
        }

        return $next($request);
    }

    /**
     * Получаем путь, на который пользователь должен быть перенаправлен при аутентификации.
     */
    protected function redirectTo(Request $request): ?string
    {
        return static::$redirectToCallback
            ? call_user_func(static::$redirectToCallback, $request)
            : $this->defaultRedirectUri();
    }

Вот почему должно быть именно так, по крайней мере, на данный момент. Вы можете настроить его по своему усмотрению, не стесняйтесь выбирать то, что вам больше подходит. А пока давайте проверим результат.

Идеально!

Что делать, если серверная часть уже развернута, а фронтенд еще локальный?

Сначала я воссоздам эту ситуацию. Раньше я запускал проект с помощью команды php artisan serve, но теперь я буду использовать Laragon для его размещение на домене sanctum‑cookie.test. Для этого просто поместите папку проекта в каталог www Laragon и нажмите кнопку перезагрузки!

Примечание: sanctum‑cookie — это имя папки проекта внутри папки www, поэтому sanctum‑cookie.test — это домен для него.

Далее я изменю параметр VITE_BACKEND_URL в .env файле на http://sanctum‑cookie.test/api. Теперь я очищу куки и попробую еще раз.

​Первый запрос к /api/csrf‑cookie работает, но запрос на вход в систему — нет.

Мы видим CSRF token mismatch, что, очевидно, означает, что не был отправлен или отправлен с неверным значением заголовок X‑XSRF‑TOKEN. Давайте проверим заголовки запроса.

И, конечно, его там нет. Если вы помните, этот заголовок получает свое значение из куки XSRF‑TOKEN, который устанавливается при запросе на адрес /api/csrf‑cookie. Это значит, что нам нужно взглянуть на заголовки ответа на этот запрос, хотя похоже, что он работает.

​Как мы можем увидеть, наведя курсор на предупреждающий знак, браузеру не удалось установить куки, поскольку для samesite установлен lax. Это означает, что сервер разрешает устанавливать куки только для того же домена верхнего уровня, о котором мы упоминали при настройке фронтенд домена. Теперь, когда мы знаем причину проблемы, давайте перейдем к ее решению. Мы будем использовать обратный прокси (reverse proxy), который разместит оба приложения на одном домене. В нашем случае фронтенд будет на localhost:5371, а серверная часть — на localhost:5371/api. Мы сделаем это, не затрагивая Laragon или серверную часть. Для этого нам нужно настроить прокси‑сервер в файле vite.config.ts.

import { fileURLToPath, URL } from 'node:url'

import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default ({ mode }) => {
  process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }

  return defineConfig({
    plugins: [vue()],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    },
    server: {
      proxy: {
        '/api': process.env.VITE_BACKEND_URL
      }
    }
  })
}

Мы еще не закончили, но я хочу показать вам почему. Давайте попробуем проверить браузер.

Мы получили 404 потому что прокси‑сервер /api указан на http://sanctum‑cookie.test/api что означает, что при запросе http://localhost:5173/api/csrf‑cookie он становится http://sanctum‑cookie.test/api/api/csrf‑cookie!

Наш последний шаг — изменить параметр VITE_BACKEND_URL в .env‑файле на http://sanctum‑cookie.test и перезапустить приложение, поскольку изменения в .env не вступают в силу сразу.

Идеально! Эта проблема очень распространена, надеюсь, она больше вас не побеспокоит.

Как нам включить токены API для мобильной разработки с текущими настройками?

Это довольно просто: добавьте трейт HasApiKeys в модель пользователя и затем реализуйте новый маршрут login в систему, как показано ниже:

// routes/api.php
use App\Http\Controllers\Api\Auth\Mobile\LoginController;

Route::prefix('auth/mobile')->group(function (){
    Route::post('login', LoginController::class)->middleware('guest');
});

// Mobile\LoginController
public function __invoke(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $token = $request->user()->createToken($request->token_name);

            return response()->json(['message' => __('Welcome!'), 'token' => $token->plainTextToken]);
        }

        throw ValidationException::withMessages([
            'email' => __('The provided credentials do not match our records.'),
        ]);
    }

Готово! Вам не нужно выполнять никаких дополнительных шагов, так как Sanctum будет использовать заголовок Authorization, если не обнаружит куки аутентификации. И последнее, на что следует обратить внимание: убедитесь, что вы не отправляете заголовок Referer из мобильного приложения, так как это может ввести Sanctum в заблуждение.

Заключение

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

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

Спасибо за внимание!


Если вы дошли до этой строчки, то вы точно готовы идти дальше. Статья подготовлена в рамках онлайн‑курса Framework Laravel. На странице курса можно ознакомиться с полной программой, а также посмотреть записи открытых уроков.

Каждый день в Otus проходят открытые уроки — переходите в календарь мероприятий и выбирайте интересующие темы.

Теги:
Хабы:
+11
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS