在本文中,您将学习如何使用Symfony Security组件在PHP中设置用户身份验证。 除了身份验证之外,我还将向您展示如何使用其基于角色的授权,您可以根据需要对其进行扩展。
Symfony安全组件
Symfony安全组件使您可以更轻松地设置安全功能,例如身份验证,基于角色的授权,CSRF令牌。 实际上,它进一步分为四个子组件,您可以根据需要进行选择。
安全组件具有以下子组件:
- symfony /安全核心
- symfony /安全性-http
- symfony /安全性-CSRF
- symfony /安全性-acl
在本文中,我们将探讨symfony / security-core组件提供的身份验证功能。
与往常一样,我们将从安装和配置说明开始,然后我们将探索一些实际示例来演示关键概念。
安装与配置
在本节中,我们将安装Symfony Security组件。 我假设您已经在系统上安装了Composer,我们将需要它来安装Packagist上可用的Security组件。
因此,继续使用以下命令安装安全组件。
$composer require symfony/security
在我们的示例中,我们将从MySQL数据库中加载用户,因此我们还需要一个数据库抽象层。 让我们安装最流行的数据库抽象层之一:Doctrine DBAL。
$composer require doctrine/dbal
那应该已经创建了composer.json文件,它应该看起来像这样:
{
"require": {
"symfony/security": "^4.1",
"doctrine/dbal": "^2.7"
}
}
让我们将composer.json文件修改为如下所示。
{
"require": {
"symfony/security": "^4.1",
"doctrine/dbal": "^2.7"
},
"autoload": {
"psr-4": {
"Sfauth\\": "src"
},
"classmap": ["src"]
}
}
当我们添加了一个新的classmap
条目后,让我们继续运行以下命令来更新composer自动加载器。
$composer dump -o
现在,您可以使用Sfauth
命名空间在src目录下自动加载类。
这就是安装部分,但是您应该如何使用它呢? 实际上,只需要在应用程序中包含由Composer创建的autoload.php文件即可,如以下代码片段所示。
<?php
require_once './vendor/autoload.php';
// application code
?>
真实的例子
首先,让我们看一下Symfony Security组件提供的常规身份验证流程。
- 第一件事是检索用户凭据并创建未经身份验证的令牌。
- 接下来,我们将一个未经身份验证的令牌传递给身份验证管理器以进行验证。
- 身份验证管理器可能包含不同的身份验证提供程序,并且其中一个将用于对当前用户请求进行身份验证。 在身份验证提供程序中定义了如何对用户进行身份验证的逻辑。
- 身份验证提供程序与用户提供程序联系以检索用户。 用户提供者有责任从各自的后端加载用户。
- 用户提供者尝试使用身份验证提供者提供的凭据来加载用户。 在大多数情况下,用户提供者返回实现
UserInterface
接口的用户对象。 - 如果找到了用户,则身份验证提供程序将返回未经身份验证的令牌,您可以为后续请求存储此令牌。
在我们的示例中,我们将用户凭据与MySQL数据库进行匹配,因此我们需要创建数据库用户提供程序。 我们还将创建处理身份验证逻辑的数据库身份验证提供程序。 最后,我们将创建User类,该类实现UserInterface
接口。
用户类别
在本节中,我们将创建User类,该类代表身份验证过程中的用户实体。
继续并使用以下内容创建src / User / User.php文件。
<?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文件。
<?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
和supportsClass
方法。
loadUserByUsername
方法应按用户名加载用户,这在getUser
方法中完成。 如果找到用户,我们将返回相应的Sfauth\User\User
对象,该对象实现UserInterface
接口。
另一方面, refreshUser
方法通过从数据库中获取最新信息来刷新提供的User
对象。
最后, supportsClass
方法检查DatabaseUserProvider
提供程序是否支持所提供的用户类。
数据库身份验证提供程序类
最后,我们需要实现用户身份验证提供程序,该提供程序定义了身份验证逻辑-如何对用户进行身份验证。 在我们的例子中,我们需要将用户凭据与MySQL数据库进行匹配,因此我们需要相应地定义身份验证逻辑。
继续并使用以下内容创建src / User / DatabaseAuthenticationProvider.php文件。
<?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文件,并用以下内容填充它。
<?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";
}
回想一下本文开头讨论的身份验证流程-上面的代码反映了该顺序。
第一件事是检索用户凭据并创建未经身份验证的令牌。
$unauthenticatedToken = new UsernamePasswordToken(
$username,
$password,
'frontend'
);
接下来,我们将该令牌传递给身份验证管理器进行验证。
// authenticate user & get authenticated token
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);
调用authenticate方法时,幕后发生了很多事情。
首先,身份验证管理器选择适当的身份验证提供程序。 在我们的例子中,它是DatabaseAuthenticationProvider
身份验证提供程序,将选择该身份验证提供程序。
接下来,它通过DatabaseUserProvider
用户提供者的用户名检索用户。 最后, checkAuthentication
方法执行必要的检查以验证当前用户请求。
如果您希望测试db_auth.php脚本,则需要在MySQL数据库中创建sf_users
表。
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脚本以查看其运行情况。 成功完成后,您将收到一个经过身份验证的令牌,如以下代码片段所示。
$php db_auth.php
UsernamePasswordToken(user="admin", authenticated=true, roles="admin")
用户通过身份验证后,可以将身份验证的令牌存储在会话中以用于后续请求。
至此,我们已经完成了简单的身份验证演示!
结论
今天,我们研究了Symfony Security组件,该组件使您可以在PHP应用程序中集成安全功能。 具体来说,我们讨论了symfony / security-core子组件提供的身份验证功能,并向您展示了如何在自己的应用程序中实现此功能的示例。
请使用下面的提要随意发表您的想法!