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:
- 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.
- 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.
- 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:
- 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. - 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:
- Chamando o método invalidateSession(); que usamos no SessionContext:
public void encerrarSessao(){ currentExternalContext().invalidateSession(); }
- 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.