3

我有一个 OAuth API,它需要用户名和密码才能获取用户对象(资源所有者密码凭证流)。我试图得到这个最终结果:

  1. 用户输入用户名/密码
  2. Symfony 交换用户名/密码以获取访问和刷新令牌,然后获取用户对象并使用获取的对象填充令牌
  3. 用户现在在网站上通过身份验证

我遇到的问题是,我似乎无法弄清楚如何以我能看到的最佳方式做到这一点:使用用户提供程序。UserProviderInterface 要求实现 loadUserByUsername(),但是我不能这样做,因为我需要用户名和密码来获取用户对象。

我试图实现 SimplePreAuthenticatorInterface,但我仍然遇到同样的问题:在创建 PreAuthenticated 令牌后createToken(),我需要使用它进行身份验证authenticateToken(),我仍然无法通过 UserProvider 获取用户,因为我首先必须使用用户名/密码来获取允许我获取用户对象的访问令牌。我考虑过在我的 UserProvider 中添加一种登录方法,该方法使用用户名/密码通过 API 登录,并将任何用户名的登录令牌存储在数组中,然后通过该数组中的用户名获取令牌,但这并不感觉不对。

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

4

1 回答 1

8

不久前,我需要实现一种通过 Web 服务对用户进行身份验证的方法。这就是我最终基于此文档和 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

如果您有任何问题,请告诉我。

于 2015-10-05T21:42:22.800 回答