Как аутентифицировать пользователей с ключами 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;
}
// ...
}
}
Вот и всё! Повеселитесь!