2015-10-05 94 views
2

我有一个OAuth API,需要用户名和密码来获取用户对象(资源所有者密码凭证流)。我试图让这个最终结果是:使用Symfony2通过API进行用户密码验证

  1. 用户输入用户名/密码
  2. Symfony的交流访问的用户名/密码,并刷新令牌,然后获取用户对象和填充令牌所获取的对象
  3. 用户现在认证的网站

说我有这个问题上是,我似乎无法弄清楚如何做到这一点,我可以看到的最好的方式,即用一个用户提供。 UserProviderInterface要求实现loadUserByUsername(),但是我不能那样做,因为我需要用户名和密码来获取用户对象。

我试图实现SimplePreAuthenticatorInterface,但我仍然会碰到同样的问题:在createToken()创建预验证令牌之后,我需要使用authenticateToken()来验证它,我仍然无法通过UserProvider获取用户,因为我第一次必须使用用户名/密码才能获取访问令牌,以便获取用户对象。我想添加一个方法来登录我的UserProvider,它将使用用户名/密码通过API登录,并将登录的令牌存储在数组中的任何用户名中,然后通过该数组中的用户名获取令牌,我觉得没错。

我从错误的角度看它吗?我应该不使用PreAuthenticated令牌吗?

回答

4

前段时间我需要实现一种通过web服务验证用户的方式。这是我最终根据这个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,即时通讯存储是由允许我执行请求作为认证的用户web服务返回的令牌。

创建一个web服务认证听者负责守备请求,防火墙和调用身份验证提供:

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

的validaty
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; 
    } 

} 

最后创建一个用户提供者。在我收到webservice响应后,我检查用户是否存储在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安全要求,因此您可以将凭证存储在任何您想要的地方。 –

+0

你把工厂放在哪里,并将它添加到堆栈中?在我的情况下(sf3.2)DependencyInjection文件夹不在那里,所以我创建了它。但我不认为这个工厂是装载和使用的。 – rolandow

相关问题