Como criar um CMS completo com CakePHP – Parte 5 – Criando o sistema de login com bcrypt e lembrar de mim (primeira etapa) Artigo

Conheça os cursos gratuitos do WebDevBr! - Inscreva-se!


Este artigo foi publicado a 4 anos, 11 meses, 2 semanas, 1 dia atrás.

Nesta nossa 5ª aula de como criar um CMS completo com CakePHP vou mostrar como criar um sistema de Login com Senhas em bcrypt e implementar o famoso Lembrar de mim, que muitos sabem, faz com que você não precise mais acessar com usuário e senha a área protegida da aplicação (esta é a primeira parte da 5ª aula).

O Model

Lembra que na aula 03 nós baixamos um Model chamado Usuario, bem agora vamos usá-lo.

[Se você ainda não baixou o Model já configurado, clique aqui.][http://blog.erikfigueiredo.com.br/wp-content/uploads/2013/07/Usuario.zip]

Gostou deste artigo?

Receba atualizações semanais com novos artigos do WebDevBr e outras dicas!

A estrutura básica de um Model diz que ele deve se parecer com:

class Usuario extends AppModel
{
    public $name = 'Usuario';

}

A variável $name é opcional neste caso, já que ela está declarando algo que já acontece naturalmente, mas como eu estou acostumado a colocar assim, vou manter.

Validação dos dados

Abaixo da variável $name temos a $validate que é um recurso nativo do CakePHP, quando ela é declarada vai ativar a validação de dados, quando não usamos um framework a validação é realmente muito dispendioso e costuma tomar muito tempo e várias linhas de expressões regulares, ifs, elses e muita, mas muita lógica mesmo. Aqui no CakePHP é só criar um array e pronto.

Por razões de organização eu sempre mantenho minhas regras de validação dentro de grupos nomeados, vou explicar:

Temos a situação:

public $validate = array(
  'nome' => array(
    'required' => array(
      'rule' => array('notEmpty'),
      'message' => 'O nome não pode ficar em branco!'
    ),
    'unico' => array(
      'rule' => array('minLength',3),
      'message' => 'Nome muito curto!'
    )
  ),

  'username' => array(
    'required' => array(
      'rule' => array('notEmpty'),
      'message' => 'O usuário não pode ficar em branco!'
    ),
    'unico' => array(
      'rule' => array('isUnique'),
      'message' => 'Este usuário já tem dono, tente outro!'
    )
  ),

  'email' => array(
    'required' => array(
      'rule' => array('notEmpty'),
      'message' => 'O email não pode ficar em branco!'
    ),
    'unico' => array(
      'rule' => array('isUnique'),
      'message' => 'Este email já está sendo usado!'
    )
  ),

  'password' => array(
    'required' => array(
      'rule' => array('notEmpty'),
      'message' => 'A senha é obrigatória!'
    ),
    'minimo' => array(
      'rule' => array('minLength',6),
      'message' => 'A senha é muito curta, o mínimo são 6 caracteres!'
    )
  ),

  'confirma' => array(
    'required' => array(
      'rule' => array('passwordconfirm','password'),
      'message' => 'A senha de confirmação é diferente da sua senha escolhida!'
    )
  )
);

Ok, a variável $validate é um array multidimensional. O primeiro nivel é o nome do campo no banco de dados e o segundo o nome que dei ao grupo de configurações de validação, esta é a forma mais prática de organizar as suas regras de validação, facilita a manutenção e também as configurações posteriores, além de ser organizado e bonito de se ver.

Cada grupo de validação tem dois parâmetros de configuração, o "rule" e o "message", o rule define qual é a regra para aquele campo, e o message a mensagem que vai aparecer caso ele não esteja de acordo, por exemplo, no campo "nome", grupo "required", temos o rule notEmpty que diz que aquele campo não deve ficar em branco, agora se estiver a mensagem "O nome não pode ficar em branco" será exibida.

Note também que usei o nome "unico" para uma regra de minimo de caracteres, fiz isso propositalmente, para mostrar que estes nomes são apenas para ajudar, eu poderia fazer assim que ia funcionar:

public $validate = array(
  'nome' => array(
    'Erik' => array(
      'rule' => array('notEmpty'), 'message' => 'O nome não pode ficar em branco!'
    ),
    'Fernanda' => array(
      'rule' => array('minLength',3), 'message' => 'Nome muito curto!'
    )
  ),

  'username' => array(
    'Maria' => array(
      'rule' => array('notEmpty'), 'message' => 'O usuário não pode ficar em branco!'
    ),
    'Joaquim' => array(
      'rule' => array('isUnique'), 'message' => 'Este usuário já tem dono, tente outro!'
    )
  ),

  'email' => array(
    'Jose' => array(
      'rule' => array('notEmpty'), 'message' => 'O email não pode ficar em branco!'
    ),
    'Joao' => array(
      'rule' => array('isUnique'), 'message' => 'Este email já está sendo usado!'
    )
  ),

  'password' => array(
    'FulanodeTal' => array(
      'rule' => array('notEmpty'), 'message' => 'A senha é obrigatória!'
    ),
    'SeiLa' => array(
      'rule' => array('minLength',6), 'message' => 'A senha é muito curta, o mínimo são 6 caracteres!'
    )
  ),

  'confirma' => array(
    'NomeQualquer' => array(
      'rule' => array('passwordconfirm','password'), 'message' => 'A senha de confirmação é diferente da sua senha escolhida!'
    )
  )
);

Mas aconselho você a colocar nomes que facilitem na hora de realizar alguma alteração, para o grupo unico do campo nome eu devia ter colocado "minimo", e o mesmo para diversos outros grupos!

Para saber quais rules você pode usar, veja na documentação do CakePHP: http://book.cakephp.org/2.0/en/models/data-validation.html#core-validation-rules

Outra coisa legal é o campo "confirma", note que a regra de validação "passwordconfirm" não existe na documentação do Cake, quando isso acontece, o CakePHP faz referência a uma função disponível no seu Model, neste caso, em baixo da validação você encontra:

//Note os dois parâmetros passados na função:
//$data são os dados do campo da validação que no nosso caso é o 'confirma' 
//$controlField é o valor enviado na hora que criamos a validação, lembra que era
//assim: array('passwordconfirm','password'), então o $controlField é o 'password'
public function passwordconfirm($data, $controlField) {
    //Se o campo não for enviar  
    if (!isset($this->data[$this->alias][$controlField])) {
        //Gera um erro e retorna falso  
        trigger_error('O campo de comparação de senha não foi enviado');
        return false;
    }

    //Se o campo existir o bloco anterior é ignorado e continua  
    //O current passa o valor do ponteiro interno de um array,  
    //veja mais na documentação do PHP, no nosso caso é o valor  
    //do campo 'confirma' (de um debug($data) pra entender melhor)
    $field = key($data);
    $password = current($data);

    //e finalmente pegamos o valor do campo enviado pelo validate
    //o $this->data é o mesmo que $this->request->data, só que no Model.  
    $controlPassword = $this->data[$this->alias][$controlField];

    //Se a senha do confirma for diferente do campo que quero verificar (o password) 
    if ($password !== $controlPassword) {
        //Envia a mensagem de erro de validação  
        $this->invalidate($controlField, 'Repita a senha!');
        //retorna false pra avisar que ta errado  
        return false;
    }
    //se estiver correto, retorna verdadeiro e valida o campo
    return true;
}

O que essa função faz? Bem quando chamada pelo validate, ela deve retornar false caso a validação não combine ou true caso combine, a função está bem comentada, então, qualquer dúvida, poste nos comentários no fim da página.

Salvando os dados

Falei no começo do artigo que iria fazer um sistema de login bcrypt, pois bem, aqui no nosso Model só tem um cuidado a se tomar, fazer nosso campo senha ser cadastrado da forma correta, já que o CakePHP não gera o hash do bcrypt, ele só compara, temos que fazer nós mesmos.

Vá até o fim do nosso model, na função beforeSave, logo abaixo da que vimos anteriormente, vou explicar o que estamos fazendo.

O beforeSave é chamado antes que um salvamento seja feito, e nela podemos tratar os dados da maneira que precisarmos, uma atenção a se tomar é que a beforeSave sempre deve retornar true, se retornar false não vai salvar (quando você quer impedir que os dados sejam salvos, é isso que deve fazer, colocar um return false no beforeSave).

Na primeira linha da função eu coloquei um isset() que vai checar se o campo password foi enviado, isso vai evitar erros no nosso código, sendo assim, toda vez que o nosso campo estiver presente, eu uso o utilitário Security (que instanciei na primeira linha do model com app::uses) para gerar o hash da senha e substituo o valor enviado pelo formulário, com isso já temos a nossa senha salva corretamente.

O Controller

Neste artigo vamos apenas fazer o CRUD do Controller, vou deixar o resto pro próximo artigo porque vai estender muito.

Em primeiro lugar crie o arquivo UsuariosController.php em app/Controllers/ e dentro:

class UsuariosController extends AppController
{

}

Dentro dele vamos criar as actions (funções) do nosso Controller, vou postar tudo aqui e vou comentando em baixo:

public function admin_index(){
  $retorno = $this->Usuario->find('all');
  $this->set('retorno',$retorno);
}

public function admin_add($id=null) {
  if ($id!=null) {
    $this->Usuario->id=$id;
    if (!$this->Usuario->exists())
      throw new NotFoundException(__('Usuário inexistente'));
  }

  if ($this->request->is('post') || $this->request->is('put')) {

    if ($this->request->data['Usuario']['password']=='')
      unset($this->request->data['Usuario']['password']);

    if ($id==null)
      $this->Usuario->create();

    if ($this->Usuario->save($this->request->data)) {
      $this->Session->setFlash(__('Usuário criado com sucesso!'),'sucesso');
      $this->redirect('/admin/');
    } else {
      $this->Session->setFlash(__('Alguma coisa está errada, verifique abaixo!'),'erro');
    }
  }
  else if($id!=null) {
    $user=$this->Usuario->read();
    unset($user['Usuario']['password']);
    $this->request->data=$user;
  }
}

public function admin_remove($id=null){
  $this->Usuario->id = $id;

  if(!$this->Usuario->exists())
    throw new NotFoundException('Usuario inexistente');

  if($this->Usuario->id==1){
    $this->Session->setFlash(__('Você não pode apagar este usuário!'),'erro');
    return $this->redirect(array('action'=>'index'));
  }

  if($this->Usuario->delete()):
    $this->Session->setFlash(__('Usuario removido com sucesso!'),'sucesso');
    return $this->redirect(array('action'=>'index'));

  endif;

  $this->Session->setFlash(__('Usuario não pode ser removido!'),'erro');

  return $this->redirect(array('action'=>'index'));
}

Bem, temos três actions para o nosso CRUD, que na verdade fazem 4 coisas, então 4 coisas - 3 actions = ... opa.. ta faltando alguma coisa.

Calma vou explicar um por um e você vai entender.

A primeira action, a index é bem simples, apenas da um find("all") para pegar todos os dados do banco e com o $this->set envia para a view, já a action remove (a última) toma o cuidado de impedir que o usuário de id 1 (o que cadastramos no Instalador, passo 3 da série de artigos) seja apagado, ninguém pode apagá-lo, nem ele mesmo, então impedi isso ali, na sequência  ele apaga tenta apagar o usuário e envia para a action index setando uma mensagem avisando se deu certo ou não. Note que eu devia ter feito a verificação do usuário 1 no validate do Model, mas estou colocando aqui para que vocês vejam que o CakePHP ainda é PHP, e vocês podem improvisar naquele momento em que você precisa entregar o job com urgência e não consegue usar o recurso que deveria. Da forma que fiz não traz nenhum incomodo, na prática.

Cadastro e edição com uma action só

Este método de cadastro eu só vou usar aqui, pra não ficar estendendo os próximos artigos, ok, nos outros Controllers vou fazer separado, é só pra vocês terem uma alternativa a mais no seu arsenal de conhecimento.

Na action add eu tenho 3 seções principais, então vou explicar por partes:

Primeiro verifico se o $id foi enviado, se foi verifico se o usuário existe, isso é básico, se não existir ele da um erro 404 e se existir ele ignora e continua, porque verificar se o $id foi passado, porque se ele não foi passado o CakePHP vai criar um usuário novo, quer ver?

Depois verifico se o formulário foi enviado, se foi ele vai verificar se a senha foi passada e se estiver em branco ele remove o valor todo do array, assim evita a validação. Em seguida ele verifica novamente se existe um $id e se não existir, da um create, para criar um novo cadastro (na verdade o create só zera qualquer id que não tenha sido passado). e Por fim salvamos, se salvar redireciona para a index, se não informa o que está errado.

Novamente fazemos uma ultima verificação para saber se o id foi passado, e se foi ele da um read() e traz os dados do usuário, removendo a senha na sequencia (já que além de ser um grave erro de segurança exibi-la, você vai apenas conseguir o hash bcrypt que ao ser salvo vai alterar a senha para ele mesmo, e isso companheiros, não valida o login).

Gente.. esse artigo já está com quase 2.000 palavras e vai ficar enorme, vou ter que continuar no próximo aonde vou falar sobre o bcrypt, o  recurso lembrar de mim e também as views.

Então este foi 5º artigo (parte 1) de 9 que ensina como construir um CMS completo com CakePHP.


Cursos relacionados


* Parcelamento apenas cartão de crédito! Pode haver uma pequena variação no parcelamento em relação a simulações apresentadas!