JSF Filter: Criando um sistema de login com criptografia MD5

Veja neste artigo como criar um sistema de login utilizando a criptografia MD5 e JSF 2.0 e Filter.

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.

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados