Доступ к API по токену
У API нет формы входа и сессии: программа-клиент не хранит cookie. Вместо этого она с каждым запросом присылает токен — длинную случайную строку, выданную пользователю заранее. Приложение по токену определяет, кто пришёл. Проверку выполняет посредник.
Таблица токенов
-- database/schema.sql
CREATE TABLE api_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE
);Как и пароль, токен хранят в виде хеша. Выдайте токен первому пользователю скриптом:
// bin/create-token.php
$app = require __DIR__ . '/../config/bootstrap.php';
$token = bin2hex(random_bytes(32));
$app->getContainer()->get(PDO::class)
->prepare('INSERT INTO api_tokens (user_id, token_hash) VALUES (?, ?)')
->execute([(int) $argv[1], hash('sha256', $token)]);
echo $token . "\n";php bin/create-token.php 1Команда напечатает токен — его видно только сейчас, в базе остаётся хеш.
Поиск по токену
// src/Repository/TokenRepository.php
final class TokenRepository
{
public function __construct(private PDO $pdo) {}
public function findUserId(string $token): ?int
{
$statement = $this->pdo->prepare('SELECT user_id FROM api_tokens WHERE token_hash = ?');
$statement->execute([hash('sha256', $token)]);
$userId = $statement->fetchColumn();
return $userId === false ? null : (int) $userId;
}
}Посредник
Клиент передаёт токен в заголовке Authorization: Bearer токен. Посредник читает заголовок и либо пропускает запрос, либо сам отвечает кодом 401:
// src/Middleware/TokenAuthMiddleware.php
final class TokenAuthMiddleware implements MiddlewareInterface
{
public function __construct(
private TokenRepository $tokens,
private ResponseFactoryInterface $responseFactory,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$header = $request->getHeaderLine('Authorization');
$userId = str_starts_with($header, 'Bearer ')
? $this->tokens->findUserId(substr($header, 7))
: null;
if ($userId === null) {
return Json::write($this->responseFactory->createResponse(), ['error' => 'Unauthorized'], 401);
}
return $handler->handle($request->withAttribute('user_id', $userId));
}
}Номер пользователя кладётся в атрибут запроса — действие узнаёт автора через $request->getAttribute('user_id'), а не из присланных данных.
Привязка к маршрутам
Чтение оставим открытым, а создание и удаление закроем вложенной группой:
// config/routes.php
$app->group('/api/posts', function (RouteCollectorProxy $group) {
$group->get('', Api\ListPostsAction::class);
$group->get('/{id:[0-9]+}', Api\ShowPostAction::class);
$group->group('', function (RouteCollectorProxy $protected) {
$protected->post('', Api\CreatePostAction::class);
$protected->delete('/{id:[0-9]+}', Api\DeletePostAction::class);
})->add(TokenAuthMiddleware::class);
});Проверка
curl -i -X DELETE http://localhost:8080/api/posts/1
curl -i -X DELETE http://localhost:8080/api/posts/1 -H "Authorization: Bearer ваш-токен"Первый запрос вернёт код 401, второй — 204.
JWT
Другой распространённый вид токена — JWT. Это строка, в которую зашиты номер пользователя и срок действия, заверенные подписью. Приложение проверяет подпись и не обращается к базе. Работает с ними пакет firebase/php-jwt:
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$token = JWT::encode(['sub' => 1, 'exp' => time() + 3600], $secret, 'HS256');
$payload = JWT::decode($token, new Key($secret, 'HS256'));$secret — длинная случайная строка из .env. При неверной подписи или истёкшем сроке decode() бросает исключение. Посредник остаётся тем же, меняется только способ проверки. Цена удобства: выданный JWT нельзя отозвать до истечения срока, а токен из таблицы достаточно удалить.
Частая ошибка
Токен в адресе — /api/posts?token=... — попадает в журналы веб-сервера, историю браузера и заголовок Referer. Передавайте токен только в заголовке Authorization и только по HTTPS.