Redirecionar usuário autenticado em páginas anônimas no Symfony

O que deve acontecer quando um usuário autenticado vai para a página de login, página de registro, senha esquecida, etc.? Uma opção é não fazer nada – apenas exibir a página e deixar que o usuário decida o que deseja fazer. Redirecionar o usuário pode ser uma solução melhor.

Acesso anônimo no Symfony

As funções são hierárquicas, portanto, seguindo a definição, app/config/security.ymlfaz com que todos possam acessar a página de login. O usuário autenticado pode acessar a página de login mesmo quando já estiver logado.

security:
access_control
:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

Isso pode confundir os usuários, especialmente se um aplicativo não oferecer suporte para alternar entre várias contas. Mostrar a página de erro ou redirecionar o usuário para outra página é a melhor abordagem. Vamos cobrir esse comportamento com testes e experiências com diferentes implementações.

Scenario Outline: Authenticated user is redirected on anonymous page
Given I am logged as admin
When I am on "<url>"
Then I should not see "<text>"
# And the url should match "/admin/" # assert if user is redirected

Examples:
| url | text |
| /login | Login |
| /resetting/request | Forgotten password |
| /registration | Create account |

1. Negar acesso no controle de acesso

Atualize a expressão de segurança sem escrever uma única linha de código. Digamos que você tenha uma função padrão ROLE_USERcomo em FOSUserBundle :

security:
access_control
:
- { path: ^/login$, allow_if: "not has_role('ROLE_USER')" }
- { path: ^/resetting, allow_if: "not has_role('ROLE_USER')" }

Quando o usuário autenticado (ou lembrado) vai para a página de login, ele termina na página 403 Proibida ( AccessDeniedException ). Os testes foram aprovados, mas se você deseja redirecionar o usuário para outra página, é necessário ouvir e fazer alguma mágica para determinar quando o usuário será redirecionado.kernel.exception

Se a página 403 Proibida for boa o suficiente para você, esta é a solução mais simples. Os redirecionamentos precisam ser tratados em outro lugar. O código cheira, certo? O uso de exceções para o fluxo de controle e o código é distribuído em vários lugares (condição e redirecionamento no ouvinte).security.yml

2. Verifique o usuário no (s) controlador (es)

Primeira ideia que vem à mente. Bem, é uma abordagem meio ingênua, mas funciona. Copie e cole três controladores e os testes voilá acabaram de ser aprovados. São apenas três páginas, então tudo bem, não é?

class SomeController extends BaseController
{
public function someAction()
{
if ($this->isUserLogged()) {
return $this->redirectToRoute('somewhere');
}
// do default action
}
}

A duplicação em vários controladores pode se tornar um grande problema. Imagine o código se cada ação precisar fazer essa verificação. Por exemplo, se você deseja forçar os usuários a alterar a senha todos os meses? Além disso, se você estiver usando FOSUserBundle (ou qualquer outro pacote de usuário externo), você deve substituir os controladores do terceiro pacote . É muito código clichê , então prefiro evitar essa solução. Não repita meus erros e leia StackOverflow com mais atenção 🙂

3. Ouça no eventokernel.request

Vamos recapitular as desvantagens das soluções anteriores:

  • A regra ACL não pode definir a verificação de segurança e a página redirecionada em um só lugar
  • Modificar controladores causa duplicação e requer a substituição de pacotes externos

Os eventos do Symfony resolvem tudo. Inscreva-se no evento, se as condições forem atendidas, redirecione o usuário:kernel.request

services:
app
.tokens.action_listener:
class: AppBundleEventListenerRedirectUserListener
arguments
:
- "@security.token_storage"
- "@router"
tags
:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
<?php

namespace AppBundleEventListener;

use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface;
use SymfonyComponentRoutingRouterInterface;
use SymfonyComponentHttpFoundationRedirectResponse;
use FOSUserBundleModelUser;

class RedirectUserListener
{
private $tokenStorage;
private $router;

public function __construct(TokenStorageInterface $t, RouterInterface $r)
{
$this
->tokenStorage = $t;
$this
->router = $r;
}

public function onKernelRequest(GetResponseEvent $event)
{
if ($this->isUserLogged() && $event->isMasterRequest()) {
$currentRoute
= $event->getRequest()->attributes->get('_route');
if ($this->isAuthenticatedUserOnAnonymousPage($currentRoute) {
$response
= new RedirectResponse($this->router->generate('homepage'));
$event
->setResponse($response);
}
}
}

private function isUserLogged()
{
$user
= $this->tokenStorage->getToken()->getUser();
return $user instanceof User;
}

private function isAuthenticatedUserOnAnonymousPage($currentRoute)
{
return in_array(
$currentRoute
,
['fos_user_security_login', 'fos_user_resetting_request', 'app_user_registration']
);
}
}

Este princípio pode ser usado para ações semelhantes – quando o usuário deve alterar a senha gerada automaticamente, alterar a senha uma vez por ano etc. O evento capturará esse usuário em cada url (ignore o profiler no modo dev, caso contrário, todas as solicitações do profiler serão redirecionadas para alterar a senha página). O ouvinte pode até mesmo lidar com várias condições e redirecionamentos:

public function onKernelRequest(GetResponseEvent $event)
{
$loggedUser
= $this->getLoggedUser();
if ($loggedUser && $event->isMasterRequest()) {
$currentRoute
= $event->getRequest()->attributes->get('_route');
$redirectRoute
= $this->getRedirectRoute($loggedUser, $currentRoute);
if ($redirectRoute) {
$response
= new RedirectResponse($this->router->generate($redirectRoute));
$event
->setResponse($response);
}
}
}

private function getLoggedUser()
{
$user
= $this->securityContext->getToken()->getUser();
return $user instanceof User ? $user : null;
}

private function getRedirectRoute($user, $currentRoute)
{
if ($this->isChangingPassword($user, $currentRoute)) {
return 'fos_user_change_password';
} elseif ($this->isAuthenticatedUserOnAnonymousPage($currentRoute)) {
return 'homepage';
}
}

private function isChangingPassword($user, $currentRoute)
{
return $user->hasRole('ROLE_CHANGE_PASSWORD')
&& strpos($currentRoute, '_profiler') === false
&& !in_array($currentRoute, ['_wdt', 'fos_user_change_password']);
}