Neste artigo veremos como construir um sistema de login com JSF 2.0 utilizando Filters, um recurso muito poderoso e útil para tal tarefa. Nosso sistema de login contará ainda com um nível a mais de segurança, implementando a criptografia MD5 nas senhas, evitando assim que as mesmas possam ser visualizadas por qualquer um.

Em qualquer sistema é muito comum pensarmos como será a estrutura de segurança do mesmo, garantir que apenas os usuários autorizados acessem ao sistema é muito importante e essencial. Nem sempre o cliente diz: “Preciso que tenha um módulo de login", mas pela experiência adquirida já devemos ter a noção de que isso será necessário, pelo menos na maioria das vezes.

Nota: Não deixe de conferir nossos cursos de Java online

Neste artigo veremos construir um módulo de login utilizando Filters em JSF 2.0. Podem lhe dizer que existem outras formas como JAAS, Spring Security e etc., mas para aqueles que preferem seu próprio esquema de segurança, esse artigo será muito bem vindo e ainda para aqueles que preferem frameworks prontos, este artigo também é válido para aprendizado.

Utilizaremos neste artigo a criptografia MD5 para gravar a senha do usuário com segurança, então vejamos um pouco sobre MD5.

O MD5 ou Message-Digest Algorithm 5 é um algoritmo hash de 128bits unidirecional e pelo simples fato de ser unidirecional não há como descriptografar o mesmo, ou seja, se você criptografar determinada senha em MD5, não terá como fazer o processo inverso, que seria descobrir a senha contida no MD5. Então se não há como descriptografar um hash MD5 como saberemos se a senha que o usuário digitou está correta? Pense um pouco, nós podemos criptografar a senha digitada pelo usuário para MD5 e simplesmente comparar os dois hash MD5. Sendo assim, se os dois forem iguais saberemos que a senha está correta. Mas caso o usuário esqueça a senha, não há maneira de recuperá-la, apenas gerar uma nova senha. É por esse motivo que em muitos sistemas a recuperação da senha é na verdade a geração de uma nova senha.

Criptografar a senha em MD5 lhe dá muitos pontos em segurança, confiabilidade e qualidade. Começando pelo fato de que qualquer pessoa que tiver acesso ao banco de dados não poderá visualizar as senhas de nenhum usuário, pois imagine se o usuário “joao2014” utiliza sua senha para outras coisas como: bank online, e-mail, Facebook e etc.

Por isso, a senha do usuário deve ser uma informação sigilosa que nem você (desenvolvedor) deve ter conhecimento. Essa é uma questão simples de ética profissional. Existem ainda outros algoritmos HASH para criptografar informações, mas não é nosso foco estudá-los, nos atearemos ao MD5.

Dada toda a explicação acima sobre a importação do uso de um Hash MD5 juntamente com o Filter no JSF 2.0, começaremos na prática a construir nosso sistema.

Construindo sistema de login

Partiremos do princípio para criar nosso sistema de login, utilizando frameworks como JPA/Hibernate e Spring com banco de dados PostgresSQL. Começaremos então a definir a estrutura do nosso banco de dados, conforme o código da Listagem 1.


CREATE TABLE usuario
(
  id serial NOT NULL,
  data_cadastro date,
  email character varying(255),
  nome character varying(255),
  senha character varying(255),
  CONSTRAINT usuario_pkey PRIMARY KEY (id ),
  CONSTRAINT usuario_email_key UNIQUE (email )
)
Listagem 1. Criação da Tabela

Criada a tabela acima em nosso banco de dados, precisamos criar nossa classe Usuario, que ficará como o código definido na Listagem 2.


import java.util.Date;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
 
@Entity
@NamedQueries(value = { @NamedQuery(name = "Usuario.findByEmailSenha",
query = "SELECT c FROM Usuario c "
                   + "WHERE c.email = :email AND c.senha = :senha")})
@Table(name = "usuario")
public class Usuario {
 
         /**
          *
          */
         private static final long serialVersionUID = 1L;
         @Transient
         public static final String FIND_BY_EMAIL_SENHA = "Usuario.findByEmailSenha";       
 
         @Id
         @GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY)
         private Integer id;
         
 
         @Column
         private String nome;
 
         @Column(unique = true)
         private String email;
 
         @Column
         private String senha;
 
         @Column(name = "data_cadastro")
         @Temporal(TemporalType.DATE)
         private Date dataCadastro;
 
         public Integer getId() {
                   return id;
         }
 
         public void setId(Integer id) {
                   this.id = id;
         }
         
         public String getNome() {
                   return nome;
         }
 
         public void setNome(String nome) {
                   this.nome = nome.trim();
         }
         
         public String getEmail() {
                   return email;
         }
 
         public void setEmail(String email) {
                   this.email = email.trim().toLowerCase();
         }
 
         public String getSenha() {
                   return senha;
         }
 
         public void setSenha(String senha) {
                   this.senha = senha.trim();
         }
 
         public Date getDataCadastro() {
                   return dataCadastro;
         }
 
         public void setDataCadastro(Date dataCadastro) {
                   this.dataCadastro = dataCadastro;
         }
 
         @Override
         public int hashCode() {
                   final int prime = 31;
                   int result = 1;
                   result = prime * result + ((id == null) ? 0 : id.hashCode());
                   return result;
         }
 
         @Override
         public boolean equals(Object obj) {
                   if (this == obj)
                            return true;
                   if (obj == null)
                            return false;
                   if (getClass() != obj.getClass())
                            return false;
                   
                   return (obj instanceof AbstractBean) ? (this.getId() == null ? this == obj : 
                   this.getId().equals(((AbstractBean)obj).getId())):false;
         }
         
}
Listagem 2. Criando Classe Usuario

Nossa classe é completa e possui todas as notações JPA necessárias, juntamente com os métodos equals() e hashCode() e as namedQueries que nos serão úteis para pesquisar os usuários no banco e dados.

Como dissemos anteriormente, se fossemos mostrar detalhes da construção de cada parte do sistema com por exemplo: DAO, (Data Access Object), BO (Bussiness Object) e Configurações, nosso artigo perderia o foco, então mostraremos agora os métodos de verificação de login presentes em nosso BO, chamado UsuarioBOImpl. Observe o código da Listagem 3.


// Verifica se usuário existe ou se pode logar
public Usuario isUsuarioReadyToLogin(String email, String senha) {
     try {
            email = email.toLowerCase().trim();
            logger.info("Verificando login do usuário " + email);
            List retorno = dao.findByNamedQuery(
                          Usuario.FIND_BY_EMAIL_SENHA,
                          new NamedParams("email", email
                                       .trim(), "senha", convertStringToMd5(senha)));

            if (retorno.size() == 1) {
                   Usuario userFound = (Usuario) retorno.get(0);
                   return userFound;
            }

            return null;
     } catch (DAOException e) {
            e.printStackTrace();
            throw new BOException(e.getMessage());
     }
}
Listagem 3. Método de validação do usuário no UsuarioBOImpl

Bom, nosso método recebe como parâmetro um Email e Senha, que são passados para o DAO utilizando aquela NamedQuery chamada findByEmailSenha que definimos em nosso Bean Usuario. O importante aqui é perceber duas coisas:

  1. A senha que é passada por parâmetro não está criptografada, sendo assim, não conseguiríamos comparar com a senha no banco. Então antes de passar o parâmetro ao DAO, convertemos a senha para MD5 com o método convertStringToMD5(senha).
  2. Caso esse retorno do DAO seja uma Lista com um elemento, significa que o usuário foi encontrado no banco e retornamos o mesmo, caso contrário o retorno será null.

Veja no código da Listagem 4 como é nosso método para converter de String para MD5.


private String convertStringToMd5(String valor) {
  MessageDigest mDigest;
  try { 
      //Instanciamos o nosso HASH MD5, poderíamos usar outro como
      //SHA, por exemplo, mas optamos por MD5.
      mDigest = MessageDigest.getInstance("MD5");
      
      //Convert a String valor para um array de bytes em MD5
      byte[] valorMD5 = mDigest.digest(valor.getBytes("UTF-8"));
      
      //Convertemos os bytes para hexadecimal, assim podemos salvar
      //no banco para posterior comparação se senhas
      StringBuffer sb = new StringBuffer();
      for (byte b : valorMD5){
             sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1,3));
      }

      return sb.toString();
      
  } catch (NoSuchAlgorithmException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      return null;
  } catch (UnsupportedEncodingException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      return null;
  }
}
Listagem 4. Método conversor de String para MD5

Então agora temos dois métodos importantes para nossa aplicação BO: um para verificar se o usuário é válido e outro para converter a senha para MD5. O próximo passo é criar um ManagedBean que comunicará a página XHTML de Login com o nosso BO, vamos chamá-lo de UsuarioLogadoMBImpl. Observe o código da Listagem 5.


import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.SessionScoped;
import javax.faces.context.FacesContext;
 
import org.apache.log4j.Logger;
 
/**
 * Controla o LOGIN e LOGOUT do Usuário
 * */
@ManagedBean(name = "usuarioLogadoMB")
@SessionScoped
public class UsuarioLogadoMBImpl extends BasicMBImpl {
 
    private static final long serialVersionUID = 1L;
 
    private static Logger logger = Logger.getLogger(UsuarioLogadoMBImpl.class);
 
    @ManagedProperty(value = "#{userBO}")
    private UserBOImpl userBO;
 
    private String email;
    private String login;
    private String senha;
 
    /**
     * Retorna usuario logado
     * */
    public User getUser() {
       return (User) SessionContext.getInstance().getUsuarioLogado();
    }
 
    public String doLogin() {
       try {
           logger.info("Tentando logar com usuário " + login);
           User user = userBO.isUsuarioReadyToLogin(login, senha);
 
           if (user == null) {
             addErrorMessage("Login ou Senha errado, tente novamente !");
             FacesContext.getCurrentInstance().validationFailed();
             return "";
           }
 
           Usuario usuario = (Usuario) getUserBO().findByNamedQuery(Usuario.FIND_BY_ID,
                 new NamedParams("id", user.getId())).get(0);
           logger.info("Login efetuado com sucesso");
           SessionContext.getInstance().setAttribute("usuarioLogado", usuario);
           return "/index.xhtml?faces-redirect=true";
       } catch (BOException e) {
           addErrorMessage(e.getMessage());
           FacesContext.getCurrentInstance().validationFailed();
           e.printStackTrace();
           return "";
       }
 
    }
 
    public String doLogout() {
       logger.info("Fazendo logout com usuário "
               + SessionContext.getInstance().getUsuarioLogado().getLogin());
       SessionContext.getInstance().encerrarSessao();
       addInfoMessage("Logout realizado com sucesso !");
       return "/security/form_login.xhtml?faces-redirect=true";
    }
 
    public void solicitarNovaSenha() {
       try {
           getUserBO().gerarNovaSenha(login, email);
           addInfoMessage("Nova Senha enviada para o email " + email);
       } catch (BOException e) {
           addErrorMessage(e.getMessage());
           FacesContext.getCurrentInstance().validationFailed();
       }
    }
 
    public UserBOImpl getUserBO() {
       return userBO;
    }
 
    public void setUserBO(UserBOImpl userBO) {
       this.userBO = userBO;
    }
 
    public String getLogin() {
       return login;
    }
 
    public void setLogin(String login) {
       this.login = login;
    }
 
    public String getSenha() {
       return senha;
    }
 
    public void setSenha(String senha) {
       this.senha = senha;
    }
 
    public String getEmail() {
       return email;
    }
 
    public void setEmail(String email) {
       this.email = email;
    }
 
}
Listagem 5. ManagedBean para Login do Usuário

Na listagem acima temos uma chamada ao nosso método isUsuarioReadyToLogin() que está no nosso BO criado anteriormente. Caso a instância da variável user seja nula, significa que não foi encontrado nenhum usuário na base, então simplesmente retornamos um erro ao usuário e redirecionamos o mesmo para a página de login novamente. Caso seja encontrado o usuário na base, então colocaremos o objeto na sessão atual, que veremos mais adiante como é manipulada.

Temos também um método para solicitar uma nova senha. Não entraremos em detalhes de como utilizá-lo, fica como exercício para você desenvolver a lógica do mesmo no BO.

O método de logout é simples, apenas fazemos o inverso que fizemos no método de login: em vez de adicionar um atributo na sessão, nós destruímos a sessão atual.

Vamos ver agora nossa página XHTML de Login. Observe o código da Listagem 6.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
 xmlns:h="http://java.sun.com/jsf/html"
 xmlns:f="http://java.sun.com/jsf/core"
 xmlns:ui="http://java.sun.com/jsf/facelets"
 xmlns:p="http://primefaces.org/ui"
 xmlns:pe="http://primefaces.org/ui/extensions">

<h:head>
</h:head>
<h:body>

 <ui:include src="/templates/processing.xhtml" />
 <p:growl autoUpdate="true" />

 <div id="container">

       <div id="login_box">
              <div id="logo_login">
                     <h:graphicImage value="/resources/image/logo.jpg" 
                     width="120" />
              </div>
              <div id="titulo_login">Acesso Restrito</div>
              <div id="subtitulo_login">Apenas Pessoas autorizadas podem
                     acessar este painel</div>
              <h:form id="formLogin" enctype="multipart/form-data">

                     <p:growl autoUpdate="true" id="messages" />

                     <p:panelGrid styleClass="semBorda" columns="2">

                            <h:outputText value="Login: " />
                            <p:inputText value="#{usuarioLogadoMB.login}" 
                            size="37" />

                            <h:outputText value="Senha: " />
                            <p:password value="#{usuarioLogadoMB.senha}" 
                            size="37" />

                     </p:panelGrid>

                     <br />
                     <br />
                     <p:panelGrid columns="2" styleClass="semBorda">
                            <p:commandButton icon="ui-icon-unlocked" 
                            value="Entrar"
                                  action="#{usuarioLogadoMB.doLogin}" />
                            <p:commandButton icon="ui-icon-mail-closed" 
                            value="Recuperar Senha"
                                  onclick="varDialogResetarSenha.show()" 
                                  type="button" />
                     </p:panelGrid>

              </h:form>
              
              <h:form>
              <p:dialog id="dialogResetarSenha" header="Resetar Senha"
                     widgetVar="varDialogResetarSenha" modal="true" 
                     showEffect="fade" resizable="false"
                     hideEffect="fade">
                     <p:panelGrid styleClass="semBorda" columns="1">
                            <p:inputText value="#{usuarioLogadoMB.login}" 
                            required="true" size="60"
                                  requiredMessage="O login é obrigatório" 
                                  id="loginRecuperaSenha" />
                            <p:watermark value="Digite seu login" 
                            for="loginRecuperaSenha" />
                            
                            <p:inputText value="#{usuarioLogadoMB.email}" 
                            required="true" size="60"
                                  requiredMessage="O email é obrigatório" 
                                  id="emailRecuperaSenha" />
                            <p:watermark value="Digite seu email" 
                            for="emailRecuperaSenha" />
                     </p:panelGrid>
                            <p:commandButton value="Confirmar" 
                            icon="ui-icon-circle-check"
                                  actionListener="#{usuarioLogadoMB
                                  .solicitarNovaSenha}"
                                  oncomplete="if (!args.validationFailed)
                                  {varDialogResetarSenha.hide()}" />
                            <p:commandButton value="Cancelar" 
                            type="button" icon="ui-icon-circle-close"
                                  onclick="varDialogResetarSenha.hide()" />

              </p:dialog>
       </h:form>

       </div>


 </div>
 <!-- div container -->
</h:body>
</html>
Listagem 6. login.xhtml

Nosso XHTML acima realiza o login do usuário e ainda gera uma nova senha se o mesmo solicitar.

Temos então quase todo mecanismo pronto:

  1. A página de login;
  2. A comunicação do XHTML com o BO através do ManagedBean;
  3. As tabelas do banco e o mapeamento via JPA no Java da nossa classe Usuario;
  4. As validações e conversões no BO.

Falta agora o principal, que é criar o Filter para direcionar o usuário para o local certo e criar o Session capaz de guardar as informações do nosso usuário, então começaremos definindo o filter no arquivo web.xml, conforme mostra a Listagem 7.


<!--  login filter -->
<filter>
    <filter-name>LoginFilter</filter-name>
    <filter-class>br.com.meuprojeto.LoginFilter</filter-class>    
</filter>
<filter-mapping>
    <filter-name>LoginFilter</filter-name>
    <url-pattern>/restricted/*</url-pattern>
</filter-mapping>
Listagem 7. Definindo filter no web.xml

Acima estamos definindo duas coisas:

  1. Dizemos através da tag que a nossa classe responsável por realizar o controle do filtro fica em br.com.meuprojeto.LoginFilter e chama-se LoginFilter.
  2. Através da tag dizemos que o LoginFilter (definido através do ) deve interceptar todas as requisições que passam por /restricted/*, ou seja, tudo que estiver dentro do diretório restricted será redirecionado para o LoginFilter que tomará alguma decisão ou simplesmente mandará prosseguir com a requisição. Este é o conceito chave, então entenda que se você acessar /restricted/paginabbbb.xhtml automaticamente você será enviado para o LoginFilter, claro que de forma imperceptível.

Então temos finalmente nosso LoginFilter, conforme o código da Listagem 8.


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.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class LoginFilter implements Filter {

 public void destroy() {
           // TODO Auto-generated method stub

 }

 public void doFilter(ServletRequest request, ServletResponse response,
                    FilterChain chain) throws IOException, ServletException {
   User user = null;
   HttpSession sess = ((HttpServletRequest) request).getSession(false);
   
   if (sess != null){
         user = (User) sess.getAttribute("usuarioLogado");
   }      

     if (user == null) {
              String contextPath = ((HttpServletRequest) request)
                                 .getContextPath();
              ((HttpServletResponse) response).sendRedirect(contextPath
                                 + "/security/form_login.xhtml");
     } else {
              chain.doFilter(request, response);
     }

 }

 public void init(FilterConfig arg0) throws ServletException {
           // TODO Auto-generated method stub

 }

}
Listagem 8. LoginFilter

Fizemos questão de mostrar toda a classe LoginFilter para que você possa perceber a sua totalidade. Veja que a única função desta classe (neste exemplo simples), mandar o usuário para a página de login.xhtml ou mandar ele prosseguir com a requisição através do chain.doFilter.

Mas ainda falta definirmos o nosso Session, que é tão importante quando o Filter, já que é ele quem vai garantir que as informações fiquem até o fim da Sessão. Veja o código da Listagem 9.

Para quem não sabe, uma Sessão ou Session (não só no Java, mas no contexto WEB como um todo) é um “container “ capaz de guardar informações que duram durante um determinado tempo ou até o usuário fechar o navegador, semelhante ao cookie. Em JSF, utilizaremos o recurso do HttpSession para gravar os dados do usuário logado por 60 minutos, ou seja, se ele ficar inativo por 60 minutos, automaticamente sua sessão é encerrada e ele terá que fazer o login novamente, mas se ele fechar o navegador sua sessão também é encerrada.


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 User getUsuarioLogado(){
         return (User) getAttribute("usuarioLogado");
    }
    
    public void setUsuarioLogado(User usuario){
         setAttribute("usuarioLogado", usuario);
    }
    
    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);
    }
    
}
Listagem 9. Criando classe Session

Nossa classe SessionContext nada mais é do que um Wrapper para recuperar a Sessão através do FacesContext, perceba que tudo gira em todo do método currentExternalContext.

Vejamos agora no web.xml como definir o tempo que a sessão deve ficar viva, sem ser encerrada. Observe o código da Listagem 10.


<session-config>
     <session-timeout>60</session-timeout>
</session-config>
Listagem 10. Configuração do tempo de inatividade da sessão

MD5 em Detalhes

Como você está trabalhando com senhas criptografadas em MD5, não há a possibilidade de recuperar uma senha perdida, ou seja, aquela senha que o usuário por algum motivo esqueceu.

A única forma de acessar o sistema novamente, é gerando uma nova senha para este usuário. Então sugerimos o método presente na Listagem 11, mas fique a vontade para adicionar a complexidade que achar necessária ao mesmo.


public String gerarNovaSenha() {
     String[] carct = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
                   "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l",
                   "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x",
                   "y", "z", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
                   "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
                   "W", "X", "Y", "Z" };

     String senha = "";

     for (int x = 0; x < 10; x++) {
            int j = (int) (Math.random() * carct.length);
            senha += carct[j];

     }

     return senha;
}
Listagem 11. Método gerador de senhas

Você pode utilizar o método acima gerando uma nova senha para o usuário e enviado ao seu e-mail ou mesmo mostrando diretamente na tela, o que não é muito seguro.

Com essa aplicação é possível criar uma sistema de login poderoso e robusto, obviamente que realizando algumas modificações como por exemplo a adição de “Perfis de Usuário”.

Veja como torna-se simples controlar o que o usuário está fazendo com nosso LoginFilter, pois temos a URL para onde ele deseja ir, cabe a nós decidir se ele deve ou não continuar. Poderíamos até criar um log de todos os acessos em cada URL, na hora e minuto exato que ele acessou e muitos outros recursos.

Para finalizar, é importante salientar que existem outros métodos para implementação de um sistema de login, frameworks com o Spring Security ou o JAAS e etc. Mas um bom filter pode realizar tarefas tão robustas quanto, só depende do nível de complexidade adotado.