[Security] add "anonymous: lazy" mode to firewalls
This commit is contained in:
parent
3c7172d81e
commit
5cd1d7b4cc
@ -8,6 +8,7 @@ CHANGELOG
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible
|
||||
* Added new encoder types: `auto` (recommended), `native` and `sodium`
|
||||
* The normalization of the cookie names configured in the `logout.delete_cookies`
|
||||
option is deprecated and will be disabled in Symfony 5.0. This affects to cookies
|
||||
|
@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
|
||||
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
|
||||
@ -127,7 +128,7 @@ class SecurityDataCollector extends DataCollector implements LateDataCollectorIn
|
||||
|
||||
$logoutUrl = null;
|
||||
try {
|
||||
if (null !== $this->logoutUrlGenerator) {
|
||||
if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) {
|
||||
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
|
@ -55,7 +55,12 @@ class AnonymousFactory implements SecurityFactoryInterface
|
||||
public function addConfiguration(NodeDefinition $builder)
|
||||
{
|
||||
$builder
|
||||
->beforeNormalization()
|
||||
->ifTrue(function ($v) { return 'lazy' === $v; })
|
||||
->then(function ($v) { return ['lazy' => true]; })
|
||||
->end()
|
||||
->children()
|
||||
->booleanNode('lazy')->defaultFalse()->end()
|
||||
->scalarNode('secret')->defaultNull()->end()
|
||||
->end()
|
||||
;
|
||||
|
@ -243,7 +243,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
|
||||
|
||||
$contextId = 'security.firewall.map.context.'.$name;
|
||||
$context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context'));
|
||||
$context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
|
||||
$context = $container->setDefinition($contextId, $context);
|
||||
$context
|
||||
->replaceArgument(0, new IteratorArgument($listeners))
|
||||
->replaceArgument(1, $exceptionListener)
|
||||
@ -409,7 +410,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
}
|
||||
|
||||
// Access listener
|
||||
if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) {
|
||||
$listeners[] = new Reference('security.access_listener');
|
||||
}
|
||||
|
||||
// Exception listener
|
||||
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));
|
||||
|
@ -151,6 +151,16 @@
|
||||
<argument /> <!-- FirewallConfig -->
|
||||
</service>
|
||||
|
||||
<service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true">
|
||||
<argument type="collection" />
|
||||
<argument type="service" id="security.exception_listener" />
|
||||
<argument /> <!-- LogoutListener -->
|
||||
<argument /> <!-- FirewallConfig -->
|
||||
<argument type="service" id="security.access_listener" />
|
||||
<argument type="service" id="security.untracked_token_storage" />
|
||||
<argument type="service" id="security.access_map" />
|
||||
</service>
|
||||
|
||||
<service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true">
|
||||
<argument /> <!-- name -->
|
||||
<argument /> <!-- user_checker -->
|
||||
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
use Symfony\Component\Security\Http\AccessMapInterface;
|
||||
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
|
||||
use Symfony\Component\Security\Http\Firewall\AccessListener;
|
||||
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
|
||||
use Symfony\Component\Security\Http\Firewall\LogoutListener;
|
||||
|
||||
/**
|
||||
* Lazily calls authentication listeners when actually required by the access listener.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class LazyFirewallContext extends FirewallContext
|
||||
{
|
||||
private $accessListener;
|
||||
private $tokenStorage;
|
||||
private $map;
|
||||
|
||||
public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map)
|
||||
{
|
||||
parent::__construct($listeners, $exceptionListener, $logoutListener, $config);
|
||||
|
||||
$this->accessListener = $accessListener;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->map = $map;
|
||||
}
|
||||
|
||||
public function getListeners(): iterable
|
||||
{
|
||||
return [$this];
|
||||
}
|
||||
|
||||
public function __invoke(RequestEvent $event)
|
||||
{
|
||||
$this->tokenStorage->setInitializer(function () use ($event) {
|
||||
$event = new LazyResponseEvent($event);
|
||||
foreach (parent::getListeners() as $listener) {
|
||||
if (\is_callable($listener)) {
|
||||
$listener($event);
|
||||
} else {
|
||||
@trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED);
|
||||
$listener->handle($event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
[$attributes] = $this->map->getPatterns($event->getRequest());
|
||||
|
||||
if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) {
|
||||
($this->accessListener)($event);
|
||||
}
|
||||
} catch (LazyResponseException $e) {
|
||||
$event->setResponse($e->getResponse());
|
||||
}
|
||||
}
|
||||
}
|
@ -59,6 +59,6 @@ class LocalizedController implements ContainerAwareInterface
|
||||
|
||||
public function homepageAction()
|
||||
{
|
||||
return new Response('<html><body>Homepage</body></html>');
|
||||
return (new Response('<html><body>Homepage</body></html>'))->setPublic();
|
||||
}
|
||||
}
|
||||
|
@ -129,6 +129,16 @@ class SecurityRoutingIntegrationTest extends AbstractWebTestCase
|
||||
$client->request('GET', '/unprotected_resource');
|
||||
}
|
||||
|
||||
public function testPublicHomepage()
|
||||
{
|
||||
$client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']);
|
||||
$client->request('GET', '/en/');
|
||||
|
||||
$this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse());
|
||||
$this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public'));
|
||||
$this->assertSame(0, self::$container->get('session')->getUsageIndex());
|
||||
}
|
||||
|
||||
private function assertAllowed($client, $path)
|
||||
{
|
||||
$client->request('GET', $path);
|
||||
|
@ -27,7 +27,7 @@ security:
|
||||
check_path: /login_check
|
||||
default_target_path: /profile
|
||||
logout: ~
|
||||
anonymous: ~
|
||||
anonymous: lazy
|
||||
|
||||
# This firewall is here just to check its the logout functionality
|
||||
second_area:
|
||||
@ -38,6 +38,7 @@ security:
|
||||
path: /second/logout
|
||||
|
||||
access_control:
|
||||
- { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
|
@ -24,7 +24,7 @@
|
||||
"symfony/security-core": "^4.4",
|
||||
"symfony/security-csrf": "^4.2|^5.0",
|
||||
"symfony/security-guard": "^4.2|^5.0",
|
||||
"symfony/security-http": "^4.3"
|
||||
"symfony/security-http": "^4.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/asset": "^3.4|^4.0|^5.0",
|
||||
|
@ -25,12 +25,18 @@ use Symfony\Contracts\Service\ResetInterface;
|
||||
class TokenStorage implements TokenStorageInterface, ResetInterface
|
||||
{
|
||||
private $token;
|
||||
private $initializer;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
if ($initializer = $this->initializer) {
|
||||
$this->initializer = null;
|
||||
$initializer();
|
||||
}
|
||||
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
@ -43,9 +49,15 @@ class TokenStorage implements TokenStorageInterface, ResetInterface
|
||||
@trigger_error(sprintf('Not implementing the "%s::getRoleNames()" method in "%s" is deprecated since Symfony 4.3.', TokenInterface::class, \get_class($token)), E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
$this->initializer = null;
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function setInitializer(?callable $initializer): void
|
||||
{
|
||||
$this->initializer = $initializer;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->setToken(null);
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Core\Exception;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* A signaling exception that wraps a lazily computed response.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class LazyResponseException extends \Exception implements ExceptionInterface
|
||||
{
|
||||
private $response;
|
||||
|
||||
public function __construct(Response $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
|
||||
/**
|
||||
* Wraps a lazily computed response in a signaling exception.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class LazyResponseEvent extends RequestEvent
|
||||
{
|
||||
private $event;
|
||||
|
||||
public function __construct(parent $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setResponse(Response $response)
|
||||
{
|
||||
$this->stopPropagation();
|
||||
$this->event->stopPropagation();
|
||||
|
||||
throw new LazyResponseException($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getKernel(): HttpKernelInterface
|
||||
{
|
||||
return $this->event->getKernel();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->event->getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRequestType(): int
|
||||
{
|
||||
return $this->event->getRequestType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isMasterRequest(): bool
|
||||
{
|
||||
return $this->event->isMasterRequest();
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Exception\AccountStatusException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
use Symfony\Component\Security\Core\Exception\LogoutException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
|
||||
@ -103,6 +104,12 @@ class ExceptionListener
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exception instanceof LazyResponseException) {
|
||||
$event->setResponse($exception->getResponse());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exception instanceof LogoutException) {
|
||||
$this->handleLogoutException($exception);
|
||||
|
||||
|
Reference in New Issue
Block a user