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

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

    我们遵循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. $this->Flash->error(
    47. __('The user could not be saved. Please, try again.')
    48. );
    49. } else {
    50. $this->request->data = $this->User->findById($id);
    51. unset($this->request->data['User']['password']);
    52. }
    53. }
    54.  
    55. public function delete($id = null) {
    56. // 在 2.5 版本之前,请使用
    57. // $this->request->onlyAllow('post');
    58.  
    59. $this->request->allowMethod('post');
    60.  
    61. $this->User->id = $id;
    62. if (!$this->User->exists()) {
    63. throw new NotFoundException(__('Invalid user'));
    64. }
    65. if ($this->User->delete()) {
    66. $this->Flash->success(__('User deleted'));
    67. return $this->redirect(array('action' => 'index'));
    68. }
    69. $this->Flash->error(__('User was not deleted'));
    70. return $this->redirect(array('action' => 'index'));
    71. }
    72.  
    73. }

    在 2.5 版更改: 自从 2.5 版本起,请使用 而不是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 文件,添加如下代码:

    我们在 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. );
    15. }
    16. return true;
    17. }
    18.  
    19. // ...

    注解

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

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

    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 动作也不要求登录。

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

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

    另外,必须对 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() 方法。通常,最佳实践是尽量把逻辑挪到模型中。下面让我们来实现这个函数:

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