Saber trabalhar com sessões em qualquer linguagem web é de extrema importância para usar recursos importantes como: usuário logado, produtos escolhidos e etc. Por outro lado o uso dos filtros em JSF nos ajudam a realizar operações importantes antes que o usuário conclua o seu fluxo de navegação. Com estes dois recursos em mãos podemos criar estruturas poderosas e complexas.

Neste artigo vamos aprender como sessões funcionam e como aplicá-las em JSF, para complementar o artigo veremos como trabalhar com filtros em conjunto com a sessão que criamos.

Sessões e Cookies

Você já deve ter ouvido falar sobre Sessões e Cookies mesmo que minimamente. Ambos armazenam informações que devem ser mantidas durante toda a navegação do usuário, mas qual a diferença destes?

A grande diferença entre ambos está no fato de que os Cookies são armazenados no navegador e as sessões são armazenadas no servidor web. Em alguns casos é melhor gravar informações sigilosas em sessões e não em cookies, exatamente porque não desejamos que ela fique trafegando pela web.

Imagine por exemplo quando você faz acesso ao seu “Banking Online” você não gostaria que a senha da sua conta fosse rastreada por um Cracker, sendo assim a Sessão garante maior segurança deste ponto de vista.

Tudo isso faz-se necessário pelo fato do HTTP ser stateless, isso significa que ele não mantém um estado/conexão, a cada requisição feita novos dados são enviados e os antigos são perdidos, o HTTP não tem como saber que a próxima requisição veio da mesma origem que a anterior.

Filtros em JSF

Os filtros funcionam como redirecionadores de fluxo de navegação, em outras palavras você pode evitar que o usuário consiga acessar determinado conteúdo se alguma condição não for aceita, ou você poderia criar um sistema de logs gravando o acesso do usuário a todas as páginas do sistema deixando o mesmo prosseguir com o fluxo. O filtro tem diversas utilidades e não apenas “sistemas de login”.

Neste artigo veremos a utilização de filtros para gravar históricos de navegação anteriores, permitindo que o usuário volte quando precisar. Comumente utilizamos a função “history.back()” do JavaScript para permitir que o usuário volte a uma página anteriormente acessada, mas este comando tem alguns problemas:

  • Dependendo do histórico de navegação você pode continuar na mesma página de ocorreu um refresh dela e nem sempre isso é o desejado. No caso em que o usuário está em um formulário de cadastro e deseja voltar a listagem de registros, isso pode ser um incômodo.
  • Se o usuário desabilitar o JavaScript então o history.back não funcionará mais.

Em nossa solução gravaremos sempre a navegação atual e a navegação anterior do usuário, se ele fizer um refresh na mesma página então checamos que a página acessada foi a mesma e não mudamos nada.

Desenvolvendo o SessionContext

Nossa classe SessionContext trabalhará com o padrão de projeto Singleton, assim garantimos que só existirá uma instância deste objeto durante toda a aplicação. Vamos mostrar a classe completa e depois explicá-la em partes, como mostra a Listagem 1

Listagem 1. SessionContext


  import javax.faces.context.ExternalContext;
  import javax.faces.context.FacesContext;
  import javax.servlet.http.HttpSession;
   
   
  public class SessionContext {
      
      private static SessionContext instance;
      
      public static SessionContext getInstance(){
         if (instance == null){
             instance = new SessionContext();
         }
         
         return instance;
      }
      
      private SessionContext(){
         
      }
      
      private ExternalContext currentExternalContext(){
         if (FacesContext.getCurrentInstance() == null){
             throw new RuntimeException("O FacesContext não pode ser chamado fora de uma requisição HTTP");
         }else{
             return FacesContext.getCurrentInstance().getExternalContext();
         }
      }
      
      
      public void encerrarSessao(){
         currentExternalContext().invalidateSession();
      }
      
      public Object getAttribute(String nome){
         return currentExternalContext().getSessionMap().get(nome);
      }
      
      public void setAttribute(String nome, Object valor){
         currentExternalContext().getSessionMap().put(nome, valor);
      }
      
  }

A classe acima segue o padrão Singleton para evitar múltiplas instâncias no mesmo contexto da aplicação. O método currentExternalContext() é de extrema importância para funcionamento da nossa sessão, o seu objetivo é retornar um objeto ExternalContext através da requisição HTTP atual e isso só é possível se uma requisição foi disparada. Isso significa que você não pode chamar este método fora de uma requisição HTTP, você terá o seguinte erro:

throw new RuntimeException("O FacesContext não pode ser chamado fora de uma requisição HTTP");

Você pode perguntar-se, mas qual a diferença entre o FacesContext e o ExternalContext? Não entraremos em detalhes sobre suas diferenças mas o importante a saber para este artigo é que o ExternalContext trabalha em uma “camada” mais baixa do que o FacesContext, sendo que o FacesContext foi projetado para trabalhar diretamente com recursos do JSF especificamente, enquanto que o ExternalContext consegue trabalhar com HTTP servlet, HTTP request e etc., que não estão ligados ao JSF, eles são usados pelo JSF.

Logo depois temos o método encerrarSessao() que como o próprio nome já diz, é responsável por encerrar a sessão aberta. Se você observar como estamos encerrando a sessão:

currentExternalContext().invalidateSession();

Notará que usamos o retorno do método currentExternalContext(), imediatamente todos os atributos salvos são perdidos e não haverá nenhuma sessão aberta para o usuário atual. Este método é útil para realizar logout em sistemas, assim o usuário precisará logar novamente e criar uma nova sessão.

Por fim temos dois métodos: setAttribute() e getAttribute(), vejamos :

public Object getAttribute(String nome){
         return currentExternalContext().getSessionMap().get(nome);
      }

O ExternalContext possui um método chamado getSessionMap() que retorna um Map com os atributos salvos na sessão corrente. Usamos este método para capturar o valor do atributo pelo seu nome:

public void setAttribute(String nome, Object valor){
         currentExternalContext().getSessionMap().put(nome, valor);
      }

Igualmente o método getAttribute(), o método setAttribute() usa o getSessionMap() para retornar os atributos da sessão em forma de Map, mas desta vez usando o put() para inserir os valores necessários.

Como usaremos nossa sessão? Lembre-se que só podemos fazer uso desta classe quando houver uma requisição HTTP, isso significa que você não pode chamar o SessionContext sem que haja uma requisição vinda do cliente (browser). Veja como podemos usar adicionando atributos a sessão:

SessionContext.getInstance().setAttribute(“valor”,123);

Podemos recuperar estas informações em qualquer parte da nossa aplicação. Muito útil para manter valores como: usuário logado, tempo logado, última página acessada, atributos do banco que podem perdurar durante toda a aplicação, evitando a busca contínua destes dados.

Desenvolvendo o Filtro

O nosso filtro irá capturar todo e qualquer acesso do usuário e manter a página anterior e atual que está sendo acessada, assim podemos garantir que ele possa voltar à página anterior sempre que precisar.

Como guardaremos estas informações? Através da nossa classe SessionContext criada na seção anterior, assim podemos recuperar em qualquer parte do código a página anterior para dar possibilidade de volta rápida e segura.

Um filtro é responsável por filtrar, como o próprio nome sugere, o fluxo de navegação do usuário, permitindo que o mesmo prossiga ou não. Neste momento que o filtro está sendo realizado, você pode realizar inúmeras tarefas, como por exemplo: salvar a página que está sendo acessada para futuras auditorias, realizar processamentos específicos para que a página seja renderizada corretamente e etc. Em nosso caso vamos salvar a página acessada e deixaremos o usuário continuar o fluxo normalmente, para ele será transparente este processo. Observe a Listagem 2.

Listagem 2. PageFilter


  package br.com.jwebbuild.filter;
   
  import java.io.IOException;
   
  import javax.servlet.Filter;
  import javax.servlet.FilterChain;
  import javax.servlet.FilterConfig;
  import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpSession;
   
  public class PageFilter implements Filter {
   
      public void destroy() {
         // TODO Auto-generated method stub
   
      }
   
      public void doFilter(ServletRequest request, ServletResponse response,
             FilterChain chain) throws IOException, ServletException {
   
         HttpSession sess = ((HttpServletRequest) request).getSession(true);
   
         String newCurrentPage = ((HttpServletRequest) request).getServletPath();
   
         if (sess.getAttribute("currentPage") == null) {
             sess.setAttribute("lastPage", newCurrentPage);
             sess.setAttribute("currentPage", newCurrentPage);
         } else {
   
             String oldCurrentPage = sess.getAttribute("currentPage").toString();
             if (!oldCurrentPage.equals(newCurrentPage)) {
               sess.setAttribute("lastPage", oldCurrentPage);
               sess.setAttribute("currentPage", newCurrentPage);
             }
         }
   
         chain.doFilter(request, response);
   
      }
   
      public void init(FilterConfig arg0) throws ServletException {
         // TODO Auto-generated method stub
   
      }
   
  }

O nosso filtro é um pouco mais complexo do que a classe SessionContext criada anteriormente, pois o nosso PageFilter implementa a interface Filter pois só assim é possível que o nosso filtro funcione e seja mapeado no web.xml (veremos mais à frente). Logo temos a implementação do método destroy() que não tem nenhuma lógica pois não usaremos ele. O método destroy() e o método init() geralmente são utilizados para configuração do filtro, mas nesse caso daremos atenção apenas ao doFilter().

O método doFilter() é chamado sempre que uma página está sendo acessada, página essa que deve estar sendo monitorada pelo nosso filtro.

Logo no início temos a recuperação da Sessão atual ou a criação de uma nova caso não exista:

HttpSession sess = ((HttpServletRequest) request).getSession(true);

Usamos o objeto request para capturar a sessão como um HttpSession. Depois capturamos a página atual que está sendo acessada:

 String newCurrentPage = ((HttpServletRequest) request).getServletPath();

Logo em seguida verificamos se não existe uma página atual gravada na sessão, caso isso seja verdade então sabemos que é o primeiro acesso do usuário. Neste caso iremos gravar a última página e a página atual como sendo as mesmas:


  if (sess.getAttribute("currentPage") == null) {
             sess.setAttribute("lastPage", newCurrentPage);
             sess.setAttribute("currentPage", newCurrentPage);
         }

Para não confundirmos os conceitos vamos chamar a página atual que está sendo acessada neste exato momento de PAR (Página Atual Real), a página atual que estava salva de PAH (Página atual do histórico) e a página anterior de PANT.

Caso já exista uma PAH na Sessão, esta deverá virar a página anterior e página salva no objeto newCurrentPage (PAR) deverá ser a real página atual. O problema é que devemos checar sempre se PAR não é igual a PAH, para não sobrescrevemos a página anterior e perdermos informações.

Imagine o seguinte exemplo:

  1. O usuário acessa pela primeira vez o sistema, na página index.xhtml. A PANT recebe o valor index.xhtml assim como a PAH, por tratar-se do seu primeiro acesso.
  2. No segundo acesso do usuário ele acessa a página home.xhtml, assim verificamos que PAR é home.xhtml que é diferente de PAH, sendo assim a nossa PANT torna-se a PAH e a nossa PAH tornar-se a PAR.
  3. Se no segundo acesso do usuário ele acessar home.xhtml?parametro=ola, isso ainda significa que ele está na mesma página então não devemos mudar nada, por isso checamos de PAR é igual a PAH e nesse caso nada fazemos.

Por fim, depois desta lógica nós iremos executar o método que permitirá que o usuário prossiga com o seu fluxo de navegação:

chain.doFilter(request, response);

Para que o filtro seja ativado nós temos que configurá-lo em nosso arquivo web.xml do projeto, assim o mesmo começará a monitorar as páginas acessadas e verificar se deve ou não passar a algum filtro. Vejamos a Listagem 3.

Listagem 3. Habilitando filtro no web.xml


  <filter>
       <filter-name>PageFilter</filter-name>
       <filter-class>br.com.meuprojeto.filter.PageFilter</filter-class>     
   </filter>
   
   <filter-mapping>
      <filter-name>PageFilter</filter-name>
      <url-pattern>/core/*</url-pattern>
  </filter-mapping>

Acima temos duas tags para definição do filtro: filter e filter-mapping, onde cada uma tem outras tags internas.

A tag filter define o nome do nosso filtro através do filter-name e o local onde ele se encontra em nosso projeto através do filter-class. Já a tag filter-mapping é responsável por definir quais páginas este filtro irá monitorar, em nosso caso tudo que estiver dentro do diretório “/core”.

Pronto, agora temos um filtro que faz o uso do SessionContext para gravar os dados manipulados no doFilter(). Vamos recapitular o que acontecerá em nosso fluxo de navegação:

  1. Quando o usuário acessar pela primeira vez, se ele estiver navegando dentro da pasta “/core” o nosso PageFilter será chamado e consequentemente como ainda não deve haver sessão criada, o PageFilter irá se responsabilizar por inicializar uma nova sessão através desta linha:
    HttpSession sess = ((HttpServletRequest) request).getSession(true);

    Com isso garantimos a existência de uma sessão para guardar os atributos necessários.
  2. Com a sessão criada nós gravamos a página que está sendo acessada (seguindo a mesma lógica que já explicamos nas seções anteriores).

Monitorando a sessão criada

Como podemos saber quando uma sessão foi criada ou destruída? O ideal é criar um Listener que seja capaz de monitorar esse ciclo de vida da nossa Sessão. Vejamos como fazer com o código da Listagem 4.

Listagem 4. SessionListener


  import java.text.SimpleDateFormat;
  import java.util.Date;
   
  import javax.servlet.http.HttpSessionEvent;
  import javax.servlet.http.HttpSessionListener;
   
  public class SessionListener implements HttpSessionListener {
      
   
      public void sessionCreated(HttpSessionEvent event) {        
           System.out.println("Sessão criada " + event.getSession().getId());
      }
   
      public void sessionDestroyed(HttpSessionEvent event) {     
           String ultimoAcesso = (new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")).format(new Date(event.getSession().getLastAccessedTime()));
           System.out.println("Sessão expirada "+event.getSession().getId()+". Ultimo Acesso = "+ultimoAcesso);
      }
   
  }

Nossa classe SessionListener implementa a interface HttpSessionListener que é mandatório para que o listener funcione. O método sessionCreated() mostra o ID da sessão quando a mesma for criada, já o método sessionDestroyed() mostra o ID da sessão que está sendo destruída e a data e hora de último acesso:

Sessão criada 7F37598DEAEBF1E8B0FAD186DE784853

A sessão foi foi criada com o ID mencionado. A partir deste momento podemos começar a gravar nossos atributos:

Sessão expirada 53C60A043734D9CCCC889F81CE93CE5C. Ultimo Acesso = 08/03/2015 13:41:33

Temos duas formas de destruir uma sessão:

  1. Chamando o método invalidateSession(); que usamos no SessionContext:
    
          public void encerrarSessao(){
           currentExternalContext().invalidateSession();
        }
  2. A outra forma é deixar que a sessão expire através de um tempo definido no web.xml, como mostra a Listagem 5.

Listagem 5. Definindo tempo para expirar sessão


  <session-config>
               <session-timeout>60</session-timeout>
         </session-config>

Acima temos a definição do session-timeout em 60 minutos, ou seja, se em 60 minutos não houver nenhum requisição cliente-servidor a sessão irá expirar-se automaticamente e a mensagem mostrada anteriormente aparecerá.

Se chamarmos o método invalidatesession() a mensagem mostrada também será a mesma mostrada acima, mas isso não significa que a sessão foi expirada. Neste caso ela foi destruída manualmente. Você pode melhorar esta mensagem deixando-a mais genérica, exemplo: Sessão finalizada em vez de Sessão expirada ou destruída.

Se você iniciar o seu servidor agora e tentar ver as mensagens verá que não funcionará, pois ainda falta uma configuração: adicionar o Listener no nosso web.xml, como mostra a Listagem 6.

Listagem 6. Adicionando o listener no web.xml


  <listener>
               <listener-class>br.com.meuprojeto.util.SessionListener</listener-class>
         </listener>

Acima criamos o mapeamento do nosso SessionListener no arquivo web.xml, usando a tag <listener> e internamente a <listener-class> que mapeará o nosso SessionListener, agora sempre que uma sessão for criada ou destruída a classe SessionListener será chamada, pois ela implementa o HttpSessionListener, caso contrário não conseguiríamos fazer este mapeamento.

Vimos neste artigo como fazer uso de Sessões e Filtros com JSF. Criamos uma classe chamada SessionContext que é capaz de armazenar atributos na sessão corrente e usamos estes atributos em nosso PageFilter que armazena a página acessada pelo usuário.

A aplicação de ambos os conceitos vai além apenas do armazenamento de atributos, cabe a você leitor, dado os conceitos ministrados neste artigo, desenvolver a lógica necessária para o seu projeto. O importante é ter conhecimento de toda a base como a sessão e o filtro funcionam.