Symfony 3 FosUserBundle 非唯一用户名登录
Symfony 3 FosUserBundle non unique username login
我们希望在应用程序中实现的目标是:
- 非唯一用户名[完成]
- 唯一的用户名和电子邮件组合
- FosUserBundle 将获取所有具有给定用户名的用户(在用户登录时),并检查是否有任何用户具有给定的密码(使用 bcrypt 散列)。当找到用户时,它会让用户登录。
通过覆盖用户 ORM 中的用户名字段,使用户名不唯一非常简单。但我们有点受困于如何继续实现最后两点。我们已经开始创建自定义用户提供程序,但 Symfony 安全似乎只能处理一个用户(名称)。
是否有任何有经验的人可以帮助我们?如果您需要更多信息或代码片段,请询问。提前致谢!
因此,在查看了 Symfony 安全模块的大量文档后,我们找到了答案。
我们向用户模型添加了一个额外的字段(显示名称),因为 Symfony 完全围绕用户名是唯一的这一事实构建的。它总是获取具有给定用户名的第一个用户,这不是我们想要的。
所以我们开始编写我们自己的 Guard 身份验证系统,虽然我们必须进行一些调整,但这是非常简单的。
这一切都运行良好,但我们 运行 遇到了内置 UsernamePasswordFormAuthenticationListener 的问题,该侦听器仍在从登录表单中获取显示名称。我们实际上想要唯一的用户名,以便 Symfony 知道要使用哪个用户。
我们创建了一个扩展标准监听器的自定义监听器,并确保用户名不是从登录表单中获取的,而是从用户令牌中获取的。
所以我们现在的流程是这样的:用户填写他的用户名(实际上是他的显示名)和密码,系统获取所有具有该显示名的用户。然后我们循环这些用户并检查是否有人拥有该密码。如果是这样,请对用户进行身份验证。
在用户创建时,管理员填写显示名称,系统将自动将其作为用户名递增。 (admin_1, admin_2, ...).
我们必须监控@kero 所说的是否属实,但使用 Bcrypt 似乎即使使用像“123”这样的简单密码,它也会为每个用户生成不同的哈希值。
唯一剩下的就是对显示名称和电子邮件的唯一 组合 设置 UniqueConstraint。如果有人知道如何在我们的 orm.xml 和表格中实现这一点,谢谢。
http://symfony.com/doc/current/security/guard_authentication.html
自定义防护验证器
class Authenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $userRepository;
private $tokenStorage;
private $router;
public function __construct(EncoderFactoryInterface $encoderFactory, UserRepositoryInterface $userRepository, TokenStorageInterface $tokenStorage, Router $router)
{
$this->encoderFactory = $encoderFactory;
$this->userRepository = $userRepository;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$encoder = $this->encoderFactory->getEncoder(new User());
$displayname = $request->request->get('_username');
$password = $request->request->get('_password');
$users = $this->userRepository->findByDisplayname($displayname);
if ($users !== []) {
foreach ($users as $user) {
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
} else {
if ($this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
return null;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials !== null) {
return $userProvider->loadUserByUsername($credentials["username"]);
}
return null;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user !== null) {
return true;
} else {
return false;
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$exclusions = ['/login'];
if (!in_array($request->getPathInfo(), $exclusions)) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw $exception;
}
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
自定义侦听器
class CustomAuthListener extends UsernamePasswordFormAuthenticationListener
{
private $csrfTokenManager;
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null)
{
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
$this->tokenStorage = $tokenStorage;
}
/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($user = $this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
$username = $user->getUsername();
if ($this->options['post_only']) {
$password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
} else {
$password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
}
if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
} else {
return null;
}
}
}
监听器服务
<service id="security.authentication.listener.form" class="Your\Path\To\CustomAuthListener" parent="security.authentication.listener.abstract" abstract="true" />
我们希望在应用程序中实现的目标是:
- 非唯一用户名[完成]
- 唯一的用户名和电子邮件组合
- FosUserBundle 将获取所有具有给定用户名的用户(在用户登录时),并检查是否有任何用户具有给定的密码(使用 bcrypt 散列)。当找到用户时,它会让用户登录。
通过覆盖用户 ORM 中的用户名字段,使用户名不唯一非常简单。但我们有点受困于如何继续实现最后两点。我们已经开始创建自定义用户提供程序,但 Symfony 安全似乎只能处理一个用户(名称)。
是否有任何有经验的人可以帮助我们?如果您需要更多信息或代码片段,请询问。提前致谢!
因此,在查看了 Symfony 安全模块的大量文档后,我们找到了答案。
我们向用户模型添加了一个额外的字段(显示名称),因为 Symfony 完全围绕用户名是唯一的这一事实构建的。它总是获取具有给定用户名的第一个用户,这不是我们想要的。
所以我们开始编写我们自己的 Guard 身份验证系统,虽然我们必须进行一些调整,但这是非常简单的。 这一切都运行良好,但我们 运行 遇到了内置 UsernamePasswordFormAuthenticationListener 的问题,该侦听器仍在从登录表单中获取显示名称。我们实际上想要唯一的用户名,以便 Symfony 知道要使用哪个用户。
我们创建了一个扩展标准监听器的自定义监听器,并确保用户名不是从登录表单中获取的,而是从用户令牌中获取的。
所以我们现在的流程是这样的:用户填写他的用户名(实际上是他的显示名)和密码,系统获取所有具有该显示名的用户。然后我们循环这些用户并检查是否有人拥有该密码。如果是这样,请对用户进行身份验证。 在用户创建时,管理员填写显示名称,系统将自动将其作为用户名递增。 (admin_1, admin_2, ...).
我们必须监控@kero 所说的是否属实,但使用 Bcrypt 似乎即使使用像“123”这样的简单密码,它也会为每个用户生成不同的哈希值。
唯一剩下的就是对显示名称和电子邮件的唯一 组合 设置 UniqueConstraint。如果有人知道如何在我们的 orm.xml 和表格中实现这一点,谢谢。
http://symfony.com/doc/current/security/guard_authentication.html
自定义防护验证器
class Authenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $userRepository;
private $tokenStorage;
private $router;
public function __construct(EncoderFactoryInterface $encoderFactory, UserRepositoryInterface $userRepository, TokenStorageInterface $tokenStorage, Router $router)
{
$this->encoderFactory = $encoderFactory;
$this->userRepository = $userRepository;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$encoder = $this->encoderFactory->getEncoder(new User());
$displayname = $request->request->get('_username');
$password = $request->request->get('_password');
$users = $this->userRepository->findByDisplayname($displayname);
if ($users !== []) {
foreach ($users as $user) {
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
} else {
if ($this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
return null;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials !== null) {
return $userProvider->loadUserByUsername($credentials["username"]);
}
return null;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user !== null) {
return true;
} else {
return false;
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$exclusions = ['/login'];
if (!in_array($request->getPathInfo(), $exclusions)) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw $exception;
}
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
自定义侦听器
class CustomAuthListener extends UsernamePasswordFormAuthenticationListener
{
private $csrfTokenManager;
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null)
{
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
$this->tokenStorage = $tokenStorage;
}
/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($user = $this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
$username = $user->getUsername();
if ($this->options['post_only']) {
$password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
} else {
$password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
}
if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
} else {
return null;
}
}
}
监听器服务
<service id="security.authentication.listener.form" class="Your\Path\To\CustomAuthListener" parent="security.authentication.listener.abstract" abstract="true" />