2015-10-05 2 views
2

У меня есть API OAuth, для которого требуется имя пользователя и пароль для получения пользовательского объекта (поток учетных данных пользователя владельца ресурса). Я пытаюсь получить этот конечный результат:Аутентификация с помощью пароля пользователя через API с Symfony2

  1. Пользователь вводит имя пользователя/пароль
  2. Symfony биржи имя пользователя/пароль для доступа и обновления маркеров, а затем выбирает объект пользователя и заселяет маркер с извлеченной объекта
  3. Пользователь теперь аутентифицирован на сайте

вопрос, который я имею, что я не могу показаться, чтобы выяснить, как сделать это наилучшим образом я могу видеть: с провайдером пользователя. UserProviderInterface просит реализовать loadUserByUsername(), однако я не могу этого сделать, поскольку мне нужно имя пользователя и пароль для извлечения объекта пользователя.

Я пытался реализовать SimplePreAuthenticatorInterface, но я по-прежнему работать с той же проблемой: после создания маркеров PreAuthenticated в createToken(), мне нужно проверить подлинность его с помощью authenticateToken(), и я до сих пор не могу принести пользователя через UserProvider, так как я первый должны использовать имя пользователя/пароль, чтобы получить токен доступа, который позволит мне получить объект User. Я подумал о добавлении метода для входа в свой UserProvider, который должен был бы войти через API, используя имя пользователя/пароль и сохранить зарегистрированные логены для любого имени пользователя в массиве, а затем извлечь токены по имени пользователя в этом массиве, но это не означает, t чувствую себя хорошо.

Я смотрю на это с неправильного угла? Должен ли я вообще не использовать токены с предварительной аутентификацией?

ответ

4

Некоторое время назад мне нужно было реализовать способ аутентификации пользователей через веб-сервис. Это то, что я в конечном итоге делаю на основе этого doc и формы входа в систему ядра symfony.

Сначала создайте токен, который представляет данные аутентификации пользователя, присутствующую в запросе:

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, что им хранение является лексема возвращается из веба-сервиса, который позволяет мне выполнять запросы как авторизованный пользователь.

Создание слушателя аутентификации 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)); 
    } 

} 

Создать Webservice входа в завод, где мы Вук в компонент безопасности, и сказать, что пользователь Поставщик и доступные варианты:

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'; 
    } 

} 

Создание поставщика проверки подлинности, который будет проверять validaty в 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; 
    } 

} 

И, наконец, создать поставщика услуг. В моем случае после получения ответа от веб-службы я проверяю, сохранен ли пользователь в 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 

Если у вас есть какие-либо вопросы, пожалуйста, дайте мне знать.

+0

Спасибо за подробный ответ. У меня есть вопрос, поскольку я чувствую, что это все еще содержит часть моей проблемы: в UserProvider вы сохраняете объект User в своем хранилище данных redis, но мне кажется, что у меня есть массив в моем UserProvider, который временно хранит учетные данные, может быть вызван методом 'loadUserByUsername()'. Это единственный способ сделать это? – azenet

+1

loadUserByUsername необходимо вернуть класс, который реализует UserInterface. Вы можете хранить свои учетные данные в любом месте, которое требуется, поскольку loadUserByUsername соответствует требованиям Symfony Security. –

+0

Где вы положили завод и добавили его в стек? В моем случае (sf3.2) папка DependencyInjection не была там, поэтому я ее создал. Но я не думаю, что завод загружен и использован. – rolandow