Авторизация в системе
Как мы уже выяснили в прошлой заметке, пользователь авторизовывается в контроллерах при каждем запросе с помощью токена 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
👍
👎
❤️
🔥
😮
😢
😀
😡
280
06.06.2022, 10:22:22
Комментарии
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Александр Наумов
23.07.2024, 00:20:37
Василий, спасибо большое!!
Vesp 3.0
101
Василий Наумкин
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
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Релиз @vesp/nuxt-fontawesome
3
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.
Новая структура таблиц магазина
15
Василий Наумкин
22.01.2024, 07:43:20
Давай-давай!
Начинаем новый курс
4
Василий Наумкин
24.12.2023, 14:26:13
Спасибо!
Запуск в продакшн с помощью Docker
20