Всем привет!

Вчера мне пришлось немного поломать голову над тем, как совместить использование CSRF и JWT с минимальными изменениями в рабочем коде. Как известно, использование CSRF токена в Laravel настроено по умолчанию, и при POST запросах ajax используется следующая схема.

Сам токен записывается в meta тег csrf-token, верно? Это стандартно в Laravel, упоминания об этом можно найти сразу же в разделе Routing в документации. Однако все хорошо до поры до времени — эта схема нерабочая, если ваша система должна поддерживать Rest интерфейсы, например, для доступа с мобильного приложения.

В Packagist  есть очень хорошая библиотека для работы с токенами с помощью JWT Auth. Подробно узнать что такое JSON Web Token можно здесь, попробую описать кратко, буквально в пару слов что это за зверь и как мы его можем использовать.

JWT это открытый стандарт обмена данными между ресурсами в качестве JSON объекта. Эта информация считается проверенной и ей можно доверять. Указанная выше библиотека берет на себя всю логику обработки, проверки и подтверждения полученных запросов, в настройках можно указать время жизни токена, а уже в готовых Middleware процедуры проверки подлинности и обновления токена, если он истек.

В общем, эта библиотека берет на себя весь геморой, все что остается нам — это правильно определять, в какой момент нам нужно использовать проверку CSRF, а в какой — JWT.

Итак, ближе к делу: давайте пройдем по этапам. Первый — это поддержка многочисленных приложений. Вообще, я придерживаюсь такого подхода, что если уж и создавать REST API, то нужно иметь возможность создания большого количества приложений — для Android, iOS и проч., благо сейчас мы живем в такое время, когда ограничивать себя в таком выборе сложно.

Миграция для создания таблицы с приложениями выглядит следующим образом:

Индексы я не стал указывать, это, если хотите, небольшое домашнее задание по оптимизации. Хотя, это конечно имеет смысл, если вы закладываете в основу предположение, что у вас будет ОЧЕНЬ большое количество приложений. По факту (вы увидите это ниже), запрос на определение приложения будет храниться в сессии и выполняться при каждом запросе ему не обязательно.

Не забудьте создать данные для тестирования.

Ну, а теперь самое сладкое — нужно организовать работу Middleware так, чтобы определение, с каким токеном работать, было наиболее прозрачным. Сразу скажу, мое решение лично мне кажется не совсем причесанным, но пришлось красотой кода немного пожертвовать, потому что решить надо было как можно срочнее.

Итак, первым делом, мы должны убедиться, что VerifyCsrfToken у нас находится в списке middleware, которые выполняться будут при каждом запросе. Узнать это очень просто — достаточно открыть app/Kernel.php и удостовериться, что этот класс находится в массиве $middleware

Да, кстати, не стоит бездумно копировать этот код — в этом списке есть, например, класс, который по дефолту отсутствует (подсказка — начинается на time, заканчивается на zone).

Затем переходим к самому главному — правим класс VerifyCsrfToken под свои нужды.

Объяснения.

При каждом запросе необходимо передавать Header со значением Application.secret, который вы получите, создав свое первое приложение. Если честно, я не стал особо заморачиваться, как разделять REST запросы от Ajax, возможно, есть более элегантное решение с помощью встроенных функций Laravel, но я решил остановиться именно на таком варианте.

Я в принудительном порядке устанавливаю этот header запросу, чтобы $request -> wantsJson() всегда возвращал true. Это не в сегодняшней теме, но в проекте я добавил перехват ошибок ajax и rest и возвращение их в Json, а не в дефолтном html варианте. Поэтому если вас это не заботит (или у вас это не реализовано), можете этот момент пропустить.

Если пользователь неавторизован, о каких токенах может идти речь, верно? Поэтому в этом случае мы перехватываем дальнейший вызов проверки JWT и возвращаем 401ю ошибку. Кстати, если у вас Exceptions/Handler.php нетронутый, то получать вы будете html страницу со стектрейсом (если у вас дебаг включен, конечно).

Оставшийся код обрабатывает JWT и определяет, годный ли получен токен со стороны, или нет.

Какие есть минусы на данный момент у этого решения.

  1. При каждом запросе производится проверка на существование приложения с таким secret в базе. Я бы хотел записывать эту информацию в сессию, но во-первых, не уверен, будет ли она сохраняться для REST запросов, а во-вторых, запись в сессию до выполнения $next($request) невозможна.

О, только один минус, это вроде как неплохо!

P.S. Я за то, чтобы закрыть с помощью CSRF и JWT абсолютно все запросы, поэтому я переписал базовый класс BaseVerify следующим образом.

Метод $this -> isReading($request) определяет тип запроса. По умолчанию, при запросах GET проверки на токен нет.