Neste artigo veremos como construir um sistema de login com JSF 2.0 utilizando Filters. 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 JSF, o Filter é um recurso que possibilita o gerenciamento de todas as requisições HTTP do seu servidor, filtrando o endereço que está sendo acessado. Sendo assim, quando o usuário João acessar aquela URL que é proibida, você pode imediatamente redirecioná-lo para outro endereço, antes que a resposta seja dada ao cliente.

O MD5 ou Message-Digest Algorithm 5 é um algoritmo hash de 128bits unidirecional e pelo simples fato de ser unidirecional não há como decriptar 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 decriptar 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 o desenvolvedor deve ter conhecimento, por uma questão simples de ética profissional. Existem ainda outros algoritmos HASH para criptografar informações, mas não é nosso foco estudá-los.

Construindo sistema de login

Além do JSF, trabalharemos com outros frameworks para nos auxiliar no desenvolvimento desta aplicação, mas não é obrigatoriedade usá-los, pois você pode adaptar o mesmo para a sua realidade. Usaremos então o JPA/Hibernate, Spring Framework e o nosso banco de dados será o PostgreSQL, mas fique a vontade para escolher outro de sua preferência. Lembrando que não mostraremos configurações básicas de JPA ou mesmo Spring, já que estamos partindo do principio que o foco deste artigo é mostrar a construção de um Login com Filter.

Observe na Listagem 1 a criação da tabela usuario.


  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 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 (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 na 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, no 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, que se UsuarioMBImpl, e também mostraremos apenas os métodos importantes. Observe a Listagem 5.


//True se usuário está logado e false caso contrário
private boolean loggedIn;
 
//Armazena o usuário logado
private Usuario usuarioLogado;
 
//Email e senha digitado pelo usuário na página XHTML
private String email, senha;     
 
public String getEmail() {
return email;
}
 
public void setEmail(String email) {
       this.email = email;
}
 
public String getSenha() {
       return senha;
}
 
public void setSenha(String senha) {
       this.senha = senha;
}
 
//Realiza o login caso de tudo certo
public String doLogin(){
                    
       //Verifica se o e-mail e senha existem e se o usuario pode logar          
       Usuario usuarioFound = (Usuario) 
         usuarioBO.isUsuarioReadyToLogin(email, senha);
             
       //Caso não tenha retornado nenhum usuario, então mostramos um erro
       //e redirecionamos ele para a página login.xhtml              
      //para ele realiza-lo novamente          
       if (usuarioFound == null){
             addErrorMessage("Email ou Senha errado, tente novamente !");
             FacesContext.getCurrentInstance().validationFailed();
             return "/login/login.xhtml?faces-redirect=true";     
       }else{
         //caso tenha retornado um usuario, setamos a variável loggedIn      
         //como true e guardamos o usuario encontrado na variável 
         //usuarioLogado. Depois de tudo, mandamos o usuário 
         //para a página index.xhtml
         loggedIn = true;
         usuarioLogado = usuarioFound;
         return "/restricted/index.xhtml?faces-redirect=true";
       }
}
 
//Realiza o logout do usuário logado
public String doLogout(){
  
       //Setamos a variável usuarioLogado como nulo, ou seja, limpamos
      //os dados do usuário que estava logado e depois setamos a variável
      //loggedIn como false para sinalizar que o usuário não está mais           
      //logado
       usuarioLogado = null;
       loggedIn = false;
       //Mostramos um mensagem ao usuário e redirecionamos ele para a         
       //página de login
       addInfoMessage("Logout realizado com sucesso !");
       return "/login/login.xhtml?faces-redirect=true";
}
Listagem 5. ManagedBean para Login do Usuário

No código acima temos uma chamada ao nosso método “isUsuarioReadyToLogin()” que está no nosso BO criado anteriormente. Caso a instância da variável “usuarioFound” 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 algum usuário setamos a variável “loggedIn” como true, guardamos os dados do usuário logado na variável usuarioLogado e redirecionamos ele para o index.xhtml, ou seja, a página de bem vindo.

O método de logout é simples, apenas fazemos o inverso que fizemos no método de login, setando o loggedIn como false e o usuarioLogado como nulo.

Vamos ver agora nossa página XHTML de Login, conforme a 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:outputStylesheet library="css" name="login.css" />
</h:head>
<h:body>

     <h:form id="formLogin" enctype="multipart/form-data">
           <p:growl autoUpdate="true" id="messages" />

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

             <h:outputText value="Email: " />
             <p:inputText value="#{usuarioMB.email}" 
                styleClass="lowercase"
                 size="35" required="true"
               requiredMessage="O Email é obrigatório" />

             <h:outputText value="Senha: " />
             <p:password value="#{usuarioMB.senha}"
               size="35" required="true" 
                requiredMessage="A Senha é obrigatória" />

           </p:panelGrid>

           <p:panelGrid columns="2" styleClass="semBorda">
                  <p:commandButton icon="ui-icon-unlocked" 
                     value="Entrar"
                    action="#{usuarioMB.doLogin}" />
                  <p:commandButton icon="ui-icon-mail-closed" 
                    value="Recuperar Senha"
                    action="#{usuarioMB.doLogin}" />
           </p:panelGrid>

     </h:form>
</h:body>
</html>
Listagem 6. login.xhtml

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, então começaremos definindo o filter no arquivo web.xml, de acordo com 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 finalmente nosso LoginFilter será como o 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 br.com.meuprojeto.mb.UsuarioMBImpl;
 
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 {

               //Captura o ManagedBean chamado “usuarioMB”
               UsuarioMBImpl usuarioMB = (UsuarioMBImpl) 
               ((HttpServletRequest) request)
                 .getSession().getAttribute("usuarioMB");

               //Verifica se nosso ManagedBean ainda não 
               //foi instanciado ou caso a
               //variável loggedIn seja false, assim saberemos que  
               // o usuário não está logado
               if (usuarioMB == null || !usuarioMB.isLoggedIn()) {
                 String contextPath = ((HttpServletRequest) request)
                  .getContextPath();
                   //Redirecionamos o usuário imediatamente 
                   //para a página de login.xhtml
                 ((HttpServletResponse) response).sendRedirect
                  (contextPath + "/login/login.xhtml");
               } else {
                      //Caso ele esteja logado, apenas deixamos 
                      //que o fluxo continue
                    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”.

Recuperação de senha

Como bônus a este artigo, decidimos acrescentar mais um método muito útil para que você possa implementar a geração de novas senhas automaticamente. 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 da Listagem 9, 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 9. 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.