前段時間我需要實現一種通過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
如果您有任何問題,請讓我知道。
感謝您的詳細解答。我確實有一個問題,因爲我覺得這仍然包含我的問題的一部分:在你的UserProvider中,你確實存儲了一個User對象到你的redis數據存儲中,但是在我看來,在我的UserProvider中有一個臨時存儲證書的數組,可以在'loadUserByUsername()'方法中獲取。這是唯一可以做到的方法嗎? – azenet
loadUserByUsername需要返回實現UserInterface的類。由於loadUserByUsername符合Symfony安全要求,因此您可以將憑證存儲在任何您想要的地方。 –
你把工廠放在哪裏,並將它添加到堆棧中?在我的情況下(sf3.2)DependencyInjection文件夾不在那裏,所以我創建了它。但我不認爲這個工廠是裝載和使用的。 – rolandow