Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Доступ к 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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.