Continuando com o exemplo de Tutorial - Criando um Blog - Parte 1, imagine que queríamos garantir o acesso a certas URLs, com base no usuário logado. Temos também uma outra exigência: permitir que o nosso blog para tenha vários autores que podem criar, editar e excluir seus próprios artigos, e bloquear para que outros autores não façam alterações nos artigos que não lhes pertencem.
Primeiro, vamos criar uma nova tabela no banco de dados do blog para armazenar dados de nossos usuários:
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255),
password VARCHAR(255),
role VARCHAR(20),
created DATETIME DEFAULT NULL,
modified DATETIME DEFAULT NULL
);
Respeitado as convenções do CakePHP para nomear tabelas, mas também aproveitando
de outras convenção: Usando as colunas email
e password
da tabela de
usuários, CakePHP será capaz de configurar automaticamente a maioria das coisas
para nós, na implementação do login do usuário.
O próximo passo é criar a nossa classe UsersTable, responsável por encontrar, salvar e validar os dados do usuário:
// src/Model/Table/UsersTable.php
namespace App\Model\Table;
use Cake\ORM\Table;
use Cake\Validation\Validator;
class UsersTable extends Table
{
public function validationDefault(Validator $validator)
{
return $validator
->notEmpty('email', 'Email é necessário')
->email('email')
->notEmpty('password', 'Senha é necessária')
->notEmpty('role', 'Função é necessária')
->add('role', 'inList', [
'rule' => ['inList', ['admin', 'author']],
'message' => 'Por favor informe uma função válida'
]);
}
}
Vamos também criar o nosso UsersController. O conteúdo a seguir corresponde a
partes de uma classe UsersController básica gerado atráves do utilitário de
geração de código bake
fornecido com CakePHP:
// src/Controller/UsersController.php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event;
class UsersController extends AppController
{
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow('add');
}
public function index()
{
$this->set('users', $this->Users->find('all'));
}
public function view($id)
{
$user = $this->Users->get($id);
$this->set(compact('user'));
}
public function add()
{
$user = $this->Users->newEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->getData());
if ($this->Users->save($user)) {
$this->Flash->success(__('O usuário foi salvo.'));
return $this->redirect(['action' => 'add']);
}
$this->Flash->error(__('Não é possível adicionar o usuário.'));
}
$this->set('user', $user);
}
}
Da mesma maneira que criamos as views
para os nossos artigos usando
a ferramenta de geração de código, podemos implementar as views
do
usuário. Para o propósito deste tutorial, vamos mostrar apenas o add.php:
<!-- templates/Users/add.php -->
<div class="users form">
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Add User') ?></legend>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->input('role', [
'options' => ['admin' => 'Admin', 'author' => 'Author']
]) ?>
</fieldset>
<?= $this->Form->button(__('Submit')); ?>
<?= $this->Form->end() ?>
</div>
Agora estamos prontos para adicionar a nossa camada de autenticação. Em CakePHP
isso é tratado pelo Cake\Controller\Component\AuthComponent
, uma
classe responsável por exigir o login
para determinadas ações, a manipulação
de login
e logout
de usuário, e também permite as ações para que estão
autorizados.
Para adicionar este componente em sua aplicação abra o arquivos src/Controller/AppController.php e adicione as seguintes linha:
// src/Controller/AppController.php
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Event\Event;
class AppController extends Controller
{
//...
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginRedirect' => [
'controller' => 'Articles',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
}
public function beforeFilter(Event $event)
{
$this->Auth->allow(['index', 'view', 'display']);
}
//...
}
Não há muito para ser configurado, como usamos as convenções para a tabela
de usuários. Nós apenas configuramos as URLs que serão carregados após o
login
e logout
, estás ações são realizadas no nosso caso para os
/articles/
e /
respectivamente.
O que fizemos na função beforeFilter()
foi dizer ao AuthComponent
para
não exigir login
em todos index()
e view()
, em cada controlador.
Queremos que os nossos visitantes sejam capaz de ler e listar as entradas sem
registrar-se no site.
Agora, precisamos ser capaz de registrar novos usuários, salvar seu email
e password
, e mais importante, o hash da senha para que ele não seja
armazenado como texto simples no nosso banco de dados. Vamos dizer ao
AuthComponet
para permitir que usuários deslogados acessem a função add e
execute as ações de login
e logout
:
// src/Controller/UsersController.php
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
// Permitir aos usuários se registrarem e efetuar logout.
// Você não deve adicionar a ação de "login" a lista de permissões.
// Isto pode causar problemas com o funcionamento normal do AuthComponent.
$this->Auth->allow(['add', 'logout']);
}
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error(__('Usuário ou senha ínvalido, tente novamente'));
}
}
public function logout()
{
return $this->redirect($this->Auth->logout());
}
O hashing da senha ainda não está feito, precisamos de uma classe a fim de manipular sua geração. Crie o arquivo src/Model/Entity/User.php e adicione a seguinte trecho:
// src/Model/Entity/User.php
namespace App\Model\Entity;
use Cake\Auth\DefaultPasswordHasher;
use Cake\ORM\Entity;
class User extends Entity
{
// Gera conjunto de todos os campos exceto o com a chave primária.
protected array $_accessible = [
'*' => true,
'id' => false
];
// ...
protected function _setPassword($password)
{
if (strlen($password) > 0) {
return (new DefaultPasswordHasher)->hash($password);
}
}
// ...
}
Agora, a senha criptografada usando a classe DefaultPasswordHasher
.
Está faltando apenas o arquivo para exibição da tela de login.
Abra o arquivo templates/Users/login.php e adicione as seguintes linhas:
<!-- File: templates/Users/login.php -->
<div class="users form">
<?= $this->Flash->render('auth') ?>
<?= $this->Form->create() ?>
<fieldset>
<legend><?= __('Por favor informe seu usuário e senha') ?></legend>
<?= $this->Form->input('email') ?>
<?= $this->Form->input('password') ?>
</fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
Agora você pode registrar um novo usuário, acessando a URL /users/add
e faça login com o usuário recém-criado, indo para a URL /users/login
.
Além disso, tente acessar qualquer outro URL que não tenha sido explicitamente
permitido, como /articles/add
, você vai ver que o aplicativo redireciona
automaticamente para a página de login.
E é isso! Parece simples demais para ser verdade. Vamos voltar um pouco para
explicar o que aconteceu. A função beforeFilter()
está falando para o
AuthComponent não solicitar um login para a ação add()
em adição as ações
index()
e view()
que foram prontamente autorizadas na função
beforeFilter()
do AppController.
A ação login()
chama a função $this->Auth->identify()
da AuthComponent,
que funciona sem qualquer outra configuração porque estamos seguindo convenções,
como mencionado anteriormente. Ou seja, ter uma tabela de usuários com um
email
e uma coluna de password
, e usamos um form para postar os dados
do usuário para o controller. Esta função retorna se o login foi bem sucedido ou
não, e caso ela retorne sucesso, então nós redirecionamos o usuário para a URL
que configuramos quando adicionamos o AuthComponent em nossa aplicação.
O logout funciona quando acessamos a URL /users/logout
que irá redirecionar
o usuário para a url configurada em logoutUrl. Essa url é acionada quando a
função AuthComponent::logout()
.
Como afirmado anteriormente, nós estamos convertendo esse blog em uma ferramenta multi usuário de autoria, e para fazer isso, precisamos modificar a tabela de artigos um pouco para adicionar a referência à tabela de Usuários:
ALTER TABLE articles ADD COLUMN user_id INT(11);
Além disso, uma pequena mudança no ArticlesController é necessário para armazenar o usuário conectado no momento como uma referência para o artigo criado:
// src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEmptyEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Adicione esta linha
$article->user_id = $this->Auth->user('id');
// Você também pode fazer o seguinte
//$newData = ['user_id' => $this->Auth->user('id')];
//$article = $this->Articles->patchEntity($article, $newData);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Seu artigo foi salvo.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Não foi possível adicionar seu artigo.'));
}
$this->set('article', $article);
}
A função user()
fornecida pelo componente retorna qualquer coluna do usuário
logado no momento. Nós usamos esse metódo para adicionar a informação dentro de
request data para que ela seja salva.
Vamos garantir que nossa app evite que alguns autores editem ou apaguem posts de outros. Uma regra básica para nossa aplicação é que usuários admin possam acessar qualquer url, enquanto usuários normais (o papel author) podem somente acessar as actions permitidas. Abra novamente a classe AppController e adicione um pouco mais de opções para as configurações do Auth:
// src/Controller/AppController.php
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authorize' => ['Controller'], // Adicione está linha
'loginRedirect' => [
'controller' => 'Articles',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
}
public function isAuthorized($user)
{
// Admin pode acessar todas as actions
if (isset($user['role']) && $user['role'] === 'admin') {
return true;
}
// Bloqueia acesso por padrão
return false;
}
Acabamos de criar um mecanismo de autorização muito simples. Nesse caso os
usuários com papel admin
poderão acessar qualquer url no site quando
estiverem logados, mas o restante dos usuários (author) não podem acessar
qualquer coisa diferente dos usuários não logados.
Isso não é exatamente o que nós queremos, por isso precisamos corrigir nosso
metódo isAuthorized()
para fornecer mais regras. Mas ao invés de fazer
isso no AppController, vamos delegar a cada controller para suprir essas
regras extras. As regras que adicionaremos para o add
de ArticlesController
deve permitir ao autores criarem os posts mas evitar a edição de posts que não
sejam deles. Abra o arquivo src/Controller/ArticlesController.php e adicione
o seguinte conteúdo:
// src/Controller/ArticlesController.php
public function isAuthorized($user)
{
// Todos os usuários registrados podem adicionar artigos
if ($this->request->getParam('action') === 'add') {
return true;
}
// Apenas o proprietário do artigo pode editar e excluí
if (in_array($this->request->getParam('action'), ['edit', 'delete'])) {
$articleId = (int)$this->request->getParam('pass.0');
if ($this->Articles->isOwnedBy($articleId, $user['id'])) {
return true;
}
}
return parent::isAuthorized($user);
}
Estamos sobrescrevendo a chamada isAuthorized()``do AppController e
internamente verificando na classe pai se o usuário está autorizado. Caso não
esteja, então apenas permitem acessar a action ``add
, e condicionalmente
action edit
e delete
. Uma última coisa não foi implementada. Para dizer
ou não se o usuário está autorizado a editar o artigo, nós estamos chamando uma
função isOwnedBy()
na tabela artigos. Vamos, então, implementar essa
função:
// src/Model/Table/ArticlesTable.php
public function isOwnedBy($articleId, $userId)
{
return $this->exists(['id' => $articleId, 'user_id' => $userId]);
}
Isso conclui então nossa autorização simples e nosso tutorial de autorização. Para garantir o UsersController você pode seguir as mesmas técnicas que usamos para ArticlesController, você também pode ser mais criativo e codificar algumas coisas mais gerais no AppController para suas próprias regras baseadas em papéis.
Se precisar de mais controle, nós sugerimos que leia o guia completo do Auth AuthComponent seção onde você encontrará mais sobre a configuração do componente, criação de classes de Autorização customizadas, e muito mais.
Geração de código com bake Generating basic CRUD code
AuthComponent: User registration and login