认证和授权
按照我们的博客教程示例,假设我们希望根据登录的用户安全访问某些网址。 我们还有另一个要求:允许我们的博客拥有多个作者,他们可以创建,编辑和删除自己的文章,同时不允许其他作者对他们不拥有的文章进行更改。
创建所有与用户相关的代码
首先,让我们在我们的博客数据库中创建一个新表来保存用户的数据:
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(255),
role VARCHAR(20),
created DATETIME DEFAULT NULL,
modified DATETIME DEFAULT NULL
);
我们在命名表中遵守了CakePHP约定,但我们还利用了另一个约定:通过在用户表中使用用户名和密码列,CakePHP将能够在实现用户时自动配置大多数事情登录。
下一步是创建我们的UsersTable类,负责查找,保存和验证任何用户数据:
// 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('username', 'A username is required')
->notEmpty('password', 'A password is required')
->notEmpty('role', 'A role is required')
->add('role', 'inList', [
'rule' => ['inList', ['admin', 'author']],
'message' => 'Please enter a valid role'
]);
}
}
让我们创建我们的UsersController。 以下内容对应于使用CakePHP捆绑的代码生成实用程序的基本烘焙的UsersController类的部分:
// 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(__('The user has been saved.'));
return $this->redirect(['action' => 'add']);
}
$this->Flash->error(__('Unable to add the user.'));
}
$this->set('user', $user);
}
}
以同样的方式,我们使用代码生成工具创建我们的文章的视图,我们可以实现用户视图。 为了本教程的目的,我们将只显示add.ctp:
<!-- src/Template/Users/add.ctp -->
<div class="users form">
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Add User') ?></legend>
<?= $this->Form->input('username') ?>
<?= $this->Form->input('password') ?>
<?= $this->Form->input('role', [
'options' => ['admin' => 'Admin', 'author' => 'Author']
]) ?>
</fieldset>
<?= $this->Form->button(__('Submit')); ?>
<?= $this->Form->end() ?>
</div>
认证(登录和注销)
我们现在准备添加我们的身份验证层。 在CakePHP中,这由Cake\Controller\Component\AuthComponent
,该类负责要求登录某些操作,处理用户登录和注销以及授权登录用户允许他们访问的操作。
要将此组件添加到应用程序,请打开src / Controller / AppController.php文件,并添加以下行:
// 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']);
}
//...
}
没有太多配置,因为我们使用用户表的约定。 我们只是设置了在登录和注销操作执行后将加载的URL,在我们的例子中分别为/articles/
和/
。
我们在beforeFilter()
函数中做的是告诉AuthComponent在每个控制器中不要求所有index()
和view()
操作的登录。 我们希望我们的访客能够阅读和列出条目,而无需在网站上注册。
现在,我们需要能够注册新用户,保存他们的用户名和密码,更重要的是,哈希他们的密码,所以它不是作为纯文本存储在我们的数据库中。 让我们告诉AuthComponent让未经身份验证的用户访问用户添加功能并实现登录和注销操作:
// src/Controller/UsersController.php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Event\Event;
class UsersController extends AppController
{
// Other methods..
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
// Allow users to register and logout.
// You should not add the "login" action to allow list. Doing so would
// cause problems with normal functioning of 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(__('Invalid username or password, try again'));
}
}
public function logout()
{
return $this->redirect($this->Auth->logout());
}
}
密码散列还没有完成,我们需要一个Entity类为我们的用户处理自己的具体逻辑。 创建src / Model / Entity / User.php实体文件并添加以下内容:
// src/Model/Entity/User.php
namespace App\Model\Entity;
use Cake\Auth\DefaultPasswordHasher;
use Cake\ORM\Entity;
class User extends Entity
{
// Make all fields mass assignable except for primary key field "id".
protected $_accessible = [
'*' => true,
'id' => false
];
// ...
protected function _setPassword($password)
{
return (new DefaultPasswordHasher)->hash($password);
}
// ...
}
现在每次将密码属性分配给用户时,它将使用DefaultPasswordHasher类进行散列。 我们只是缺少登录功能的模板视图文件。 打开src / Template / Users / login.ctp文件并添加以下行:
<!-- File: src/Template/Users/login.ctp -->
<div class="users form">
<?= $this->Flash->render() ?>
<?= $this->Form->create() ?>
<fieldset>
<legend><?= __('Please enter your username and password') ?></legend>
<?= $this->Form->input('username') ?>
<?= $this->Form->input('password') ?>
</fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
您现在可以通过访问/users/add
URL注册一个新用户,并通过转至/users/login
URL /users/login
新创建的凭据。 此外,尝试访问任何其他未明确允许的URL,例如/articles/add
,您会看到应用程序自动将您重定向到登录页面。
就是这样! 它看起来太简单,是真的。 让我们回去一点解释发生了什么。 beforeFilter()
函数告诉AuthComponent除了在AppController的beforeFilter()
函数中已经允许的index()
和view()
操作之外,不需要add()
操作的登录。
login()
操作调用$this->Auth->identify()
的$this->Auth->identify()
函数,它可以在没有任何进一步配置的情况下工作,因为我们遵循前面提到的约定。 也就是说,拥有一个包含用户名和密码列的Users表,并使用带有用户数据的控制器发布的表单。 此函数返回登录是否成功,如果成功,则将用户重定向到我们在将AuthComponent添加到我们的应用程序时使用的配置的重定向URL。
注销只需访问/users/logout
URL即可,并将用户重定向到之前描述的配置的logoutUrl。 此URL是成功时AuthComponent::logout()
函数的结果。
授权(谁被允许访问什么)
如前所述,我们将这个博客转换为多用户创作工具,为了做到这一点,我们需要修改articles表一点,以添加对Users表的引用:
ALTER TABLE articles ADD COLUMN user_id INT(11);
// src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Added this line
$article->user_id = $this->Auth->user('id');
// You could also do the following
//$newData = ['user_id' => $this->Auth->user('id')];
//$article = $this->Articles->patchEntity($article, $newData);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$this->set('article', $article);
// Just added the categories list to be able to choose
// one category for an article
$categories = $this->Articles->Categories->find('treeList');
$this->set(compact('categories'));
}
组件提供的user()
函数返回当前登录用户的任何列。 我们使用此方法将数据添加到保存的请求信息中。
让我们保护我们的应用程序,以防止一些作者编辑或删除他人的文章。 我们的应用程序的基本规则是,管理员用户可以访问每个网址,而普通用户(作者角色)只能访问允许的操作。 再次,打开AppController类,并添加几个选项到Auth配置:
// src/Controller/AppController.php
public function initialize()
{
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authorize' => ['Controller'], // Added this line
'loginRedirect' => [
'controller' => 'Articles',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
}
public function isAuthorized($user)
{
// Admin can access every action
if (isset($user['role']) && $user['role'] === 'admin') {
return true;
}
// Default deny
return false;
}
我们刚刚创建了一个简单的授权机制。 具有admin
角色的用户将能够在登录时访问网站中的任何网址。 所有其他用户(具有author
角色的用户)将与未登录的用户具有相同的访问权限。
这不是我们想要的。 我们需要为我们的isAuthorized()
方法提供更多的规则。 但是,不是在AppController中执行它,我们将委托将这些额外的规则提供给每个单独的控制器。 我们要添加到ArticlesController的规则应该允许作者创建文章,但阻止作者编辑他们不拥有的文章。 将以下内容添加到您的ArticlesController.php :
// src/Controller/ArticlesController.php
public function isAuthorized($user)
{
// All registered users can add articles
if ($this->request->getParam('action') === 'add') {
return true;
}
// The owner of an article can edit and delete it
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);
}
我们现在重写AppController的
isAuthorized()
调用,并在内部检查父类是否已经授权用户。 如果他不是,那么只允许他访问添加操作,并有条件地访问编辑和删除。 最后一件事没有实施。 要告诉用户是否有权编辑文章,我们在Articles表中调用一个isOwnedBy()
函数。 让我们实现该
// src/Model/Table/ArticlesTable.php
public function isOwnedBy($articleId, $userId)
{
return $this->exists(['id' => $articleId, 'user_id' => $userId]);
}
这就是我们简单的认证和授权教程。 为了保护UsersController,你可以遵循我们为ArticlesController所做的相同的技术。 你也可以更具创造性,并根据你自己的规则在AppController中编写更一般的代码。
如果您需要更多的控制,我们建议您阅读认证部分中的完整的认证指南,您可以在其中找到有关配置组件,创建自定义授权类等等的更多信息。