Capítulo 14

Autenticação

14.1 - Criando Usuários

No nosso sistema, atualmente, qualquer um pode adicionar, editar ou remover produtos. Será que isso é o desejável? Não seria melhor apenas habilitar essas funcionalidades para os administradores do sistema?

Então vamos criar um sistema de login para a nossa aplicação, começando pelo modelo de usuários:

package br.com.caelum.goodbuy.modelo;

public class Usuario {

  private String login;

  private String senha;

  private String nome;
  
  //getters e setters
}

É uma boa idéia guardar os usuários no banco de dados, então vamos adicionar as anotações do hibernate. Não há a necessidade de criar um campo id para usuários, pois o login já será um identificador único.

package br.com.caelum.goodbuy.modelo;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Usuario {

  @Id
  private String login;

  private String senha;

  private String nome;
  
  //getters e setters
}

Como estamos adicionando uma entidade nova, precisamos colocá-la no hibernate.cfg.xml. Ainda vamos adicionar a propriedade hibernate.hbm2ddl.auto com o valor update, assim o hibernate fará as mudanças nas tabelas do banco quando necessário.

<hibernate-configuration>
  <session-factory>
    <!--...-->    
    <property name="hibernate.hbm2ddl.auto">update</property>
    
    <mapping class="br.com.caelum.goodbuy.modelo.Produto" />
    <mapping class="br.com.caelum.goodbuy.modelo.Usuario" />
  </session-factory>
</hibernate-configuration>

Com o modelo pronto já é possível criar as lógicas de cadastro e de login de usuários. Vamos começar pelo cadastro, criando o controlador de usuários com uma lógica para mostrar o formulário:

package br.com.caelum.goodbuy.controller;

import br.com.caelum.vraptor.Resource;

@Resource
public class UsuariosController {

  public void novo() {

  }

}

E então o jsp com o formulário, em /WEB-INF/jsp/usuarios/novo.jsp, com todos os campos obrigatórios:

<form id="usuariosForm" action="<c:url value="/usuarios"/>" 
  method="POST">
  <fieldset>
    <legend>Criar novo usuário</legend>
        
    <label for="nome">Nome:</label>
      <input id="nome" class="required" 
        type="text" name="usuario.nome" value="${usuario.nome }"/>

    <label for="login">Login:</label>
      <input id="login" class="required"  
        type="text" name="usuario.login" value="${usuario.login }"/>

    <label for="senha">Senha:</label>
      <input id="senha" class="required" type="password" 
      name="usuario.senha"/>

    <label for="confirmacao">Confirme a senha:</label>
      <input id="confirmacao" equalTo="#senha" type="password"/>

    <button type="submit">Enviar</button>
  </fieldset>
</form>

<script type="text/javascript">
  $('#usuariosForm').validate();
</script>

Precisamos também criar um link para esse cadastro. Para isso criaremos uma div no cabeçalho que mostrará as informações do usuário. Abra o arquivo header.jspf e modifique a div header:

<div id="header">
  <div id="usuario">
    Você não está logado. 
    <a href="<c:url value="/usuarios/novo"/>">
      Cadastre-se
    </a>
  </div>
  ...
</div>

Para completar o cadastro, vamos criar a lógica que adiciona o usuário de fato, validando se o login escolhido ainda não existe no sistema:

@Resource
public class UsuariosController {

  private final UsuarioDao dao;
  private final Result result;
  private final Validator validator;

  public UsuariosController(UsuarioDao dao, Result result, 
        Validator validator) {
    this.dao = dao;
    this.result = result;
    this.validator = validator;
  }
  @Post("/usuarios")
  public void adiciona(Usuario usuario) {
    if (dao.existeUsuario(usuario)) {
      validator.add(new ValidationMessage("Login já existe", 
          "usuario.login"));
    }
    validator.onErrorUsePageOf(UsuariosController.class).novo();

    dao.adiciona(usuario);

    result.redirectTo(ProdutosController.class).lista();
  }
  //...
}

O UsuarioDao ainda não existe. Use o Ctrl+1 para criar o dao e os seus métodos:

@Component
public class UsuarioDao {

  private final Session session;

  public UsuarioDao(Session session) {
    this.session = session;
  }
  public boolean existeUsuario(Usuario usuario) {
    Usuario encontrado = (Usuario) session.createCriteria(Usuario.class)
      .add(Restrictions.eq("login", usuario.getLogin()))
      .uniqueResult();
    return encontrado != null;
  }

  public void adiciona(Usuario usuario) {
    Transaction tx = this.session.beginTransaction();
    this.session.save(usuario);
    tx.commit();
  }

}

14.2 - Efetuando o login

Criamos o cadastro, mas o usuário ainda não consegue fazer o login. Primeiro vamos criar um link para acessar o formulário de login, no cabeçalho:

<div id="header">
  <div id="usuario">
    Você não está logado. <a href="<c:url value="/login"/>">Login</a> 
    <a href="<c:url value="/usuarios/novo"/>">Cadastre-se</a>
  </div>

e a respectiva lógica e formulário:

@Resource
public class UsuariosController {
  //...
  @Get("/login")
  public void loginForm() {
    
  }
}
<form action="<c:url value="/login"/>" method="POST">
  <fieldset>
    <legend>Efetue o login</legend>
        
    <label for="login">Login:</label>
      <input id="login" type="text" name="usuario.login"/>

    <label for="senha">Senha:</label>
      <input id="senha" type="password" name="usuario.senha"/>

    <button type="submit">Login</button>
  </fieldset>
</form>

Quando o usuário faz o login, precisamos guardar a informação de que ele já está logado. A melhor forma é guardar a informação de login na sessão, que mantém os dados enquanto o usuário estiver navegando pela aplicação. Para isso, vamos criar uma classe que guarda o usuário logado. Essa classe será acessada nos jsps para acessar as informações do usuário, então adicionamos alguns getters para expor as informações relevantes.

@Component
@SessionScoped
public class UsuarioWeb {

  private Usuario logado;

  public void login(Usuario usuario) {
    this.logado = usuario;
  }
  
  public String getNome() {
    return logado.getNome();
  }
  
  public boolean isLogado() {
    return logado != null;
  }
}

Então na nossa lógica de login, podemos colocar o usuário logado dentro da classe acima, depois de verificar que o usuário digitou o login e senha certos.

@Resource
public class UsuariosController {

  private final UsuarioWeb usuarioWeb;
  //...
  public UsuariosController(UsuarioDao dao, Result result, 
      Validator validator, UsuarioWeb usuarioWeb) {
    //...
    this.usuarioWeb = usuarioWeb;
  }

  @Post("/login")
  public void login(Usuario usuario) {
    Usuario carregado = dao.carrega(usuario);
    if (carregado == null) {
      validator.add(
          new ValidationMessage("Login e/ou senha inválidos",
              "usuario.login"));
    }
    validator.onErrorUsePageOf(UsuariosController.class)
        .loginForm();

    usuarioWeb.login(carregado);

    result.redirectTo(ProdutosController.class).lista();
  }
}

Para carregar o usuário, crie o método no UsuarioDao que busca um usuário por login e senha:

@Component
public class UsuarioDao {

  //...
  
  public Usuario carrega(Usuario usuario) {
    return (Usuario) session.createCriteria(Usuario.class)
      .add(Restrictions.eq("login", usuario.getLogin()))
      .add(Restrictions.eq("senha", usuario.getSenha()))
      .uniqueResult();
  }

}

E para mostrar que o usuário está logado mesmo, vamos modificar o cabeçalho:

<div id="header">
  <div id="usuario">
    <c:if test="${usuarioWeb.logado}">
      Olá, ${usuarioWeb.nome }! 
      <a href="<c:url value="/logout"/>">Logout</a>
    </c:if>
    <c:if test="${empty usuarioWeb or not usuarioWeb.logado}">
      Você não está logado. 
      <a href="<c:url value="/login"/>">Login</a> 
      <a href="<c:url value="/usuarios/novo"/>">Cadastre-se</a>
    </c:if>
  </div>
  ...
</div>

Por último vamos adicionar a lógica de logout:

public class UsuariosController {
  //...
  @Path("/logout")
  public void logout() {
    usuarioWeb.logout();
    result.redirectTo(ProdutosController.class).lista();
  }
}
public class UsuarioWeb {
  //...
  
  public void logout() {
    this.logado = null;
  }
}

Nova editora Casa do Código com livros de uma forma diferente

Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não conhecem programação para revisar os livros tecnicamente a fundo. Não têm anos de experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e obsessão por livros de qualidade a preços justos.

Casa do Código, ebook com preço de ebook.

14.3 - Restringindo funcionalidades para usuários logados

Como falamos no começo do capítulo, não é legal que todo mundo consiga adicionar, remover e editar produtos. Vamos, então, fazer com que só usuários logados consigam acessar essas funcionalidades. Um primeiro passo é retirar os links para elas quando o usuário não está logado:

/header.jspf:

<div id="menu">
  <ul>
    <c:if test="${usuarioWeb.logado }">
      <li><a href="<c:url value="/produtos/novo"/>">
        Novo Produto
      </a></li>
    </c:if>
 ...

/WEB-INF/jsp/produto/lista.jsp

<table>
  ...
  <tbody>
    <c:forEach items="${produtoList}" var="produto">
      <tr>
        ...
        <c:if test="${usuarioWeb.logado }">
          <td>
            <a href=
              "<c:url value="/produtos/${produto.id}"/>">
              Editar
            </a>
          </td>
          <td>
            <form action=
              "<c:url value="/produtos/${produto.id}"/>"
              method="POST">
              <button class="link" name="_method" 
                value="DELETE">
                Remover
              </button>
            </form>
          </td>
        </c:if>
      </tr>          
    </c:forEach>
  </tbody>
</table>

Agora os usuários que não estão logados não conseguem mais ver os links das ações que ele não pode executar. Mas será que isso é o suficiente? O que impede o usuário de digitar na barra de endereços do browser: http://localhost:8080/goodbuy/produtos/novo? Nada! Ele só precisa conhecer qual é a URI. Precisamos, de algum jeito, impedir que os usuários acessem certas URIs se eles não estiverem logados, monitorar todas as ações do usuário para que quando ele acessar uma URI proibida, redirecionar para uma página de erro, ou melhor, para o login.

14.4 - Interceptor

Um Interceptor no VRaptor é como se fosse um Servlet Filter: ele pode interceptar requisições, executando algo antes e/ou depois da sua lógica. Mais ainda, ele pode impedir que sua lógica seja executada, redirecionando a requisição para outro lugar. Isso é exatamente o que a gente queria: verificar se o usuário está logado antes de ir pro controller, e se ele não estiver, redirecionar para o login.

Para implementar um Interceptor do VRaptor precisamos de duas coisas: anotar a classe com @Intercepts e implementar a interface Interceptor:

@Intercepts
public class AutenticacaoInterceptor implements Interceptor {
  ...
}

Essa interface tem dois métodos:

Como qualquer classe registrada no VRaptor, você pode receber qualquer componente da sua aplicação (e do VRaptor) pelo construtor do seu interceptor. No nosso caso precisamos verificar se o usuário está logado, e essa informação está no UsuarioWeb.

@Intercepts
public class AutorizacaoInterceptor implements Interceptor {

  private final UsuarioWeb usuario;

  public AutorizacaoInterceptor(UsuarioWeb usuario) {
    this.usuario = usuario;
  }

}

Mas só precisamos executar a lógica de autenticação caso o usuário não esteja logado, então vamos implementar o método accepts:

public boolean accepts(ResourceMethod method) {
  return !this.usuario.isLogado();
}

Para o método intercepts, sabemos já que o usuário não está logado, então vamos redirecionar para a lógica de login. Para isso precisamos do Result.

@Intercepts
public class AutorizacaoInterceptor implements Interceptor {

  private final UsuarioWeb usuario;
  private final Result result;

  public AutorizacaoInterceptor(UsuarioWeb usuario, Result result) {
    this.usuario = usuario;
    this.result = result;
  }

  public boolean accepts(ResourceMethod method) {
    return false;
  }

  public void intercept(InterceptorStack stack, ResourceMethod method, 
      Object resourceInstance) throws InterceptionException {
    result.redirectTo(UsuariosController.class).loginForm();
  }
}

Não é o caso, mas se quiséssemos continuar a requisição normalmente, poderíamos fazer:

stack.next(method, resourceInstance);

Mas temos um pequeno problema: se o usuário não estiver logado ele não vai conseguir acessar nada no sistema, nem o login! Na verdade, só queremos proibir que o usuário adicione e modifique produtos, então nosso interceptor só pode executar para essas operações.

Poderíamos até colocar no Interceptor uma lista dos métodos que serão interceptados, mas assim toda vez que adicionarmos uma operação nova que precise de autenticação precisaríamos mudar o interceptor.

Um jeito mais legal de fazer isso é marcar os métodos que precisam de autenticação. Para isso podemos criar uma anotação para ser colocada nos métodos:

//a anotação vai ficar disponível em tempo de execucao
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD) // anotação para métodos
public @interface Restrito {

}

Assim, podemos anotar os métodos restritos (adiciona, atualiza, remove, formulario e edita do ProdutosController):

@Resource
public class ProdutosController {


  @Restrito
  public void formulario() {}

  @Restrito
  public Produto edita(Long id) {...}
  
  @Restrito
  public void altera(Produto produto) {...}

  @Restrito
  public void adiciona(final Produto produto) {...}

  @Restrito
  public void remove(Long id) {...}

  ...
}

E do lado do Interceptor, apenas fazemos:

public boolean accepts(ResourceMethod method) {
  return !usuario.isLogado() && method.containsAnnotation(Restrito.class);
}

Pronto. Nosso sistema de autenticação está pronto. Poderíamos trocar a anotação @Restrito por uma, por exemplo, @Liberado caso tenha bem mais operações liberadas do que restritas, depende do seu sistema.