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.
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 )
)
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;
}
}
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());
}
}
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 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;
}
}
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;
}
}
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>
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:
- 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 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>
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 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
}
}
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);
}
}
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>
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;
}
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.
Confira outros conteúdos:
<Perguntas frequentes>
Nossos casos de sucesso
Eu sabia pouquíssimas coisas de programação antes de começar a estudar com vocês, fui me especializando em várias áreas e ferramentas que tinham na plataforma, e com essa bagagem consegui um estágio logo no início do meu primeiro período na faculdade.
Estudo aqui na Dev desde o meio do ano passado!
Nesse período a Dev me ajudou a crescer muito aqui no trampo.
Fui o primeiro desenvolvedor contratado pela minha
empresa. Hoje eu lidero um time de desenvolvimento!
Minha meta é continuar estudando e praticando para ser um
Full-Stack Dev!
Economizei 3 meses para assinar a plataforma e sendo sincero valeu muito a pena, pois a plataforma é bem intuitiva e muuuuito didática a metodologia de ensino. Sinto que estou EVOLUINDO a cada dia. Muito obrigado!
Nossa! Plataforma maravilhosa. To amando o curso de desenvolvimento front-end, tinha coisas que eu ainda não tinha visto. A didática é do jeito que qualquer pessoa consegue aprender. Sério, to apaixonado, adorando demais.
Adquiri o curso de vocês e logo percebi que são os melhores do Brasil. É um passo a passo incrível. Só não aprende quem não quer. Foi o melhor investimento da minha vida!
Foi um dos melhores investimentos que já fiz na vida e tenho aprendido bastante com a plataforma. Vocês estão fazendo parte da minha jornada nesse mundo da programação, irei assinar meu contrato como programador graças a plataforma.
Wanderson Oliveira
Comprei a assinatura tem uma semana, aprendi mais do que 4 meses estudando outros cursos. Exercícios práticos que não tem como não aprender, estão de parabéns!
Obrigado DevMedia, nunca presenciei uma plataforma de ensino tão presente na vida acadêmica de seus alunos, parabéns!
Eduardo Dorneles
Aprendi React na plataforma da DevMedia há cerca de 1 ano e meio... Hoje estou há 1 ano empregado trabalhando 100% com React!
Adauto Junior
Já fiz alguns cursos na área e nenhum é tão bom quanto o de vocês. Estou aprendendo muito, muito obrigado por existirem. Estão de parabéns... Espero um dia conseguir um emprego na área.
Utilizamos cookies para fornecer uma melhor experiência para nossos usuários, consulte nossa política de privacidade.