Авторизация в системе
Как мы уже выяснили в прошлой заметке, пользователь авторизовывается в контроллерах при каждем запросе с помощью токена JWT, который передаётся или в заголовках, или в GET параметре, или получается из куки.
Теперь давайте посмотрим, как происходит получение этого токена пользователем.
Согласно нашему route.php запросы на авторизацию принимаются по адресу api/security/login контроллером App\Controllers\Security\Login, который расширяет Vesp\Controllers\Security\Login:
<?php
// Это оригинальный контроллер из vesp/core
namespace Vesp\Controllers\Security;
use Psr\Http\Message\ResponseInterface;
use Vesp\Controllers\Controller;
use Vesp\Helpers\Jwt;
use Vesp\Models\User;
class Login extends Controller
{
// Контроллер работает с моделью User
protected $model = User::class;
// Обращаться можно только методом POST
public function post(): ResponseInterface
{
// для авторизации требуются username и password
$username = trim($this->getProperty('username', ''));
$password = trim($this->getProperty('password', ''));
// Выбираем пользователя по username
$user = (new $this->model())->newQuery()->where('username', $username)->first();
// Если есть такой, и указан верный пароль
if ($user && $user->verifyPassword($password)) {
// Проверяем его статус, и возвращаем токен, или ошибку
return !$user->active
? $this->failure('This user is not active', 403)
: $this->success(['token' => Jwt::makeToken($user->id)]);
}
// Авторизовать не удалось - возвращаем ошибку
return $this->failure('Wrong username or password');
}
}
Как видите, оригинальный контроллер очень прост, и сделан скорее для примера, нежели для реальной работы.
Давайте еще посмотрим как генерируется токен в Vesp\Helpers\JWT:
public static function makeToken(int $id, array $add = []): string
{
// В токен пишется только самый минимум информации
$time = time();
$data = [
'id' => $id, // id пользователя
'iat' => $time, // время выпуска токена
'exp' => $time + getenv('JWT_EXPIRE'), // и срок годности, в зависимости от настроек
];
// Кодирование токена по стандарту занимается сторонная библиотека
// В качестве секретной подписи используется наша настройка из .env
return FirebaseJWT::encode(array_merge($data, $add), getenv('JWT_SECRET'));
}
Метод написан таким образом, чтобы вы могли передавать в токен дополнительные данные вторым параметром, если нужно.
Обратите внимание, что JWT токены предназначены для декодировани и не должны содержать секретную информацию. Более подробно про JWT можно почитать здесь, там же можно кодировать и декодировать токены, если что.
С оригинальным контроллером всё понятно: шлём через POST логин и пароль, в ответ получаем токен, который потом используем для доступа в другие контроллеры.
Расширенная авторизация
Однако Vesp предлагает использовать расширенный контроллер, который добавляет хранение выданных токенов в базе с помощью UserToken:
<?php
namespace App\Controllers\Security;
use App\Models\UserToken;
use Psr\Http\Message\ResponseInterface;
use Vesp\Helpers\Jwt;
// Наш контроллер расширяет оригинальный из vesp/core
class Login extends \Vesp\Controllers\Security\Login
{
// Тот же метод POST
public function post(): ResponseInterface
{
// Получаем ответ из оригинального контроллера
$response = parent::post();
$code = $response->getStatusCode();
// Работаем только если юзер был проверен без ошибок
if ($code === 200) {
// Декодируем содержимое токена
$token = json_decode($response->getBody()->__toString(), false)->token;
if ($decoded = JWT::decodeToken($token)) {
// И сохраняем токен в БД
(new UserToken(
[
'user_id' => $decoded->id,
'token' => $token,
'valid_till' => date('Y-m-d H:i:s', $decoded->exp),
// Вот тут-то нам и пригодился akrabat/ip-address-middleware
// для сохранения IP пользователя
'ip' => $this->request->getAttribute('ip_address'),
]
))->save();
// Если в настройках указано максимальное количество активных токенов
if ($max = getenv('JWT_MAX')) {
$query = UserToken::query()->where(['user_id' => $decoded->id, 'active' => true]);
// И юзер уже превысил этот лимит
if ($query->count() > $max) {
// Ищем самый старый активный токен
if ($result = $query->orderBy('updated_at')->orderBy('created_at')->first()) {
// и отключаем его
$result->update(['active' => false]);
}
}
}
}
} else {
// Так как vesp/vesp в отличие от vesp/core есть фронтенд
// заменяем стандартные сообщения об ошибках на строки из лекикона
// Про лексиконы потом поговорим отдельно
if ($code === 403) {
return $this->failure('errors.security.inactive');
}
if ($code === 422) {
return $this->failure('errors.security.wrong');
}
}
// Возвращаем ответ
return $response;
}
}
Как вы видите из кода, расширенный контроллер никак не меняет успешный ответ от родительского контроллера, он только:
- сохраняет выданный токен в БД
- отключает старые токены, если их стало слишком много
Забегая вперёд, старые токены затем удаляются через cron консольным скриптом в core/cli/clear-tokens.php:
UserToken::query()
->where('valid_till', '<', date('Y-m-d H:i:s'))
->orWhere('active', false)
->delete();
На предыдущем уроке мы уже разобрали, что при загрузке пользователя в App\Middlewares\Auth проверяется именно наличие UserToken в базе данных, поэтому удаление токенов пользователя заставит его авторизоваться повторно.
Это позволяет:
- немедленно блокировать доступ юзерам в систему
- менять им пароли с требованием повторной авторизации
- ограничивать количество одноременно авторизованных устройств
А если нужно, вы можете изменить расширенный контроллер и добавить какую-то свою логику.
Минутка практики
Давайте теперь авторизуемся в нашей системе! Напоминаю, что после засеивания теблиц у на есть 2 пользователя: user и admin, пароли совпадают с именами.
Запросы на авторизацию толжны быть отправлены на http://vesp-shop.test/api/security/login, но метод GET в контроллер отсутствует, нужен POST.
Для этого можно использовать cURL:
curl -X POST -F 'username=admin' -F 'password=admin' \
http://vesp-shop.test/api/security/login
В ответ прилетает JSON с полем token:
Дальше мы можем использовать этот токен для работы с закрытыми контроллерами. Например, получить профиль владельца токена по адресу http://vesp-shop.test/api/user/profile:
Именно так работает фронтенд при авторизации пользователя. Делает один запрос для получаения токена, а затем загружает и сохраняет данные пользователя с этим токеном.
Именно поэтому ничего кроме id и временных отметок в токен пользователя нам писать не нужно.
Выход из системы
В отличи vesp/core выхода из системы быть не может, потому что в нём нет и хранения токенов в базе.
А в нашей расширенной версии появляется возможность завершить сеанс отключением текущего токена:
<?php
namespace App\Controllers\Security;
use App\Models\UserToken;
use Psr\Http\Message\ResponseInterface;
use Vesp\Controllers\Controller;
// У vesp/core этой логики нет, так что и расшрять нечего
class Logout extends Controller
{
// Для обращения к этому контроллеру юзер должен быть
// авторизован в системе
protected $scope = 'profile';
public function post(): ResponseInterface
{
// Ищем текущий токен юзера
$user_token = $token = UserToken::query()
->where([
'user_id' => $this->user->id,
// Вот для этого наш App\Middlewares\Auth и
// добавлял токен в объект запроса
'token' => $this->request->getAttribute('token')
])
->first();
// Если нашли - отключаем
if ($user_token) {
$token->active = false;
$token->save();
}
// Ответом всегда код 200
return $this->success();
}
}
Вот и всё, отключенный токен будет позже удалён через cron.
Можно попробовать завершить сессию из консоли так же, методом POST. Только теперь нужно добавить заголовок авторизации:
curl -X POST --header "Authorization: Bearer ваштокен" \
http://vesp-shop.test/api/security/logout
Как видно на картинке, первый запрос прошёл без ошибки, токен был отключен. А второй запрос с тем же токеном уже говорит, что пользователь не авторизован.
Заключение
На мой взгляд логика авторизации очень простая и логичная.
Здесь не используются никакие механизмы PHP или сервера, вроде сессий. Вы просто проверяете логин с паролем, и сохраняете об этом запись в БД.
Затем эта запись проверяется при работе, а когда устарела - удаляется. При этом можно еще посмотреть IP запросов на авторизацию в БД и отключить некоторые сеансы вручную.
Сохраняется самый минимум информации, количество сенсов по умолчанию ограничено тремя - таблица не раздувается на несколько гигабайт как таблица сессий в MODX.
На этом мы закончили разбирать как работает бэкенд. Теперь вы знаете как обрабатываются запрос, как авторизуется и загружается пользователь, и как в целом работают контроллеры.
На следующем уроке мы переходим к фронтенду на VueJs и NuxtJs, и запросы будем отправлять уже оттуда.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
299
06.06.2022, 10:22:22
Комментарии
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
23.12.2024, 05:33:00
В MODX сначала создали проблему, автоматически генерируя адреса, а потом "решили" заморозкой.
Так ч...
Вывод товаров на сайте
21
Дмитрий
14.12.2024, 09:10:38
Василий, прошу прощения, тупанул, не разобрался сразу. Фреймворк отличный! "Чистый лист" на vue, рис...
Начинаем новый курс!
14
Василий Наумкин
05.12.2024, 20:01:14
В итоге основная ошибка была в неправильном общем root в Nginx, из-за чего запросы не улетали на фай...
Запуск в продакшн
55
Василий Наумкин
22.11.2024, 03:33:54
Спасибо!
День рождения 42
5
inna
06.11.2024, 15:47:13
Да. Все работает. Спасибо.
Vesp 3.0
108
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Оплата заказа
2
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Обновление проекта
2
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
Создание нового проекта
63
Василий Наумкин
20.03.2024, 21:21:52
Volledig!
Поездка в Швейцарию
8
Андрей
14.03.2024, 13:47:10
Василий! Как всегда очень круто! Моё почтение!
День рождения 41
6
Уровни подписки
Спасибо!
500 ₽ в месяц
Эта подписка ничего не даёт, просто возможность сказать спасибо за мои заметки. Подписчики отмечаются зелёненьким цветом в комментариях.
Большое спасибо!
1 000 ₽ в месяц
И эта подписка не даёт ничего, кроме оранжевого цвета в комментариях и возможности сказать спасибо, но уже большое!