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.

Artigos relacionados