Autenticação de usuário com ZF2 (Zend Framework 2)

Já faz um bom tempo que não escrevo sobre programação, e como havia comentado no último post, minha ausência se justificava por 1) desânimo, 2) falta de tempo, 3) falta de vontade. Pois bem, conversando com um novo amigo (este lá de Pernambuco), e trocando algumas ideias sobre a maravilha que é o ZF2, despertou em mim novamente a vontade de escrever sobre o assunto.

Para retomar então, lá vai um post sobre como autenticar um usuário no ZF2. O post em si não explica o processo de autenticação, mas sim como proteger áreas do seu sistema com login e senha, e só liberar acesso àqueles que estiverem autenticados. Por questões de SEO, o título do post ficou assim mesmo. E mais, em geral todo mundo sabe como verificar se um usuário está ou não autenticado, acredito eu que a dificuldade fica em como garantir que usuários não autenticados não acessem determinadas áreas, e este é o motivo e inspiração do tutorial.

Antes de começar, agradeço ao amigo pernambucano Roberto Vieira pelo bom papo que tivemos sobre este tutorial.

elefante-zf2

Vamos lá então!


Primeiramente, se você quer saber como proteger uma área com login e senha com o Zend, presume-se que você já tenha um bom conhecimento de configuração e utilização do framework. Levando isto em consideração, aviso que vou sempre direto ao ponto. Se por acaso se perder em algum momento, pesquise sobre sua dificuldade aqui no blog mesmo (tenho bastante coisa bacana de ZF2) ou então deixa um comentário lá embaixo.

A situação

Digamos então que você tenha uma aplicação rodando perfeitamente. A aplicação é web, e seu chefe chega para você e diz: “Carinha da informática, nossa aplicação está desprotegida. Você precisa criar uma página de login para somente alguns de nossos capacitados funcionários entrar

Pronto, você desaba. Um sistema inteiro rodando sobre ZF2, que em momento algum foi projetado pensando em acesso com login e senha, e agora cabe a você implementar isto. Mas como? Deve ser muito difícil, né?

–ESQUIZOFRENIC MODE = ON–

NÃO! Não é nem um pouco difícil!
Mas como não? Toda a aplicação já está feita!
Isto não é problema
Vou ter que refazer muita coisa
Nada!
Como assim?
Eu explico:

–ESQUIZOFRENIC MODE = OFF–

O ZF2, assim como diversos outros frameworks, é construído sobre o pattern “Front Controller”, ou seja, tudo que ocorre na aplicação é obrigado a fazer um caminho linear, entrando sempre pelo mesmo lugar para depois ser redirecionado para o ponto que desejamos estar. É mais ou menos como ter infinitas portas à sua frente, mas entre você e todas elas tem apenas uma porta, e você precisa passar por ela para chegar à todas as outras.

É neste ponto que vamos atacar. Vamos trabalhar somente sobre esta única porta, sem alterar em nada as outras.

Primeiramente, crie um controller chamado Login (dê o nome que você quiser, eu chamei-o de Login por ser óbvio) e uma action index. Veja os códigos abaixo:

<?php 
    namespace Application\Controller; 

    use Zend\Mvc\Controller\AbstractActionController; // isso é essencial 
    use Zend\Session\Container; // isso você vai usar em seguida...
    use Zend\View\Model\ViewModel; // isso é essencial 

    class LoginController extends AbstractActionController 
    {     
        public function indexAction()
        {
            $view =  new ViewModel();
            return $view->setTerminal(true);
        }
    }

Perai, o que é “setTerminal(true)” ?

Por ser uma página para login, qualquer coisa relativa ao template da aplicação (onde possivelmente estão o cabeçalho, menus e rodapé) não podem aparecer. Definindo este setTerminal(true), eu estou DESABILITANDO o layout, carregando na view apenas uma página totalmente em branco, esperando para ser lapidada.

Vamos montar um formulário BEM SIMPLES E SEM FRESCURA DE CSS para o login…

<!-- esse código fica no arquivo index.phtml relativo ao controller Login -->
<form method="post">
    <input type="text" name="usuario" placeholder="Usuário" />

    <input type="password" name="senha" placeholder="Senha" />

    <input type="submit" value="Entrar" />
</form>

Pronto, vamos configurar a rota para poder chegar nessa página agora…

'login' => array(
    'type' => 'Zend\Mvc\Router\Http\Literal',
        'options' => array(
            'route'    => '/login',
            'defaults' => array(
            'controller' => 'Application\Controller\Login',
            'action'     => 'index',
         ),
     ),
     'may_terminate' => true,
     'child_routes' => array(
         'default' => array(
             'type'    => 'Segment',
             'options' => array(
                  'route'    => '[/:action]',
                  'constraints' => array(
                       'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
                       'action'     => '[a-zA-Z][a-zA-Z0-9_-]*',
                   ),
                   'defaults' => array(),
             ),
         ),
     ),
 ),

Não esqueça de declarar o controller também…

'Application\Controller\Login' => 'Application\Controller\LoginController'

Agora, para completar, vamos criar mais duas actions: uma para informar o acesso negado (para usuários sem permissão ou incompetentes que esqueceram o seu próprio login – e estes são mais comuns que imaginamos…), e outra action para deslogar (sair) da aplicação.

Agora temos as seguintes rotas configuradas:
minhaaplicacao.com.br/login
minhaaplicacao.com.br/login/acesso-negado
minhaaplicacao.com.br/login/sair

Vamos dar uma incrementada na action index, uma vez que vamos ter o envio de formulário através dela. Perceba o código abaixo

public function indexAction()
{
    $request = $this->getRequest();
        
        if ($request->isPost()) {
            $dadosForm = $request->getPost()->toArray();
            
            /**
             * Aqui você aplica todo o seu processo de validação do usuário.
             * Ex.: consulta ao banco de dados procurando pelo usuário e senha.
             * 
             * O exemplo de validação abaixo é puramente didático! 
             * Não o reproduza em hipótese alguma!
             */
            
            if ($dadosForm['usuario'] == 'admin' && $dadosForm['senha'] == '123') {
                $sessao = new Container('Auth');
                $sessao->admin = true;
                return $this->redirect()->toRoute('home');
            } else {
               return $this->redirect()->toRoute('login/default', array('action' => 'acesso-negado'));
            }
        }

    $view =  new ViewModel();
    return $view->setTerminal(true);
}

Há mil e uma maneiras de identificar uma requisição POST no PHP, mas aí no código acima estamos usando o método tradicional do Zend:

1 – Pega o tipo de requisição com $this->getRequest()
2 – verifica se a requisição é POST com isPost()
3 – Caso positivo, armazena o conteúdo do POST em formato de array com $getPost()->toArray();

O que vem a seguir é vergonhoso. Estou verificando se o login bate através de variáveis estáticas no meio do código. NUNCA REPRODUZAM ISSO, E DEIXEI ISTO BEM CLARO COMENTADO NO CÓDIGO DE EXEMPLO. Isso aqui é só pra fins didáticos, não havia necessidade de criar uma tabelas com colunas criptografadas e conexão com banco de dados só para mostrar como autenticar. Pesquise sobre isso e defina qual a melhor forma de autenticar o usuário no seu sistema. Aqui faço uma verificação simples e, se estiver tudo certo:

1 – Instancio a classe Container; – entendeu agora o porque do “use” lá no topo da classe?
2 – salvo na memória um atributo qualquer, somente para identificar que o usuário se autenticou.
3 – redireciono para a página inicial.

Que comece a mágica!

Vamos trabalhar no arquivo Module.php, dentro do módulo principal da sua aplicação.
Nele, você vai adicionar o seguinte trecho aos uses:

use Zend\ModuleManager\ModuleManager;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
use Zend\Session\Container;

Explicando o Container

Esta classe serve para trabalharmos com containers no ZF2, e, assim como sessão, serve para gravar dados na memória e acessa-los de qualquer lugar da aplicação.

Agora, vamos adicionar uma função no corpo da classe Module.php

public function init(ModuleManager $moduleManager)
    {
        $sharedEvents = $moduleManager->getEventManager()->getSharedManager();
        
        $sharedEvents->attach('Zend\Mvc\Controller\AbstractActionController', MvcEvent::EVENT_DISPATCH, array($this, 'verificaAutenticacao'), 100);
    }

O que ela faz?

A função init() está fazendo toda uma parada cabulosa por trás que “anexa” uma instrução qualquer ao módulo, obrigado o framework a obedecer essa instrução toda vez que o código passar por aqui. Como ele passa por aqui SEMPRE QUE UM CONTROLLER E UMA ACTION FOR ACESSADA, (olha nossa porta milagrosa aqui!), toda vez que você carregar uma URL dessa aplicação, ele vai executar o que você quiser antes de acessar o caminho desejado.

Perceba que eu “anexei” a função “verificaAutenticacao” ao módulo. Vamos criá-la e ver o que ela faz? Logo abaixo da função init(), adicione:

public function verificaAutenticacao($e)
    {   
        // vamos descobrir onde estamos?
        $controller = $e->getTarget();
        $rota = $controller->getEvent()->getRouteMatch()->getMatchedRouteName();
        
        if ($rota != 'login' && $rota != 'login/default') {
            
            $sessao = new Container('Auth');
            if (!$sessao->admin) {
                return $controller->redirect()->toRoute('login');
            }
            
        }
    }

Primeiro estou pegando, na variável $rota, qual o caminho que estou tentando acessar.

Depois estou verificando se a rota obtida bate com a rota de Login da aplicação (agora a única rota que posso acessar sem estar logado). Se não bater, eu verifico se existe na memória a variável de autenticação, obtida apenas quando o usuário se loga corretamente (lembra que definimos lá na em login/index?). Se não existir, eu redireciono novamente para a rota de login, e caso exista, deixo o cara passar tranquilo para acessar tudo que ele tem direito dentro da aplicação.

Para fechar, implemente na action “sair” o seguinte código:

public function sairAction()
    {
        $sessao = new Container;
        $sessao->getManager()->getStorage()->clear();
        return $this->redirect()->toRoute('login');
    }

Ele busca o objeto que guardei em memória, e apaga todo seu conteúdo. A variável de autenticação morreu, e o usuário precisará se autenticar novamente para acessar qualquer página do seu sistema. Por fim, para facilitar, redirecione o infeliz usuário para a página de login.

Para finalizar de vez, faça suas firulas na action “acesso-negado”, informando o cara da impossibilidade de entrar no sistema.

Pronto, você protegeu seu sistema de acessos indevidos!

Obrigado por ler até aqui, fique a vontade para comentar, tirar dúvidas e dar sugestões sobre o post.


Publicado em Desenvolvimento, Zend Framework 2
  • Roberto Vieira

    Parabéns Ricardo!
    Continue assim, pois estou aprendendo muito com você.
    Sabe dar explicações como poucos, espero que não pare mais de criar seus grandes artigos.

  • Roberto Vieira

    Quando você fala em criar um controller login… Faço isto em que modulo? Um novo?

    • Chris Rocha

      eu fiz no módulo Application, e ficou ok.

      Quando concluíres, teste se no Internet Explorer funciona. Para mim só funcionou no chrome/firefox.

      • Roberto Vieira

        Obrigado pelo comentário. Só vi agora, mas mudei de framework…

  • Vitor

    Excelente!

  • Renato Alencar da Silva

    Excelente Ricardo, salvou meu dia.

    Muito obrigado

  • Nilton Morais

    Caso eu queria pegar na view o nome do usuário que está na sessão, como faço?

    • Ricardo Brusch

      Tudo bem Nilton?

      Tchê, tem algumas maneiras de fazer isso. Se tu implementou teu Login com a ajuda do serviço de autenticação do ZF2, pode utilizar um Helper padrão do framework (documentação http://framework.zend.com/manual/current/en/modules/zend.view.helpers.identity.html) .

      Se por acaso utilizou teu próprio serviço de autenticação, dá pra criar um ViewHelper que atenda as tuas necessidades, e chamá-lo da mesma forma que o “Identity” é chamado.

      Assim que possível (passadas correrias de faculdade, família, etc..), pretendo criar um post mostrando como utilizar um ViewHelper próprio, pra facilitar o entendimento dessa ferramenta que é muito prática =D

      Espero que minha resposta tenha te dado uma luz pra continuar teus estudos e obrigado pelo comentário!

  • Nilton Morais

    Criei uma rota “restrito” e sempre que ($rota == ‘restrito’) eu verifico se existe a sessão admin, está dando certo.. porém dentro dessa rota eu criei outras rotas filhas para as actions restrito[:/action], e quando o usuário não está autenticado se eu acessar /restrito ele vai para a página de login, porém se eu acessar /restrito/rota ele acessa direto, mesmo não estando autenticado.

  • Antônio Costa

    Ótimo artigo parabéns, espero que você continue publicando

  • Rogerio Martins Lamarques

    muito bom, ajudou pra caramba

  • Chris Rocha

    Muito bom!!

    mas no IE não funciona. Por algum motivo, os valores inseridos no container não ficam armazenados!

  • João Ricardo Biasi Gontijo

    Como faço para passar um parâmetro exemplo: /login/200

    o parâmetro 200 seria um ID ou um URL amigável

    • Ricardo Brusch

      Buenas, João! Obrigado pelo comentário!
      Para passar parâmetros na URL, mas deixando-a amigável, precisas configurar os arquivos de Rota do ZF2. Dê uma olhada neste link http://blog.ricardobrusch.com.br/arquivos-de-configuracao-do-zend-framework-2/ , onde falo um pouco sobre as rotas, e atente para a type Segment.

      Segue também, o link oficial da Zend falando sobre isso.https://framework.zend.com/manual/2.1/en/modules/zend.mvc.routing.html#zend-mvc-router-http-segment

      com uma boa configuração, podes deixar tua URL bem amigável e ainda assim trabalhar com múltiplos parâmetros, opcionais ou não, e ainda validados com expressão regular.

      Espero ter ajudado, boa sorte na continuidade dos teus estudos =)

      Forte abraço!

      • João Ricardo Biasi Gontijo

        Quando eu passo o meu parametro depois do login ficando assim: http://url/login/200 ele me retorna erro

        Segue meu router:

        ‘login’ => array(
        ‘type’ => ‘ZendMvcRouterHttpLiteral’,
        ‘options’ => array(
        ‘route’ => ‘/login’,
        ‘defaults’ => array(
        ‘controller’ => ‘ApplicationControllerLogin’,
        ‘action’ => ‘index’,
        ),
        ),
        ‘may_terminate’ => true,
        ‘child_routes’ => array(
        ‘default’ => array(
        ‘type’ => ‘Segment’,
        ‘options’ => array(
        ‘route’ => ‘/[:action[/:id]]’,
        ‘constraints’ => array(
        ‘action’ => ‘[a-zA-Z][a-zA-Z0-9_-]*’,
        ‘id’ => ‘[0-9][0-9_-]*’,
        ),
        ‘defaults’ => array(),
        ),
        ),
        ),
        ),

        • Ricardo Brusch

          Vamos lá, “/login” é tua rota literal. até ai OK.
          em child_rotes, quando definie a Segment, tu diz que a rota, será composta de ACTION / ID, [:action[/:id]]

          ou seja, o ZF está esperando algo tipo “/login/action/200”

          tente trocar o “action” da URL que sugeri acima por “index” ou o nome da tua action de login, para ficar algo do tipo “/login/index/200” ou “/login/action-qualquer/200”

          Uma dica bacana: quando precisar apresentar código-fonte, utilize serviço como o https://gist.github.com/ , onde tu posta o código e gera um link para o mesmo. Facilita na leitura de todos =D

          Abração, e me informa o resultado aí, cara!

          • João Ricardo Biasi Gontijo

            Obrigado pelo apoio!

            Sobre o funcionamento, como ta configurado no module para verificar a rota se é diferente de login e login/default, eu colocando /login/index/200 ele me redireciona automaticamente para /login

          • Ricardo Brusch

            Estranho, tchê =(

            a URI ‘/login/index/200’, pela tua configuração de rota, fecha com os requisitos da child “default”, então o ZF não poderia redirecionar. Tanto é assim, que no exemplo do tutorial quando não preenchido login corretamente, ele redireciona para uma página de acesso negado que está dentro do controller Login e no arquivo de rotas bate com o segment “default”.

            Dá uma verificada na ortografia cara,ou talvez uma debugada no meio do processo possa te dar uma luz de onde, e em que momento tua rota não está fechando com o esperado.

          • João Ricardo Biasi Gontijo

            Eu to usando os eu exemplo, sinceramente não sei onde está o erro, mas n consigo passar um parâmetro :s

          • João Ricardo Biasi Gontijo
          • Ricardo Brusch

            Cara, não estás usando um virtualhost aí no projeto? Quando usamos frameworks, é fortemente indicado o uso de virtualhosts. O roteamento é sempre baseado em uma URL, e no teu caso aí tu tem uma série de pastas até chegar no controller / action / parâmetro.

            Dúvida, se tu tirar o parâmetro da URL ( /3 ), funciona?

            Depois, tenta modificar o código conforme isto > https://gist.github.com/ricardobrusch/7f1a0d53ed84e6c8552940c26e67cfd3

          • João Ricardo Biasi Gontijo

            Sim quando eu retiro o parâmetro 3 ele me mostra o form de login, usei o seu código e me retornou isso “login/default”

          • Ricardo Brusch

            Certo, na imagem que você enviou você tirou o [:/id] de ‘route’, conforme havia digitado no primeiro comentário. Tente recolocar e vamos ver o que vai dar

          • João Ricardo Biasi Gontijo

            da na mesma… sempre me printa login/default

          • Ricardo Brusch

            Beleza cara, esqueci de pedir pra ti remover o print_r e o die inseridos anteriormente também =)

          • João Ricardo Biasi Gontijo

            Ricardo, você tem skype ou outra forma de contato?

          • João Ricardo Biasi Gontijo

            ??

          • Yuri Matheus Gress

            João, Boa Tarde!

            O que está acontecendo é que na configuração da rota de login, não está sendo especificado o parâmetro “id” além da rota ser do tipo Literal.
            Segue abaixo um exemplo de como sua rota deveria ser.

            ‘login’ => array(
            ‘type’ => ‘Zend\Mvc\Router\Http\Segment’,
            ‘options’ => array(
            ‘route’ => ‘/login/[:action[/:id]]’,
            ‘defaults’ => array(
            ‘controller’ => ‘Application\Controller\Login’,
            ‘action’ => ‘index’,
            ),
            ‘constraints’ => array(
            ‘action’ => ‘[a-zA-Z][a-zA-Z0-9_-]*’,
            ‘id’ => ‘[0-9]+’,
            ),
            ),
            ),

          • Ricardo Brusch
Volte Sempre!
Meus artigos te ajudaram de alguma forma e você não sabe como me agradecer? Que tal me pagar uma Coca-Cola bem gelada?

Sobre o autor

Me chamo Ricardo Brusch, sou programador e desenvolvedor de sistemas para internet. Também sou aspirante a escritor, e você pode ler alguns de meus contos malucos em contos.ricardobrusch.com.br.
Parceiros





Publicidade