通过使用 Symfony2 的 API 使用用户密码进行身份验证
Posted
技术标签:
【中文标题】通过使用 Symfony2 的 API 使用用户密码进行身份验证【英文标题】:Authentication with user password through an API with Symfony2 【发布时间】:2016-01-02 11:32:42 【问题描述】:我有一个 OAuth API,它需要用户名和密码才能获取用户对象(资源所有者密码凭据流)。我正在努力得到这个最终结果:
-
用户输入用户名/密码
Symfony 交换用户名/密码以获取访问和刷新令牌,然后获取用户对象并使用获取的对象填充令牌
用户现已在网站上通过身份验证
我遇到的问题是,我似乎无法弄清楚如何以我能看到的最佳方式做到这一点:使用用户提供程序。 UserProviderInterface 要求实现 loadUserByUsername(),但我不能这样做,因为我需要用户名和密码来获取用户对象。
我尝试实现 SimplePreAuthenticatorInterface,但我仍然遇到同样的问题:在 createToken()
中创建 PreAuthenticated 令牌后,我需要使用 authenticateToken()
对其进行身份验证,我仍然无法通过 UserProvider 获取用户,因为我首先必须使用用户名/密码来获取允许我获取用户对象的访问令牌。我考虑过在我的 UserProvider 中添加一个登录方法,该方法使用用户名/密码通过 API 登录,并将任何用户名的登录令牌存储在数组中,然后通过该数组中的用户名获取令牌,但这并不感觉不对。
我是不是从错误的角度看它?我不应该使用 PreAuthenticated 令牌吗?
【问题讨论】:
【参考方案1】:不久前,我需要实现一种通过 Web 服务对用户进行身份验证的方法。这就是我最终基于doc 和 symfony 核心的表单登录实现所做的事情。
首先创建一个表示请求中存在的用户身份验证数据的 Token:
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WebserviceAuthToken extends AbstractToken
/**
* The password of the user.
*
* @var string
*/
private $password;
/**
* Authenticated Session ID.
*
* @var string
*/
private $authSessionID;
public function __construct($user, $password, array $roles = array())
parent::__construct($roles);
$this->setUser($user);
$this->password = $password;
parent::setAuthenticated(count($roles) > 0);
/**
* @inheritDoc
*/
public function getCredentials()
return '';
/**
* Returns the Authenticated Session ID.
*
* @return string
*/
public function getAuthSessionID()
return $this->authSessionID;
/**
* Sets the Authenticated Session ID.
*
* @param string $authSessionID
*/
public function setAuthSessionID($authSessionID)
$this->authSessionID = $authSessionID;
/**
* Returns the Password used to attempt login.
*
* @return string
*/
public function getPassword()
return $this->password;
/**
* @inheritDoc
*/
public function serialize()
return serialize(array(
$this->authSessionID,
parent::serialize()
));
/**
* @inheritDoc
*/
public function unserialize($serialized)
$data = unserialize($serialized);
list(
$this->authSessionID,
$parent,
) = $data;
parent::unserialize($parent);
我存储的 AuthSessionID 是从 web 服务返回的令牌,它允许我作为经过身份验证的用户执行请求。
创建一个 Webservice 身份验证侦听器,负责向防火墙发送请求并调用身份验证提供程序:
use RPanelBundle\Security\Authentication\Token\RPanelAuthToken;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class WebserviceAuthListener extends AbstractAuthenticationListener
private $csrfTokenManager;
/**
* @inheritdoc
*/
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, $csrfTokenManager = null)
if ($csrfTokenManager instanceof CsrfProviderInterface)
$csrfTokenManager = new CsrfProviderAdapter($csrfTokenManager);
elseif (null !== $csrfTokenManager && !$csrfTokenManager instanceof CsrfTokenManagerInterface)
throw new InvalidArgumentException('The CSRF token manager should be an instance of CsrfProviderInterface or CsrfTokenManagerInterface.');
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'intention' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);
$this->csrfTokenManager = $csrfTokenManager;
/**
* @inheritdoc
*/
protected function requiresAuthentication(Request $request)
if ($this->options['post_only'] && !$request->isMethod('POST'))
return false;
return parent::requiresAuthentication($request);
/**
* @inheritdoc
*/
protected function attemptAuthentication(Request $request)
if (null !== $this->csrfTokenManager)
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken)))
throw new InvalidCsrfTokenException('Invalid CSRF token.');
if ($this->options['post_only'])
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
else
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$request->getSession()->set(Security::LAST_USERNAME, $username);
return $this->authenticationManager->authenticate(new WebserviceAuthToken($username, $password));
创建一个 Web 服务登录工厂,我们在其中加入安全组件,并判断哪个是用户提供程序和可用选项:
class WebserviceFormLoginFactory extends FormLoginFactory
/**
* @inheritDoc
*/
public function getKey()
return 'webservice-form-login';
/**
* @inheritDoc
*/
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
$provider = 'app.security.authentication.provider.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('app.security.authentication.provider'))
->replaceArgument(1, new Reference($userProviderId))
->replaceArgument(2, $id);
return $provider;
/**
* @inheritDoc
*/
protected function getListenerId()
return 'app.security.authentication.listener';
创建将验证 WebserviceAuthToken 有效性的身份验证提供程序
class WebserviceAuthProvider implements AuthenticationProviderInterface
/**
* Service to handle DMApi account related calls.
*
* @var AccountRequest
*/
private $apiAccountRequest;
/**
* User provider service.
*
* @var UserProviderInterface
*/
private $userProvider;
/**
* Security provider key.
*
* @var string
*/
private $providerKey;
public function __construct(AccountRequest $apiAccountRequest, UserProviderInterface $userProvider, $providerKey)
$this->apiAccountRequest = $apiAccountRequest;
$this->userProvider = $userProvider;
$this->providerKey = $providerKey;
/**
* @inheritdoc
*/
public function authenticate(TokenInterface $token)
// Check if both username and password exist
if (!$username = $token->getUsername())
throw new AuthenticationException('Username is required to authenticate.');
if (!$password = $token->getPassword())
throw new AuthenticationException('Password is required to authenticate.');
// Authenticate the User against the webservice
$loginResult = $this->apiAccountRequest->login($username, $password);
if (!$loginResult)
throw new BadCredentialsException();
try
$user = $this->userProvider->loadUserByWebserviceResponse($loginResult);
// We dont need to store the user password
$authenticatedToken = new WebserviceAuthToken($user->getUsername(), "", $user->getRoles());
$authenticatedToken->setUser($user);
$authenticatedToken->setAuthSessionID($loginResult->getAuthSid());
$authenticatedToken->setAuthenticated(true);
return $authenticatedToken;
catch (\Exception $e)
throw $e;
/**
* @inheritdoc
*/
public function supports(TokenInterface $token)
return $token instanceof WebserviceAuthToken;
最后创建一个用户提供者。就我而言,在我收到来自 web 服务的响应后,我检查用户是否存储在 redis 上,如果没有,我创建它。之后,用户总是从 redis 加载。
class WebserviceUserProvider implements UserProviderInterface
/**
* Wrapper to Access the Redis.
*
* @var RedisDao
*/
private $redisDao;
public function __construct(RedisDao $redisDao)
$this->redisDao = $redisDao;
/**
* @inheritdoc
*/
public function loadUserByUsername($username)
// Get the UserId based on the username
$userId = $this->redisDao->getUserIdByUsername($username);
if (!$userId)
throw new UsernameNotFoundException("Unable to find an UserId identified by Username = $username");
if (!$user = $this->redisDao->getUser($userId))
throw new UsernameNotFoundException("Unable to find an User identified by ID = $userId");
if (!$user instanceof User)
throw new UnsupportedUserException();
return $user;
/**
* Loads an User based on the webservice response.
*
* @param \AppBundle\Service\Api\Account\LoginResult $loginResult
* @return User
*/
public function loadUserByWebserviceResponse(LoginResult $loginResult)
$userId = $loginResult->getUserId();
$username = $loginResult->getUsername();
// Checks if this user already exists, otherwise we need to create it
if (!$user = $this->redisDao->getUser($userId))
$user = new User($userId, $username);
if (!$this->redisDao->setUser($user) || !$this->redisDao->mapUsernameToId($username, $userId))
throw new \Exception("Couldnt create a new User for username = $username");
if (!$user instanceof User)
throw new UsernameNotFoundException();
if (!$this->redisDao->setUser($user))
throw new \Exception("Couldnt Update Data for for username = $username");
return $this->loadUserByUsername($username);
/**
* @inheritdoc
*/
public function refreshUser(UserInterface $user)
if (!$user instanceof User)
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
return $this->loadUserByUsername($user->getUsername());
/**
* @inheritdoc
*/
public function supportsClass($class)
return $class === 'AppBundle\Entities\User';
所需服务:
app.security.user.provider:
class: AppBundle\Security\User\WebserviceUserProvider
arguments: ["@app.dao.redis"]
app.security.authentication.provider:
class: AppBundle\Security\Authentication\Provider\WebserviceAuthProvider
arguments: ["@api_caller", "", ""]
app.security.authentication.listener:
class: AppBundle\Security\Firewall\WebserviceAuthListener
abstract: true
parent: security.authentication.listener.abstract
配置的安全性:
security:
providers:
app_user_provider:
id: app.security.user.provider
firewalls:
default:
pattern: ^/
anonymous: ~
provider: app_user_provider
webservice_form_login: # Configure just like form_login from the Symfony core
如果您有任何问题,请告诉我。
【讨论】:
感谢您的详细回答。我确实有一个问题,因为我觉得这仍然包含我的问题的一部分:在您的 UserProvider 中,您确实在您的 redis 数据存储中存储了一个 User 对象,但在我看来,这就像在我的 UserProvider 中有一个临时存储凭据的数组,以便它可以在loadUserByUsername()
方法中获取。这是唯一的方法吗?
loadUserByUsername 需要返回一个实现 UserInterface 的类。由于 loadUserByUsername 遵循 Symfony 安全要求,因此您可以将凭据存储在任何您想要的位置。
你把工厂放在哪里并添加到堆栈中?在我的情况下(sf3.2),DependencyInjection 文件夹不存在,所以我创建了它。但我不认为工厂是加载和使用的。以上是关于通过使用 Symfony2 的 API 使用用户密码进行身份验证的主要内容,如果未能解决你的问题,请参考以下文章
guacamole免密登录,guacamole不通过登录界面登录,guacamole指定用户名免密登录