简单的身份验证和授权应用

接着我们的 博客教程 的例子,设想一下我们想要根据登录的用户来保护某些网址。我们还有另外一个要求,允许我们的博客应用有多个作者,每一位作者都可以创作他们自己的文章(posts),随意编辑和删除它们,而不允许其他作者对自己的文章做任何改动。

创建所有用户的相关代码

首先,让我们在博客数据库中新建一个表来保存用户的数据:

  1. CREATE TABLE users (
  2. id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  3. username VARCHAR(50),
  4. password VARCHAR(255),
  5. role VARCHAR(20),
  6. created DATETIME DEFAULT NULL,
  7. modified DATETIME DEFAULT NULL
  8. );

我们遵循CakePHP的约定来给表命名,同时我们也利用了另一个约定:在users表中使用username和password列,CakePHP能够自动配置好实现用户登录的大部分工作。

下一步是创建 User 模型,负责查询、保存和验证任何用户数据:

  1. // app/Model/User.php
  2. App::uses('AppModel', 'Model');
  3.  
  4. class User extends AppModel {
  5. public $validate = array(
  6. 'username' => array(
  7. 'required' => array(
  8. 'rule' => array('notBlank'),
  9. 'message' => 'A username is required'
  10. )
  11. ),
  12. 'password' => array(
  13. 'required' => array(
  14. 'rule' => array('notBlank'),
  15. 'message' => 'A password is required'
  16. )
  17. ),
  18. 'role' => array(
  19. 'valid' => array(
  20. 'rule' => array('inList', array('admin', 'author')),
  21. 'message' => 'Please enter a valid role',
  22. 'allowEmpty' => false
  23. )
  24. )
  25. );
  26. }

让我们也创建 UsersController 控制器,下面的代码是使用 CakePHP 捆绑的代码生成工具生成的基本的 UsersController 类:

  1. // app/Controller/UsersController.php
  2. App::uses('AppController', 'Controller');
  3.  
  4. class UsersController extends AppController {
  5.  
  6. public function beforeFilter() {
  7. parent::beforeFilter();
  8. $this->Auth->allow('add');
  9. }
  10.  
  11. public function index() {
  12. $this->User->recursive = 0;
  13. $this->set('users', $this->paginate());
  14. }
  15.  
  16. public function view($id = null) {
  17. $this->User->id = $id;
  18. if (!$this->User->exists()) {
  19. throw new NotFoundException(__('Invalid user'));
  20. }
  21. $this->set('user', $this->User->findById($id));
  22. }
  23.  
  24. public function add() {
  25. if ($this->request->is('post')) {
  26. $this->User->create();
  27. if ($this->User->save($this->request->data)) {
  28. $this->Flash->success(__('The user has been saved'));
  29. return $this->redirect(array('action' => 'index'));
  30. }
  31. $this->Flash->error(
  32. __('The user could not be saved. Please, try again.')
  33. );
  34. }
  35. }
  36.  
  37. public function edit($id = null) {
  38. $this->User->id = $id;
  39. if (!$this->User->exists()) {
  40. throw new NotFoundException(__('Invalid user'));
  41. }
  42. if ($this->request->is('post') || $this->request->is('put')) {
  43. if ($this->User->save($this->request->data)) {
  44. $this->Flash->success(__('The user has been saved'));
  45. return $this->redirect(array('action' => 'index'));
  46. }
  47. $this->Flash->error(
  48. __('The user could not be saved. Please, try again.')
  49. );
  50. } else {
  51. $this->request->data = $this->User->findById($id);
  52. unset($this->request->data['User']['password']);
  53. }
  54. }
  55.  
  56. public function delete($id = null) {
  57. // 在 2.5 版本之前,请使用
  58. // $this->request->onlyAllow('post');
  59.  
  60. $this->request->allowMethod('post');
  61.  
  62. $this->User->id = $id;
  63. if (!$this->User->exists()) {
  64. throw new NotFoundException(__('Invalid user'));
  65. }
  66. if ($this->User->delete()) {
  67. $this->Flash->success(__('User deleted'));
  68. return $this->redirect(array('action' => 'index'));
  69. }
  70. $this->Flash->error(__('User was not deleted'));
  71. return $this->redirect(array('action' => 'index'));
  72. }
  73.  
  74. }

在 2.5 版更改: 自从 2.5 版本起,请使用 CakeRequest::allowMethod() 而不是CakeRequest::onlyAllow() (已作废)。

以我们创建博客文章的视图同样的方式,或者使用代码生成工具,我们来实现视图。出于这个教程的目的,这里仅展示 add.ctp 视图:

  1. <!-- app/View/Users/add.ctp -->
  2. <div class="users form">
  3. <?php echo $this->Form->create('User'); ?>
  4. <fieldset>
  5. <legend><?php echo __('Add User'); ?></legend>
  6. <?php echo $this->Form->input('username');
  7. echo $this->Form->input('password');
  8. echo $this->Form->input('role', array(
  9. 'options' => array('admin' => 'Admin', 'author' => 'Author')
  10. ));
  11. ?>
  12. </fieldset>
  13. <?php echo $this->Form->end(__('Submit')); ?>
  14. </div>

身份验证 (登录和登出)

我们现在已经准备好添加我们的认证层了。在 CakePHP 中,这是由AuthComponent 组件处理的,这个类负责为某些动作要求用户登录,处理用户登录和登出,并且授权登录的用户访问他们有权限到达的的动作。

要添加这个组件到应用程序中,打开 app/Controller/AppController.php 文件,添加如下代码:

  1. // app/Controller/AppController.php
  2. class AppController extends Controller {
  3. //...
  4.  
  5. public $components = array(
  6. 'Flash',
  7. 'Auth' => array(
  8. 'loginRedirect' => array(
  9. 'controller' => 'posts',
  10. 'action' => 'index'
  11. ),
  12. 'logoutRedirect' => array(
  13. 'controller' => 'pages',
  14. 'action' => 'display',
  15. 'home'
  16. ),
  17. 'authenticate' => array(
  18. 'Form' => array(
  19. 'passwordHasher' => 'Blowfish'
  20. )
  21. )
  22. )
  23. );
  24.  
  25. public function beforeFilter() {
  26. $this->Auth->allow('index', 'view');
  27. }
  28. //...
  29. }

这里没有多少需要配置的,因为我们的 users 表遵循了命名约定。我们只设置了在登录和登出的动作完成之后要加载的网址,在我们的例子中,分别是 /posts//

我们在 beforeFilter 回调函数中所做的是告诉 AuthComponent 组件,在每个控制器中所有的 indexview 动作中都不需要登录。我们希望我们的访问者不需要在网站中注册就能够读取并列出文章。

现在,我们需要能够注册新用户,保存它们的用户名和密码,而且,更重要的是,哈希(hash)他们的密码,这样在我们的数据库中就不是用普通文本形式保存用户的密码了。让我们告诉 AuthComponent 组件让未验证的用户访问添加用户函数,并实现登录和登出动作:

  1. // app/Controller/UsersController.php
  2.  
  3. public function beforeFilter() {
  4. parent::beforeFilter();
  5. // Allow users to register and logout.
  6. $this->Auth->allow('add', 'logout');
  7. }
  8.  
  9. public function login() {
  10. if ($this->request->is('post')) {
  11. if ($this->Auth->login()) {
  12. return $this->redirect($this->Auth->redirectUrl());
  13. }
  14. $this->Flash->error(__('Invalid username or password, try again'));
  15. }
  16. }
  17.  
  18. public function logout() {
  19. return $this->redirect($this->Auth->logout());
  20. }

密码的哈希还没有做,打开 app/Model/User.php 模型文件,添加如下代码:

  1. // app/Model/User.php
  2.  
  3. App::uses('AppModel', 'Model');
  4. App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');
  5.  
  6. class User extends AppModel {
  7.  
  8. // ...
  9.  
  10. public function beforeSave($options = array()) {
  11. if (isset($this->data[$this->alias]['password'])) {
  12. $passwordHasher = new BlowfishPasswordHasher();
  13. $this->data[$this->alias]['password'] = $passwordHasher->hash(
  14. $this->data[$this->alias]['password']
  15. );
  16. }
  17. return true;
  18. }
  19.  
  20. // ...

注解

BlowfishPasswordHasher 类使用更强的哈希算法(bcrypt),而不是SimplePasswordHasher (sha1),提供用户级的 salt。SimplePasswordHasher 类会在CakePHP 3.0 版本中去掉。

所以,现在每次保存用户的时候,都会使用 BlowfishPasswordHasher 类进行哈希。我们还缺 login 函数的模板视图文件。打开文件 app/View/Users/login.ctp,添加如下这些行:

  1. //app/View/Users/login.ctp
  2.  
  3. <div class="users form">
  4. <?php echo $this->Flash->render('auth'); ?>
  5. <?php echo $this->Form->create('User'); ?>
  6. <fieldset>
  7. <legend>
  8. <?php echo __('Please enter your username and password'); ?>
  9. </legend>
  10. <?php echo $this->Form->input('username');
  11. echo $this->Form->input('password');
  12. ?>
  13. </fieldset>
  14. <?php echo $this->Form->end(__('Login')); ?>
  15. </div>

现在你可以访问 /users/add 网址来注册新用户,并在 /users/login 网址使用新创建的凭证登录。也可以试试访问任何其它没有明确允许访问的网址,比如/posts/add,你会看到应用程序会自动转向到登录页面。

就是这样!简单到不可思议。让我们回过头来解释一下发生的事情。beforeFilter回调函数告诉 AuthComponent 组件,除了在 AppController 的 beforeFilter 函数中已经允许访问的 indexview 动作,对 add 动作也不要求登录。

login 动作调用 AuthComponent 组件的 $this->Auth->login() 函数,这不需要任何更多的设置,因为,正如之前提到的,我们遵循了约定。即,User模型有 username 和 password 列,使用表单提交用户的数据到控制器。这个函数返回登录是否成功,如果成功,就重定向用户到在我们把 AuthComponent 组件添加到应用程序中时所设置的跳转网址。

要登出,只需要访问网址 /users/logout,就会重定向用户到先前描述的配置好了的logoutUrl。这个网址就是 AuthComponent::logout() 函数成功时返回的结果。

权限(谁可以访问什么)

前面已经说了,我们要把这个博客应用改成多用户的创作工具,为此,我们需要稍微修改posts 表,添加对 User 模型的引用:

  1. ALTER TABLE posts ADD COLUMN user_id INT(11);

另外,必须对 PostsController 做一个小改动,在新增的文章中要把当前登录的用户作为引用保存:

  1. // app/Controller/PostsController.php
  2. public function add() {
  3. if ($this->request->is('post')) {
  4. //Added this line
  5. $this->request->data['Post']['user_id'] = $this->Auth->user('id');
  6. if ($this->Post->save($this->request->data)) {
  7. $this->Flash->success(__('Your post has been saved.'));
  8. return $this->redirect(array('action' => 'index'));
  9. }
  10. }
  11. }

由组件提供的 user() 函数,返回当前登录用户的任何列。我们使用这个方法将数据加入请求信息中,来保存。

让我们增强应用程序的安全性,避免一些作者编辑或删除其他作者的文章。应用的基本规则是,管理用户可以访问任何网址,而普通用户(作者角色)只能访问允许的动作。再次打开AppController 类,在 Auth 的配置中再添加一些选项:

  1. // app/Controller/AppController.php
  2.  
  3. public $components = array(
  4. 'Flash',
  5. 'Auth' => array(
  6. 'loginRedirect' => array('controller' => 'posts', 'action' => 'index'),
  7. 'logoutRedirect' => array(
  8. 'controller' => 'pages',
  9. 'action' => 'display',
  10. 'home'
  11. ),
  12. 'authorize' => array('Controller') // Added this line
  13. )
  14. );
  15.  
  16. public function isAuthorized($user) {
  17. // Admin 可以访问每个动作
  18. if (isset($user['role']) && $user['role'] === 'admin') {
  19. return true;
  20. }
  21.  
  22. // 默认不允许访问
  23. return false;
  24. }

我们只创建了一个非常简单的权限机制。在这个例子中,admin 角色的用户在登录后可以访问网站的任何网址,而其余的用户(即角色 author)不能够做任何与未登录的用户不同的事情。

这并不是我们所想要的,所以我们需要为 isAuthorized() 方法提供更多的规则。但不是在 AppController 中设置,而是在每个控制器提供这些额外的规则。我们要在PostsController 中增加的规则,应当允许作者创建文章,但在作者不匹配时要防止对其文章的编辑。打开 PostsController.php 文件,添加如下内容:

  1. // app/Controller/PostsController.php
  2.  
  3. public function isAuthorized($user) {
  4. // 所有注册的用户都能够添加文章
  5. if ($this->action === 'add') {
  6. return true;
  7. }
  8.  
  9. // 文章的所有者能够编辑和删除它
  10. if (in_array($this->action, array('edit', 'delete'))) {
  11. $postId = (int) $this->request->params['pass'][0];
  12. if ($this->Post->isOwnedBy($postId, $user['id'])) {
  13. return true;
  14. }
  15. }
  16.  
  17. return parent::isAuthorized($user);
  18. }

现在,如果在父类中已授权该用户,我们就重载 AppController 的 isAuthorized() 方法的调用和内部的检查。如果用户未被授权,则只允许他访问 add 动作,并有条件地访问 edit 和 delete 动作。最后要实现的是判断用户是否有权限编辑文章,为此调用Post 模型的 isOwnedBy() 方法。通常,最佳实践是尽量把逻辑挪到模型中。下面让我们来实现这个函数:

  1. // app/Model/Post.php
  2.  
  3. public function isOwnedBy($post, $user) {
  4. return $this->field('id', array('id' => $post, 'user_id' => $user)) !== false;
  5. }

简单的身份验证和授权教程到这里就结束了。要保护 UsersController 控制器,可以采用我们在 PostsController 控制器中的做法。你也可以更有造作性,根据你自己的规则在AppController 控制器中添加更普遍的规则。

如果你需要更多的控制,我们建议你阅读完整的 Auth 组件的指南Authentication,你可以看到更多该组件的配置,创建自定义的 Authorization 类,以及更多信息。

后续阅读的建议