Solucionado (ver solução)
Solucionado
(ver solução)
15
respostas

Falha no login devido a criptografia

Ao salvar o usuário no banco é gerado um hash de senha, porém ao tentar logar é gerado um hash diferente para mesma a senha então não é possível efetuar o login. A solução provisória foi:

    public Result salvaNovoUsuario() {
        Form<Usuario> formulario = 
                formularios.form(Usuario.class).bindFromRequest();

        if (validadorDeUsuario.temErros(formulario)) {
            flash("danger", "Existem erros no preenchimento do 
                       cadastro");
            return 

              badRequest(formularioDeNovoUsuario.render(formulario));
        }

        Usuario usuario = formulario.get();
        String senha = usuario.getSenha();
        usuario.setSenha(senha);
        usuario.save();
        TokenDeCadastro token = new TokenDeCadastro(usuario);
        token.save();
        enviador.send(new EmailDeCadastro(token));
        flash("success", "Um email foi enviado para confirmar seu 
               cadastro!");
        return 
         redirect(routes.UsuarioController.formularioDeNovoUsuario());
    }

Mas com isso só foi possível acessar uma única vez já que a eu joguei a parte de criptografia para o final e volto ao mesmo problema. Estou procurando uma forma de guardar a senha do usuário salva no banco e reutiliza-la no login ou comparar as senhas descriptografadas.

15 respostas

Olá.

Nunca mexi com esse framework, mas você poderia postar aqui o conteúdo das funções getSenha() e setSenha(), por favor?

Essa é minha classe Usuário com todos os getters e setters

package models;

import javax.persistence.*;
import com.avaje.ebean.Model;

import play.data.validation.Constraints.Required;

@Entity
public class Usuario extends Model {

    @Id
    @GeneratedValue
    private long id;
    @Required(message = "Você precisa fornecer um nome.")
    private String nome;
    @Required(message = "Você precisa fornecer um email.")
    private String email;
    @Required(message = "Você precisa fornecer uma senha.")
    private String senha;
    private boolean verificado;

    public boolean isVerificado() {
        return verificado;
    }

    public void setVerificado(boolean verificado) {
        this.verificado = verificado;
    }


    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    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;
    }
}

Hmmm... você dá um getSenha() passando o valor para uma string e depois dá um setSenha() com essa string? Não parece fazer sentido. Enfim, quero entender onde está o código que gera o hash, que lib está usando pra isso.

O getSenha pega os dados passados no formulário, então eu dou um setSenha passando a senha e salvando no banco. Originalmente o código é assim:

String senhaCriptografada = BCrypt.hashpw(usuario.getSenha(), BCrypt.gensalt());
usuario.setSenha(senhaCriptografada);

O que eu fiz foi passar sem fazer o hash para quando cair no método fazLogin() a mesma senha que está no banco seja passada pelo usuário e só seja feita a criptografia após a autenticação, mas ai só da para se logar uma única vez.

E como, originalmente, você faz a verificação no login?

No vídeo o instrutor paga os dados do login e repete o procedimento acima usando sha1 e busca o usuário e a senha salvos no banco com o método comEmailESenha().

public Optional comEmailESenha(String email, String senha) { Usuario usuario = usuarios.where().eq("email", email).eq("senha", senha).findUnique(); return Optional.ofNullable(usuario); }

Eu fiz o mesmo mas como usei o JBCrypt ele nunca gera o mesmo hash.

Então, você usou como a verificação do BCrypt?

Ele tem o método checkpw para isso:

BCrypt.checkpw(password, passwordHash)

Você usou ele? Usando ele, você passa o password inserido pelo usuário no login como primeiro parâmetro e o que veio do banco como segundo parâmetro.

Eu tentei dessa forma seguindo o exemplo do site, a senha que fica salva e login ficam em métodos distintos e não consigo acessar a mesma variável para fazer a comparação.

O problema deve estar na sua controller. Faz o seguinte, me manda o código das suas classes UsuarioController.java e UsuarioDAO.java. Se possível, utilize a opção "Inserir Código" quando estiver comentando e separe em dois blocos o código das classes, assim fica mais legível. Tipo assim:

public Result doSomething() {
    ...
}
package controllers;

import java.util.Optional;
import org.mindrot.jbcrypt.BCrypt;
import com.google.inject.Inject;
import autenticadores.UsuarioAutenticado;
import daos.TokenDeCadastroDAO;
import daos.UsuarioDAO;
import models.EmailDeCadastro;
import models.TokenDeCadastro;
import models.Usuario;
import play.api.libs.mailer.MailerClient;
import play.data.DynamicForm;
import play.data.Form;
import play.data.FormFactory;
import play.mvc.*;
import play.mvc.Security.Authenticated;
import validadores.ValidadorDeUsuario;
import views.html.*;

public class UsuarioController extends Controller {

    public static final String AUTH = "AUTH";

    @Inject
    FormFactory formularios;
    @Inject
    private ValidadorDeUsuario validadorDeUsuario;
    @Inject
    private MailerClient enviador;
    @Inject
    private TokenDeCadastroDAO tokenDeCadastroDAO;
    @Inject
    private UsuarioDAO usuarioDAO;
    @Inject 
    private Usuario usuario;

    public Result formularioDeNovoUsuario() {
        play.data.Form<Usuario> formulario = formularios.form(Usuario.class);
        return ok(formularioDeNovoUsuario.render(formulario));
    }

    public Result salvaNovoUsuario() {
        Form<Usuario> formulario = formularios.form(Usuario.class).bindFromRequest();

        if (validadorDeUsuario.temErros(formulario)) {
            flash("danger", "Existem erros no preenchimento do cadastro");
            return badRequest(formularioDeNovoUsuario.render(formulario));
        }

        Usuario usuario = formulario.get();
        String senhaCriptografada = BCrypt.hashpw(usuario.getSenha(), BCrypt.gensalt());
        usuario.setSenha(senhaCriptografada);
        usuario.save();
        TokenDeCadastro token = new TokenDeCadastro(usuario);
        token.save();
        enviador.send(new EmailDeCadastro(token));
        flash("success", "Um email foi enviado para confirmar seu cadastro!");
        return redirect(routes.UsuarioController.formularioDeNovoUsuario());
    }

    @Authenticated(UsuarioAutenticado.class)
    public Result painel() {
        return ok(painel.render());
    }

    public Result formularioDeLogin() {
        return ok(formularioDeLogin.render(formularios.form()));
    }

    public Result fazLogin() {
        DynamicForm formulario = formularios.form().bindFromRequest();
        String email = formulario.get("email");
        String senha = BCrypt.hashpw(formulario.get("senha"), BCrypt.gensalt());

        Optional<Usuario> possivelUsuario = usuarioDAO.comEmailESenha(email, senha);

        if (possivelUsuario.isPresent()) {
            Usuario usuario = possivelUsuario.get();
            if (usuario.isVerificado()) {
                session(AUTH, usuario.getEmail());
                usuario.update();
                flash("success", "Login foi efetuado com sucesso!");
                return redirect(routes.UsuarioController.painel());
            }
            else {
                flash("warning", "Usuario ainda nao confirmado! Confirma seu email!");
            }
        }
        else {
            flash("danger", "Credenciais invalidas!");
        }
        return redirect(routes.UsuarioController.formularioDeLogin());
    }

    // Confirmacao de cadastro

    public Result confirmaCadastro(String email, String codigo) throws InterruptedException {
        Optional<TokenDeCadastro> possivelToken = tokenDeCadastroDAO.comCodigo(codigo);
        Optional<Usuario> possivelUsuario = usuarioDAO.comEmail(email);

        if (possivelToken.isPresent() && possivelUsuario.isPresent()) {
            TokenDeCadastro token = possivelToken.get();
            Usuario usuario = possivelUsuario.get();

            if (token.getUsuario().equals(usuario)) {
                token.delete();
                usuario.setVerificado(true);
                usuario.update();
                token.update();
                flash("success", "Usuario cadastrado com sucesso!");
                session(AUTH, usuario.getEmail());
                return redirect(routes.UsuarioController.painel());
            }
        }

        flash("danger", "Falha na tentativa de cadastro");
        return redirect(routes.UsuarioController.formularioDeNovoUsuario());

    }

    @Authenticated(UsuarioAutenticado.class)
    public Result fazLogout() {
        session().clear();
        flash("success", "Logout efetuado com sucesso!");
        return redirect(routes.UsuarioController.formularioDeLogin());
    }
}

Classe usuárioDAO

package daos;

import java.util.Optional;
import com.avaje.ebean.Model.Finder;
import models.Usuario;

public class UsuarioDAO {

    private Finder<Long, Usuario> usuarios = new Finder<>(Usuario.class);

    public Optional<Usuario> comEmail(String email) {
        Usuario usuario = usuarios.where().eq("email", email).findUnique();
        return Optional.ofNullable(usuario);
    }

    public Optional<Usuario> comEmailESenha(String email, String senha) {
        Usuario usuario = usuarios.where().eq("email", email).eq("senha", senha).findUnique();
        return Optional.ofNullable(usuario);
    }

}
solução!

Daniel, como eu tinha falado:

O código BCrypt.hashpw(usuario.getSenha(), BCrypt.gensalt()); vai gerar um hash diferente para cada execução pois o resultado do método BCrypt.gensalt() é diferente pra cada execução. Vamos a fundo:

O salt é usado pra que fique mais difícil de descobrir uma senha dentro de um banco, impedindo que um algoritmo de quebra de criptografia consiga fazer testes com as senhas mais comuns como password, 1234567890 e outras, descobrindo assim seu algoritmo de criptografia e tendo então acesso a qualquer usuário.

Porém, ele precisa ser o mesmo na hora de gerar para salvar o usuário e para fazer o login. Utilize uma constante para utilizar de salt nesse caso, e sua aplicação deve funcionar:

private static final String SALT = "2v738gh0s4981d26b49ob12gt378bg423od3h4o2";
String cryptoSenha = BCrypt.hashpw(usuario.getSenha(), SALT);

Como o Bruno disse, o método ideal para usar com o BCrypt seria com o BCrypt.checkpw(senha, cryptoSenha). Para fazer desse modo e ainda utilizar o gensalt(), você poderia alterar um pouco a lógica de login: busque o usuário somente pelo email e DEPOIS confira a senha:

public Result fazLogin() {
    DynamicForm formulario = formularios.form().bindFromRequest();
    String email = formulario.get("email");
    // GUARDA A SENHA NÃO CRIPTOGRAFADA
    String senha = formulario.get("senha");
    //BUSCA SÓ POR EMAIL
    Optional<Usuario> possivelUsuario = usuarioDAO.comEmail(email);
    if (possivelUsuario.isPresent()) {
        Usuario usuario = possivelUsuario.get();
        // VERIFICA SE A SENHA DO USUARIO BATE
        if (usuario.isVerificado() && BCrypt.checkpw(senha, usuario.getSenha())) {
            insereUsuarioNaSessao(usuario);
            flash("success", "Login foi efetuado com sucesso!");
            return redirect(routes.UsuarioController.painel());
        }
        else {
            flash("warning", "Usuário ainda não confirmado! Confirma seu email!");
        }
    }
    else {
        flash("danger", "Credenciais inválidas!");
    }
    return redirect(routes.UsuarioController.formularioDeLogin());
}

Os trechos alterados são as linhas logo abaixo dos comentários.

Nesse segundo jeito, provavelmente não é necessário mudar o tipo de criptografia usado ao cadastrar, pode salvar a senha criptografada direto no banco, tudo como você tinha feito originalmente.

Boa Marco.

Daniel, como o Marco já disse, você precisa utilizar o mesmo salt na senha digitada no login, para depois verificar com o hash do banco. Se utilizar o BCrypt.gensalt(), como você notou, terá outro hash e a senha sempre será errada.

O Marco deu a sugestão de colocar o salt como uma string. É uma opção, mas sempre será o mesmo salt para todos os usuários. Uma alternativa que eu acredito ser mais interessante é salvar o salt em uma nova coluna na tabela de usuários, na criação de usuário. Assim, ao fazer login, você teria algo tipo:

String cryptoSenhaForm = BCrypt.hashpw(formulario.get("senha"), usuario.getSalt());

Marco tentei a segunda solução e finalmente funcionou.

public Result fazLogin() {
        DynamicForm formulario = formularios.form().bindFromRequest();
        String email = formulario.get("email");
        String senha = formulario.get("senha");

        Optional<Usuario> possivelUsuario = usuarioDAO.comEmail(email);

        if (possivelUsuario.isPresent()) {
            Usuario usuario = possivelUsuario.get();
            if (usuario.isVerificado() && BCrypt.checkpw(senha, usuario.getSenha())) {
                session(AUTH, usuario.getEmail());
                flash("success", "Login foi efetuado com sucesso!");
                return redirect(routes.UsuarioController.painel());
            }
            else if(!usuario.isVerificado()){
                flash("warning", "Usuario ainda nao confirmado! Confirma seu email!");
            }
        }
        else {
            flash("danger", "Credenciais invalidas!");
        }
        return redirect(routes.UsuarioController.formularioDeLogin());
    }

Obrigado pela atenção e paciência nos últimos dias :)

Imagina, é pra isso que estamos aqui!

Último detalhe: Bruno, seu modo não tem vantagem em relação à solução que foi aceita pelo Daniel, pois de um jeito ou de outro será necessário carregar o usuário antes de conferir se a senha está correta, já que precisamos utilizar o algoritmo da biblioteca do BCrypt e este não está disponível no banco de dados.

Bons estudos aos dois!

Marco, precisaria usar sim, porém salvaria o salt utilizado na geração do hash pra senha de cada usuário, aí seria só usar o mesmo retornado do banco, sem necessidade de ter uma string final com o salt.

Contudo, a solução aceita foi sim muito mais interessante, uma vez que o BCrypt.checkpw() analisa senha passada plain text com a senha encriptada vinda do banco, não sendo necessário se preocupar com salt mais. Tinha deixado passar esse detalhe. Muito bom.

Quer mergulhar em tecnologia e aprendizagem?

Receba a newsletter que o nosso CEO escreve pessoalmente, com insights do mercado de trabalho, ciência e desenvolvimento de software