Как аутентифицировать пользователей с ключами API
Дата обновления перевода 2023-07-24
Как аутентифицировать пользователей с ключами API
Tip
Посмотрите статью Система користувацької аутентифікації з Guard (приклад API токена), чтобы узнать о более простом гибком способе выполнить такие пользовательские задачи аутентификации, как эта.
На сегодняшний день, достаточно необычно аутентифицировать пользователя через ключ API (например, при разработке веб-сервиса). Ключ API предоставляется для каждого запроса и передаётся в качестве параметра строки запроса или через HTTP-заголовок.
Аутентификатор ключа API
Аутентификация пользователя на основании информации запроса должна быть проведена с помощью механизма предварительной аутентификации. SimplePreAuthenticatorInterface позволяет вам с лёгкостью реализовывать такую схему.
Ваша конкретная ситуация может отличаться, но в этом примере, токен считывается
из параметра запроса apikey
, правильное имя пользователя загружается из этого
значение, а потом создаётся объект Пользователь:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
// src/Security/ApiKeyAuthenticator.php
namespace App\Security;
use App\Security\ApiKeyUserProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
// искать параметр запроса apikey
$apiKey = $request->query->get('apikey');
// или, если вы хотите использовать заголовок "apikey", то сделайте что-то вроде этого:
// $apiKey = $request->headers->get('apikey');
if (!$apiKey) {
throw new BadCredentialsException();
// или, чтобы просто пропустить аутентификацию ключа api
// вернуть null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// ВНИМАНИЕ: это сообщение будет возвращено клиенту
// (так что не вводите здесь недоверенные сообщения / строки ошибок)
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
}
Как только вы всё сконфигурируете, вы сможете
аутентифицировать путём добавления параметра apikey parameter в строку запроса,
как http://example.com/api/foo?apikey=37b51d194a7513e45b56f6524f2d51f2
.
Процесс аутентификации имеет несколько шагов и ваша реализация скорее всего будет отличаться:
1. createToken
На раннем этапе цикла запроса, Symfony вызывает createToken()
. Ваша задача здесь
- создать объект токена, который содержит всю информацию из запроса, которая вам нужна
для аутентификации пользователя (например, параметр запроса apikey
). Если этой информации
нет, вызов исключения BadCredentialsException
приведёт к неудаче аутентификации. Лучше вернуть null
вместо того, чтобы просто
пропускать аутентификацию, чтобы Symfony могла использовать резервный метод аутентификации,
если он существует.
Caution
В случае, если вы возвращаете null
из вашего метода createToken()
,
Symfony передаёт этот запрос следующему проводнику аутентификации. Если вы
не сконфигурировали никакого другого проводника, включите опцию anonymous
в вашем брандмауэре. Таким образом, Symfony выполняет анонимного проводника
аутентификации, и вы получите AnonymousToken
.
2. supportsToken
Після того, як Symfony викличе createToken()
, вона викличе supportsToken()
у вашому класі (і будь-яких інших слухачів аутентифікації), щоб з'ясувати, хто
має працювати з токеном. Це просто спосіб дозволити кільком механізмам
автентифікації бути використаними для одного брендмауера (отже, ви,
наприклад, можете спочатку спробувати автентифікувати користувача через сертифікат
або API-ключ, а як резерв - через форму входу).
В основному, вам потрібно просто переконатися, що цей метод повертає "true" для
токена, який був створений createToken()
. Ваша логіка, швидше за все, має
виглядати так само, як цей приклад.
3. authenticateToken
Если supportsToken()
возвращает true
, Symfony вызовет authenticateToken()
.
Ключевым моментом является $userProvider
- внешний класс, который помогает вам
загружать информацию о пользователе. Вы узнаете больше о нём далее.
В этом конкретном примере, в authenticateToken()
происходит следующее:
- Во-первых, вы используете
$userProvider
чтобы каким-то образом найти$username
, соответствующий$apiKey
; - Во-вторых, вы снова используете
$userProvider
, чтобы загрузить или создать объектUser
для$username
; - Наконец, вы создаёте токен аутентификации (т.е. токен как минимум с одной ролью), который имеет правильные роли и присоединённый объект Пользователя (User).
Целью является использование $apiKey
для того, чтобы найти или создать объект User
.
Как вы это сделаете (например, запрос в DB) иточный класс вашего объекта User
могут
разниться. Эти отличия будут наиболее очевидны в вашем поставщике пользователя.
Поставщик пользователя
$userProvider
может быть любим поставщиком пользователя (см. Як створити користувацького постачальника користувачів).
В этом примере, $apiKey
используется, чтобы как-то найти имя пользователя для пользователя.
Эта работа проводится в методе getUsernameForApiKey()
, который полностью создаётся для этого
случая использования (т.е. это не метод, который используется базовой системой поставщика пользователей
Symfony).
$userProvider
может выглядеть как-то так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
// src/Security/ApiKeyUserProvider.php
namespace App\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
public function getUsernameForApiKey($apiKey)
{
// Искать имя пользователя на основании токена в DB через
// вызов API, или сделать что-то абсолютно другое
$username = ...;
return $username;
}
public function loadUserByUsername($username)
{
return new User(
$username,
null,
// роли пользователя - вы можете решить определить
// их как-то динамически, основываясь на пользователе
array('ROLE_API')
);
}
public function refreshUser(UserInterface $user)
{
// это используется для сохранения аутентификации в сессии
// но в этом примере, токен отправляется в каждом запросе,
// так что аутентификация может быть без запоминания состояния.
// Вызов этого исключения правильный для того, чтобы сделать всё
// без запоминания состояния
throw new UnsupportedUserException();
}
public function supportsClass($class)
{
return User::class === $class;
}
}
Далее, убедитесь, что этот класс зарегистрирован, как сервис. Если вы используете конфигурацию services.yaml по умолчанию , то это происходит автоматически. Немного позже, вы будете ссылаться на этот сервис в вашей конфигурации security.yaml.
Note
Прочитайте соответствующую статью, чтобы узнать, как создать пользовательского поставщика пользователей.
Логика внутри getUsernameForApiKey()
может быть на ваш вкус. Вы можете как-либо
трансформировать ключ API (например, 37b51d
) в имя пользователя (например, jondoe
),
поискав какую-то информацию в таблице DB "токен".
То же самое относится к loadUserByUsername()
. В этом примере, базовый класс Symfony
User просто создаётся. Это имеет смысл,
если вам не нужно хранить дополнительной информации о вашем объекте пользователя (например,
firstName
). Но если вам это нужно, у вас может быть ваш собственный класс пользователя,
который вы создаёте и наполняете путём запросов в DB. Это позволит вам иметь пользовательские
данные в объекте User
.
Наконец, просто убедитесь, что supportsClass()
возвращает true
для объектов
Пользователь, с тем же классом, как и те пользователи, которых вы возвращаете в
loadUserByUsername()
.
Если ваша аутентификация без запоминания состояния, как в этом примере, (т.е.
вы ожидаете, что пользователь будет отправлять ключ API с каждым запросом, и
поэтому вы не сохраняете логин в сессии), то вы можете просто выдать исключение
UnsupportedUserException
в refreshUser()
.
Note
Если вы хотите хранить данные аутентификации в сессии так, чтобы ключ не надо было отправлять по каждому запросу, смотрите Як аутентифікувати користувачів з ключами API.
Обработка неудачи аутентификации
Для того, чтобы ваш ApiKeyAuthenticator
правильно отображал http-статус 401
при неудаче аутентификации или неправильной аккредитации, вам понадобится реализовать
AuthenticationFailureHandlerInterface
в вашем Аутентификаторе. Это предоставит метод onAuthenticationFailure()
, который вы
можете использовать для создания ошибки Response
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// src/Security/ApiKeyAuthenticator.php
namespace App\Security;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
// ...
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// содержит информацию о том, *почему* не удалась аутентификация
// используйте это, или верните ваше собственное сообщение
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
Конфигурация
Когда у вас будет полностью настроен ApiKeyAuthenticator
, вам нужно будет зарегистрировать
его как сервис. Если вы используете конфигурацию services.yaml по умолчанию ,
то это случится автоматически.
Последний шаг - активация вашего аутентификатора и пользовательского поставщика
пользователей в разделе firewalls
вашей конфигурации безопасности, используя
ключи simple_preauth
и provider
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/security.yaml
security:
# ...
providers:
api_key_user_provider:
id: App\Security\ApiKeyUserProvider
firewalls:
main:
pattern: ^/api
stateless: true
simple_preauth:
authenticator: App\Security\ApiKeyAuthenticator
provider: api_key_user_provider
Если вы определили access_control
, обязательно добавьте новую запись:
1 2 3 4 5 6
# config/packages/security.yaml
security:
# ...
access_control:
- { path: ^/api, roles: ROLE_API }
Вот и всё! Теперь, ваш ApiKeyAuthenticator
должен вызываться в начале каждого
запроса, после чего будет происходить ваш процесс аутентификации.
Параметр конфигурации stateless
предотвращает Symfony от попыток сохранить
информацию аутентификации в сессии, что необязательно, так как клиент будет
отправлять apikey
по каждому запросу. Если вам нужно сохранить аутентификацию
в сесии, то продолжайте читать!
Хранение аутентификации в сессии
До этих пор, эта статья описывала ситуацию, где некоторый токен аутентификации отправляется по каждому запросу. Но в некоторых ситуациях (как в потоке OAuth), токен может быть отправлен только по одному запросу. В этом случае, вы захотите аутентифицировать пользователя и хранить эту аутентификацию в сессии так, чтобы пользователь автоматически выполнял вход в каждом последующем запросе.
Чтобы это работало, для начала, удалите ключ stateless
из конфигурации вашего
брандмауэра или установите его как false
:
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
# ...
firewalls:
secured_area:
pattern: ^/api
stateless: false
# ...
Нессмотря на то, что токен хранится в сессии, аккредитация - в этом случае ключ API
(т.е. $token->getCredentials()
) - не хранится в сессии по причинам безопасности.
Чтобы воспользоваться преимуществами сессии, обновите ApiKeyAuthenticator
, чтобы
увидеть, имеет ли сохранённый токен валидный объект Пользователь, который можно использовать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// src/Security/ApiKeyAuthenticator.php
// ...
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
// ...
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
// User - это сущность, которая представляет вашего пользователя
$user = $token->getUser();
if ($user instanceof User) {
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
if (!$username) {
// это сообщение будет возвращено клиенту
throw new CustomUserMessageAuthenticationException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByUsername($username);
return new PreAuthenticatedToken(
$user,
$apiKey,
$providerKey,
$user->getRoles()
);
}
// ...
}
Сохранение информации аутентификации в сесси работает так:
- В конце каждого запроса, Symfony сериализирует объект токена (возвращённого из
authenticateToken()
), который также сериализирует объектUser
(так как он установлен в свойстве токена); - В следующем запросе токен десериализируется и десерилизованный объект
User
передаётся функцииrefreshUser()
поставщика пользователя.
Второй шаг очень важен: Symfony вызывает refreshUser()
и передаёт вам
объект пользователя, который был сериализован в сессии. Если ваши пользователи
хранятся в DB, то вы можете захотеть повторно запросить свежую версию пользователя,
чтобы убедиться, что он не устарел. Но вне зависимости от ваших требований,
refreshUser()
теперь должен возвращать объект пользователя:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Security/ApiKeyUserProvider.php
// ...
class ApiKeyUserProvider implements UserProviderInterface
{
// ...
public function refreshUser(UserInterface $user)
{
// $user - это User, который вы установили в токене внутри authenticateToken()
// после того, как он был десериализован из сессии
// вы можете использовать $user, чтобы запросить свежего пользователя у DB
// $id = $user->getId();
// используйте $id, чтобы сделать запрос
// если вы *не* считываете с DB и просто создаёте
// объект User (как в этом примере), вы можете просто вернуть его
return $user;
}
}
Note
Вы также захотите убедиться, что ваш объект User
сериализируется правильно.
Если ваш объект User
имеет частные свойства, PHP не может их сериализовать.
В таком случае, вы можете получить обратно объект Пользователя, который имеет
значение null
для каждого свойства. Чтобы увидеть пример, смотрите Як завантажувати користувачів безпеки з DB (постачальник сутностей).
Аутентификация только для определённых URL
Эта статья предполагала, что вы хотите искать аутентификацию apikey
в
каждом запросе. Но в некоторых ситуациях (как в потоке OAuth), вам нужно
на самом деле искать информацию аутентификации только тогда, когда пользователь
достиг определённого URL (например, URL перенаправления в OAuth).
К счастью, справиться в этой ситуацией легко: просто проверьте, какой текущий URL
перед тем, как создавать токен в createToken()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// src/Security/ApiKeyAuthenticator.php
// ...
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\Request;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
protected $httpUtils;
public function __construct(HttpUtils $httpUtils)
{
$this->httpUtils = $httpUtils;
}
public function createToken(Request $request, $providerKey)
{
// установите один URL, где мы должны искать информацию авторизации
// и возвращать токен только, если мы на этом URL
$targetUrl = '/login/check';
if ($request->getPathInfo() !== $targetUrl)
return;
}
// ...
}
}
Вот и всё! Повеселитесь!