В этой статье вы узнаете, как настроить аутентификацию пользователей в PHP с помощью компонента Symfony Security. Помимо аутентификации, я покажу вам, как использовать авторизацию на основе ролей, которую вы можете расширить в соответствии со своими потребностями.
Компонент Symfony Security
Компонент Symfony Security позволяет легко настроить функции безопасности, такие как аутентификация, авторизация на основе ролей, токены CSRF и многое другое. Фактически, он дополнительно разделен на четыре подкомпонента, которые вы можете выбрать в соответствии со своими потребностями.
Компонент безопасности имеет следующие подкомпоненты:
- Symfony / безопасность-жильный
- Symfony / безопасность-клиент
- Symfony / безопасности CSRF
- Symfony / безопасности ACL
В этой статье мы собираемся изучить функцию аутентификации, предоставляемую компонентом symfony / security-core .
Как обычно, мы начнем с инструкций по установке и настройке, а затем рассмотрим несколько реальных примеров, чтобы продемонстрировать ключевые концепции.
Установка и настройка
В этом разделе мы собираемся установить компонент Symfony Security. Я предполагаю, что вы уже установили Composer в своей системе — он нам понадобится для установки компонента Security, доступного в Packagist.
Итак, продолжайте и установите компонент безопасности, используя следующую команду.
1
|
$composer require symfony/security
|
В нашем примере мы собираемся загрузить пользователей из базы данных MySQL, поэтому нам также понадобится уровень абстракции базы данных. Давайте установим один из самых популярных уровней абстракции базы данных: Doctrine DBAL.
1
|
$composer require doctrine/dbal
|
Это должно было создать файл composer.json , который должен выглядеть следующим образом:
1
2
3
4
5
6
|
{
«require»: {
«symfony/security»: «^4.1»,
«doctrine/dbal»: «^2.7»
}
}
|
Давайте изменим файл composer.json, чтобы он выглядел следующим образом.
01
02
03
04
05
06
07
08
09
10
11
12
|
{
«require»: {
«symfony/security»: «^4.1»,
«doctrine/dbal»: «^2.7»
},
«autoload»: {
«psr-4»: {
«Sfauth\\»: «src»
},
«classmap»: [«src»]
}
}
|
Поскольку мы добавили новую запись в classmap
, давайте продолжим и обновим автозагрузчик composer, выполнив следующую команду.
1
|
$composer dump -o
|
Теперь вы можете использовать пространство имен Sfauth
для автозагрузки классов в каталоге src .
Так что это часть установки, но как вы должны ее использовать? Фактически, это просто вопрос включения файла autoload.php, созданного Composer, в ваше приложение, как показано в следующем фрагменте.
1
2
3
4
5
|
<?php
require_once ‘./vendor/autoload.php’;
// application code
?>
|
Пример из реального мира
Во-первых, давайте пройдемся по обычному процессу аутентификации, предоставляемому компонентом Symfony Security.
- Первым делом нужно получить учетные данные пользователя и создать токен без аутентификации.
- Далее мы передадим токен без проверки подлинности менеджеру проверки подлинности для проверки.
- Менеджер аутентификации может содержать разных провайдеров аутентификации, и один из них будет использоваться для аутентификации текущего запроса пользователя. Логика аутентификации пользователя определяется в провайдере аутентификации.
- Провайдер аутентификации связывается с провайдером пользователя для его получения Ответственность за загрузку пользователей из соответствующей серверной части лежит на поставщике.
- Поставщик пользователя пытается загрузить пользователя, используя учетные данные, предоставленные поставщиком аутентификации. В большинстве случаев поставщик пользователя возвращает объект пользователя, который реализует интерфейс
UserInterface
. - Если пользователь найден, поставщик аутентификации возвращает токен без аутентификации, и вы можете сохранить этот токен для последующих запросов.
В нашем примере мы собираемся сопоставить учетные данные пользователя с базой данных MySQL, поэтому нам нужно создать провайдера пользователя базы данных. Мы также создадим провайдера аутентификации базы данных, который обрабатывает логику аутентификации. И, наконец, мы создадим класс User, который реализует интерфейс UserInterface
.
Пользовательский класс
В этом разделе мы создадим класс User, который представляет сущность пользователя в процессе аутентификации.
Создайте файл src / User / User.php со следующим содержимым.
01
02
03
04
05
06
07
08
09
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
|
<?php
namespace Sfauth\User;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $username;
private $password;
private $roles;
public function __construct(string $username, string $password, string $roles)
{
if (empty($username))
{
throw new \InvalidArgumentException(‘No username provided.’);
}
$this->username = $username;
$this->password = $password;
$this->roles = $roles;
}
public function getUsername()
{
return $this->username;
}
public function getPassword()
{
return $this->password;
}
public function getRoles()
{
return explode(«,», $this->roles);
}
public function getSalt()
{
return »;
}
public function eraseCredentials() {}
}
|
Важно то, что класс User должен реализовывать интерфейс Symfony Security UserInterface
. Кроме того, здесь нет ничего необычного.
Класс провайдера базы данных
Ответственность за загрузку пользователей из серверной части лежит на поставщике. В этом разделе мы создадим поставщика базы данных, который загружает пользователя из базы данных MySQL.
Давайте создадим файл src / User / DatabaseUserProvider.php со следующим содержимым.
01
02
03
04
05
06
07
08
09
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
|
<?php
namespace Sfauth\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\DBAL\Connection;
use Sfauth\User\User;
class DatabaseUserProvider implements UserProviderInterface
{
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
public function loadUserByUsername($username)
{
return $this->getUser($username);
}
private function getUser($username)
{
$sql = «SELECT * FROM sf_users WHERE username = :name»;
$stmt = $this->connection->prepare($sql);
$stmt->bindValue(«name», $username);
$stmt->execute();
$row = $stmt->fetch();
if (!$row[‘username’])
{
$exception = new UsernameNotFoundException(sprintf(‘Username «%s» not found in the database.’, $row[‘username’]));
$exception->setUsername($username);
throw $exception;
}
else
{
return new User($row[‘username’], $row[‘password’], $row[‘roles’]);
}
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User)
{
throw new UnsupportedUserException(sprintf(‘Instances of «%s» are not supported.’, get_class($user)));
}
return $this->getUser($user->getUsername());
}
public function supportsClass($class)
{
return ‘Sfauth\User\User’ === $class;
}
}
|
Пользовательский поставщик должен реализовать интерфейс UserProviderInterface
. Мы используем доктрину DBAL для выполнения операций, связанных с базой данных. Поскольку мы реализовали интерфейс UserProviderInterface
, мы должны реализовать loadUserByUsername
, refreshUser
и refreshUser
.
Метод loadUserByUsername
должен загружать пользователя по имени пользователя, и это делается в методе getUser
. Если пользователь найден, мы возвращаем соответствующий Sfauth\User\User
, который реализует интерфейс UserInterface
.
С другой стороны, метод refreshUser
обновляет предоставленный объект User
путем извлечения самой последней информации из базы данных.
И наконец, метод SupportClass проверяет, supportsClass
ли поставщик DatabaseUserProvider
предоставленный пользовательский класс.
Класс провайдера аутентификации базы данных
Наконец, нам необходимо реализовать поставщик аутентификации пользователя, который определяет логику аутентификации — как аутентифицируется пользователь. В нашем случае нам необходимо сопоставить учетные данные пользователя с базой данных MySQL, и, таким образом, нам необходимо соответствующим образом определить логику аутентификации.
Создайте файл src / User / DatabaseAuthenticationProvider.php со следующим содержимым.
01
02
03
04
05
06
07
08
09
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
75
76
77
78
79
|
<?php
namespace Sfauth\User;
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class DatabaseAuthenticationProvider extends UserAuthenticationProvider
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, bool $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->userProvider = $userProvider;
}
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if ($user instanceof UserInterface)
{
return $user;
}
try {
$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface)
{
throw new AuthenticationServiceException(‘The user provider must return a UserInterface object.’);
}
return $user;
} catch (UsernameNotFoundException $e) {
$e->setUsername($username);
throw $e;
} catch (\Exception $e) {
$e = new AuthenticationServiceException($e->getMessage(), 0, $e);
$e->setToken($token);
throw $e;
}
}
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$currentUser = $token->getUser();
if ($currentUser instanceof UserInterface)
{
if ($currentUser->getPassword() !== $user->getPassword())
{
throw new AuthenticationException(‘Credentials were changed from another session.’);
}
}
else
{
$password = $token->getCredentials();
if (empty($password))
{
throw new AuthenticationException(‘Password can not be empty.’);
}
if ($user->getPassword() != md5($password))
{
throw new AuthenticationException(‘Password is invalid.’);
}
}
}
}
|
Поставщик аутентификации DatabaseAuthenticationProvider
расширяет абстрактный класс UserAuthenticationProvider
. Следовательно, нам необходимо реализовать абстрактные методы retrieveUser
и checkAuthentication
.
Задача метода retrieveUser
— загрузить пользователя от соответствующего поставщика. В нашем случае он будет использовать пользовательский провайдер DatabaseUserProvider
для загрузки пользователя из базы данных MySQL.
С другой стороны, метод checkAuthentication
выполняет необходимые проверки для аутентификации текущего пользователя. Обратите внимание, что я использовал метод MD5 для шифрования пароля. Конечно, вы должны использовать более безопасные методы шифрования для хранения паролей пользователей.
Как это вообще работает
Пока что мы создали все необходимые элементы для аутентификации. В этом разделе мы увидим, как собрать все это вместе для настройки функций аутентификации.
Создайте файл db_auth.php и заполните его следующим содержимым.
01
02
03
04
05
06
07
08
09
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
|
<?php
require_once ‘./vendor/autoload.php’;
use Sfauth\User\DatabaseUserProvider;
use Symfony\Component\Security\Core\User\UserChecker;
use Sfauth\User\DatabaseAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
// init doctrine db connection
$doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection(
array(‘url’ => ‘mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}’), new \Doctrine\DBAL\Configuration()
);
// init our custom db user provider
$userProvider = new DatabaseUserProvider($doctrineConnection);
// we’ll use default UserChecker, it’s used to check additional checks like account lock/expired etc.
// you can implement your own by implementing UserCheckerInterface interface
$userChecker = new UserChecker();
// init our custom db authentication provider
$dbProvider = new DatabaseAuthenticationProvider(
$userProvider,
$userChecker,
‘frontend’
);
// init authentication provider manager
$authenticationManager = new AuthenticationProviderManager(array($dbProvider));
try {
// init un/pw, usually you’ll get these from the $_POST variable, submitted by the end user
$username = ‘admin’;
$password = ‘admin’;
// get unauthenticated token
$unauthenticatedToken = new UsernamePasswordToken(
$username,
$password,
‘frontend’
);
// authenticate user & get authenticated token
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
// we have got the authenticated token (user is logged in now), it can be stored in a session for later use
echo $authenticatedToken;
echo «\n»;
} catch (AuthenticationException $e) {
echo $e->getMessage();
echo «\n»;
}
|
Вспомните процесс аутентификации, который обсуждался в начале этой статьи — приведенный выше код отражает эту последовательность.
Первым делом нужно было получить учетные данные пользователя и создать токен без аутентификации.
1
2
3
4
5
|
$unauthenticatedToken = new UsernamePasswordToken(
$username,
$password,
‘frontend’
);
|
Затем мы передали этот токен менеджеру проверки подлинности для проверки.
1
2
|
// authenticate user & get authenticated token
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
|
Когда вызывается метод authenticate, за кулисами происходит много вещей.
Во-первых, менеджер аутентификации выбирает подходящего провайдера аутентификации. В нашем случае это поставщик аутентификации DatabaseAuthenticationProvider
, который будет выбран для аутентификации.
Затем он получает пользователя по имени пользователя от провайдера DatabaseUserProvider
. Наконец, метод checkAuthentication
выполняет необходимые проверки для аутентификации текущего запроса пользователя.
Если вы хотите протестировать скрипт db_auth.php , вам необходимо создать таблицу sf_users
в вашей базе данных MySQL.
1
2
3
4
5
6
7
8
9
|
CREATE TABLE `sf_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`roles` enum(‘registered’,’moderator’,’admin’) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO `sf_users` VALUES (1,’admin’,’21232f297a57a5a743894a0e4a801fc3′,’admin’);
|
Идите вперед и запустите скрипт db_auth.php, чтобы посмотреть, как он работает. После успешного завершения вы должны получить аутентифицированный токен, как показано в следующем фрагменте.
1
2
|
$php db_auth.php
UsernamePasswordToken(user=»admin», authenticated=true, roles=»admin»)
|
Как только пользователь аутентифицирован, вы можете сохранить аутентифицированный токен в сеансе для последующих запросов.
И с этим мы завершили нашу простую демонстрацию аутентификации!
Вывод
Сегодня мы рассмотрели компонент Symfony Security, который позволяет интегрировать функции безопасности в ваши PHP-приложения. В частности, мы обсудили функцию аутентификации, предоставляемую подкомпонентом symfony / security-core, и я показал вам пример того, как эта функция может быть реализована в вашем собственном приложении.
Не стесняйтесь оставлять свои мысли, используя канал ниже!