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 )
)
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;
}
}
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());
}
}
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:
- 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)”.
- 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;
}
}
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";
}
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>
Temos então quase todo mecanismo pronto:
- A página de login
- A comunicação do XHTML com o BO através do ManagedBean
- As tabelas do banco e o mapeamento via JPA no Java da nossa classe Usuario
- 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>
Acima estamos definindo duas coisas:
- 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.
- 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
}
}
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;
}
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.