Практическая реализация Zend_ACL + Zend_Auth и лучшие практики

Контекст:

Мои вопросы относятся к форуму, который я разрабатываю почти так же, как SO, где есть:

  1. гости, которые имеют доступ к обсуждениям, но не могут отвечать или голосовать
  2. участники, которые с достаточным количеством репутации могут редактировать / голосовать за другие темы, и по умолчанию они могут отвечать и иметь те же привилегии, что и гости
  3. админы, которые могут делать что угодно

Я бы хотел, чтобы этот ACL применялся ко всему сайту и по умолчанию запрещал все ресурсы.

Я прочитал основы использования Zend_Acl - вы в основном создаете роли (гость, участник, администратор) и либо запрещаете, либо разрешаете ресурсы (контроллеры, методы) для этих ролей. В документации нет подробностей о том, как на самом деле следует реализовать код acl в своем приложении, поэтому я поискал SO ..

Наткнулся на довольно полезный stackoverflow ответ от marek, который проливает свет на проблему Однако из-за незнания я до сих пор не могу полностью понять, как правильно реализовать это с учетом передовых практик.

У плаката есть статический файл configAcl.php в корне приложения, который инициализирует объект acl, добавляет роли, создает ресурс для каждого контроллера, дает admin доступ ко всему, дает normal доступ ко всему, кроме администратора, и сохраняет объект acl в реестре. для дальнейшего использования.

$acl = new Zend_Acl();

$roles  = array('admin', 'normal');

// Controller script names. You have to add all of them if credential check
// is global to your application.
$controllers = array('auth', 'index', 'news', 'admin');

foreach ($roles as $role) {
    $acl->addRole(new Zend_Acl_Role($role));
}
foreach ($controllers as $controller) {
    $acl->add(new Zend_Acl_Resource($controller));
}

// Here comes credential definiton for admin user.
$acl->allow('admin'); // Has access to everything.

// Here comes credential definition for normal user.
$acl->allow('normal'); // Has access to everything...
$acl->deny('normal', 'admin'); // ... except the admin controller.

// Finally I store whole ACL definition to registry for use
// in AuthPlugin plugin.
$registry = Zend_Registry::getInstance();
$registry->set('acl', $acl);

Вопрос № 1. Должен ли этот код быть в начальной загрузке или в отдельном файле, таком как этот? Если да, то было бы лучше, если бы он находился внутри, скажем, каталога библиотеки?

Вторая часть - это новый класс, расширяющий класс Zend Controller Plugin Abstract, который позволяет подключать его к auth/login, логика в основном заключается в том, что если логин не удался, он перенаправляет ... в противном случае он захватывает объект acl из реестра, захватывает identity и определяет, разрешено ли пользователю просматривать этот ресурс.

$identity = $auth->getIdentity();

$frontController->registerPlugin(new AuthPlugin());

Вопрос № 2. Как именно я могу кодировать часть плагина аутентификации, которая фактически возвращает личность пользователя? Я понимаю, что у него был код ниже, который сгенерировал объект таблицы db адаптера Auth, который будет запрашивать столбец таблицы базы данных по идентификатору пользователя и учетным данным (проверка хешированного прохода). Я не понимаю, где это соответствует части getIdentity.

Допустим, моя таблица пользователей состояла из следующих данных:

user_id    user_name    level
1          superadmin   3
2          john         2
3          example.com  1

Где уровень 3 = администратор, 2 = участник, 1 = гость.

Вопрос № 3 - где именно лучше всего поместить указанный выше код авторизации? Внутри контроллера входа в систему?

Вопрос № 4 - другой автор отвечает своей статьей о том, как логика acl должна выполняться внутри моделей, но конкретный метод, который он использует, изначально не поддерживается и требует обходного пути. Возможно ли это? И действительно ли это так в идеале?


person meder omuraliev    schedule 12.01.2010    source источник


Ответы (1)


Моя реализация:

Вопрос 1

class App_Model_Acl extends Zend_Acl
{   
    const ROLE_GUEST        = 'guest';
    const ROLE_USER         = 'user';
    const ROLE_PUBLISHER    = 'publisher';
    const ROLE_EDITOR       = 'editor';
    const ROLE_ADMIN        = 'admin';
    const ROLE_GOD          = 'god';

    protected static $_instance;

    /* Singleton pattern */
    protected function __construct()
    {
        $this->addRole(new Zend_Acl_Role(self::ROLE_GUEST));
        $this->addRole(new Zend_Acl_Role(self::ROLE_USER), self::ROLE_GUEST);
        $this->addRole(new Zend_Acl_Role(self::ROLE_PUBLISHER), self::ROLE_USER);
        $this->addRole(new Zend_Acl_Role(self::ROLE_EDITOR), self::ROLE_PUBLISHER);
        $this->addRole(new Zend_Acl_Role(self::ROLE_ADMIN), self::ROLE_EDITOR);

        //unique role for superadmin
        $this->addRole(new Zend_Acl_Role(self::ROLE_GOD));

        $this->allow(self::ROLE_GOD);

        /* Adding new resources */
        $this->add(new Zend_Acl_Resource('mvc:users'))
             ->add(new Zend_Acl_Resource('mvc:users.auth'), 'mvc:users')
             ->add(new Zend_Acl_Resource('mvc:users.list'), 'mvc:users');

        $this->allow(null, 'mvc:users', array('index', 'list'));
        $this->allow('guest', 'mvc:users.auth', array('index', 'login'));
        $this->allow('guest', 'mvc:users.list', array('index', 'list'));
        $this->deny(array('user'), 'mvc:users.auth', array('login'));


        /* Adding new resources */
        $moduleResource = new Zend_Acl_Resource('mvc:snippets');
        $this->add($moduleResource)
             ->add(new Zend_Acl_Resource('mvc:snippets.crud'), $moduleResource)
             ->add(new Zend_Acl_Resource('mvc:snippets.list'), $moduleResource);

        $this->allow(null, $moduleResource, array('index', 'list'));
        $this->allow('user', 'mvc:snippets.crud', array('create', 'update', 'delete', 'read', 'list'));
        $this->allow('guest', 'mvc:snippets.list', array('index', 'list'));

        return $this;
    }

    protected static $_user;

    public static function setUser(Users_Model_User $user = null)
    {
        if (null === $user) {
            throw new InvalidArgumentException('$user is null');
        }

        self::$_user = $user;
    }

    /**
     * 
     * @return App_Model_Acl
     */
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public static function resetInstance()
    {
        self::$_instance = null;
        self::getInstance();
    }
}



class Smapp extends Bootstrap // class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    /**
     * @var App_Model_User
     */
    protected static $_currentUser;

    public function __construct($application)
    {
        parent::__construct($application);
    }

    public static function setCurrentUser(Users_Model_User $user)
    {
        self::$_currentUser = $user;
    }

    /**
     * @return App_Model_User
     */
    public static function getCurrentUser()
    {
        if (null === self::$_currentUser) {
            self::setCurrentUser(Users_Service_User::getUserModel());
        }
        return self::$_currentUser;
    }

    /**
     * @return App_Model_User
     */
    public static function getCurrentUserId()
    {
        $user = self::getCurrentUser();
        return $user->getId();
    }

}

in class bootstrap

protected function _initUser()
{
    $auth = Zend_Auth::getInstance();
    if ($auth->hasIdentity()) {
        if ($user = Users_Service_User::findOneByOpenId($auth->getIdentity())) {
            $userLastAccess = strtotime($user->last_access);
            //update the date of the last login time in 5 minutes
            if ((time() - $userLastAccess) > 60*5) {
                $date = new Zend_Date();
                $user->last_access = $date->toString('YYYY-MM-dd HH:mm:ss');
                $user->save();
            }
            Smapp::setCurrentUser($user);
        }
    }
    return Smapp::getCurrentUser();
}

protected function _initAcl()
{
    $acl = App_Model_Acl::getInstance();
    Zend_View_Helper_Navigation_HelperAbstract::setDefaultAcl($acl);
    Zend_View_Helper_Navigation_HelperAbstract::setDefaultRole(Smapp::getCurrentUser()->role);
    Zend_Registry::set('Zend_Acl', $acl);
    return $acl;
}

и Front_Controller_Plugin

class App_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
    private $_identity;

    /**
     * the acl object
     *
     * @var zend_acl
     */
    private $_acl;

    /**
     * the page to direct to if there is a current
     * user but they do not have permission to access
     * the resource
     *
     * @var array
     */
    private $_noacl = array('module' => 'admin',
                             'controller' => 'error',
                             'action' => 'no-auth');

    /**
     * the page to direct to if there is not current user
     *
     * @var unknown_type
     */
    private $_noauth = array('module' => 'users',
                             'controller' => 'auth',
                             'action' => 'login');


    /**
     * validate the current user's request
     *
     * @param zend_controller_request $request
     */
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        $this->_identity = Smapp::getCurrentUser();
        $this->_acl = App_Model_Acl::getInstance();

        if (!empty($this->_identity)) {
            $role = $this->_identity->role;
        } else {
            $role = null;
        }

        $controller = $request->controller;
        $module = $request->module;
        $controller = $controller;
        $action = $request->action;

        //go from more specific to less specific
        $moduleLevel = 'mvc:'.$module;
        $controllerLevel = $moduleLevel . '.' . $controller;
        $privelege = $action;


        if ($this->_acl->has($controllerLevel)) {
            $resource = $controllerLevel;
        } else {
            $resource = $moduleLevel;
        }

        if ($module != 'default' && $controller != 'index') {
            if ($this->_acl->has($resource) && !$this->_acl->isAllowed($role, $resource, $privelege)) {
                if (!$this->_identity) {
                    $request->setModuleName($this->_noauth['module']);
                    $request->setControllerName($this->_noauth['controller']);
                    $request->setActionName($this->_noauth['action']);
                    //$request->setParam('authPage', 'login');
                } else {
                   $request->setModuleName($this->_noacl['module']);
                   $request->setControllerName($this->_noacl['controller']);
                   $request->setActionName($this->_noacl['action']);
                   //$request->setParam('authPage', 'noauth');
               }
               throw new Exception('Access denied. ' . $resource . '::' . $role);
            }
        }
    }
}

и наконец - Auth_Controller` :)

class Users_AuthController extends Smapp_Controller_Action 
{   
    //sesssion
    protected $_storage;

    public function getStorage()
    {
        if (null === $this->_storage) {
            $this->_storage = new Zend_Session_Namespace(__CLASS__);
        }
        return $this->_storage;
    }

    public function indexAction()
    {
        return $this->_forward('login');
    }

    public function loginAction()
    {   
        $openId = null;
        if ($this->getRequest()->isPost() and $openId = ($this->_getParam('openid_identifier', false))) {
            //do nothing
        } elseif (!isset($_GET['openid_mode'])) {
            return; 
        }

        //$userService = $this->loadService('User');

        $userService = new Users_Service_User();

        $result = $userService->authenticate($openId, $this->getResponse());

        if ($result->isValid()) {
            $identity = $result->getIdentity();
            if (!$identity['Profile']['display_name']) {
                return $this->_helper->redirector->gotoSimpleAndExit('update', 'profile');
            }
            $this->_redirect('/');
        } else {
            $this->view->errorMessages = $result->getMessages();
        }
    }

    public function logoutAction()
    {
        $auth = Zend_Auth::getInstance();
        $auth->clearIdentity();
        //Zend_Session::destroy();
        $this->_redirect('/');
    }
}

Вопрос 2

держите его внутри Zend_Auth.

после успешной аутентификации - записать личность в хранилище. $auth->getStorage()->write($result->getIdentity());

identity - это просто user_id

Дизайн БД

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `open_id` varchar(255) NOT NULL,
  `role` varchar(20) NOT NULL,
  `last_access` datetime NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `open_id` (`open_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8

CREATE TABLE `user_profile` (
  `user_id` bigint(20) NOT NULL,
  `display_name` varchar(100) DEFAULT NULL,
  `email` varchar(100) DEFAULT NULL,
  `real_name` varchar(100) DEFAULT NULL,
  `website_url` varchar(255) DEFAULT NULL,
  `location` varchar(100) DEFAULT NULL,
  `birthday` date DEFAULT NULL,
  `about_me` text,
  `view_count` int(11) NOT NULL DEFAULT '0',
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

немного сахара

/**
 * SM's code library
 * 
 * @category    
 * @package     
 * @subpackage  
 * @copyright   Copyright (c) 2009 Pavel V Egorov
 * @author      Pavel V Egorov
 * @link        http://epavel.ru/
 * @since       08.09.2009
 */


class Smapp_View_Helper_IsAllowed extends Zend_View_Helper_Abstract
{
    protected $_acl;
    protected $_user;

    public function isAllowed($resource = null, $privelege = null)
    {
        return (bool) $this->getAcl()->isAllowed($this->getUser(), $resource, $privelege);
    }

    /**
     * @return App_Model_Acl
     */
    public function getAcl()
    {
        if (null === $this->_acl) {
            $this->setAcl(App_Model_Acl::getInstance());
        }
        return $this->_acl;
    }

    /**
     * @return App_View_Helper_IsAllowed
     */
    public function setAcl(Zend_Acl $acl)
    {
        $this->_acl = $acl;
        return $this;
    }

    /**
     * @return Users_Model_User
     */
    public function getUser()
    {
        if (null === $this->_user) {
            $this->setUser(Smapp::getCurrentUser());
        }
        return $this->_user;
    }

    /**
     * @return App_View_Helper_IsAllowed
     */
    public function setUser(Users_Model_User $user)
    {
        $this->_user = $user;
        return $this;
    }

}

для подобных вещей в любом скрипте просмотра

 <?php if ($this->isAllowed('mvc:snippets.crud', 'update')) : ?>
    <a title="Edit &laquo;<?=$this->escape($snippetInfo['title'])?>&raquo; snippet">Edit</a>
 <?php endif?>

Вопросы? :)

person SMka    schedule 12.01.2010
comment
Замечательно, я ценю время, которое вы потратили, чтобы вставить все это. Я попробую реализовать это и дам вам знать, еще раз спасибо. - person meder omuraliev; 12.01.2010
comment
хорошо, это намного лучше, чем я думал, хотя один вопрос. почему объект acl в App_Plugin_Auth не извлекается из реестра, в который вы его поместили? спасибо за пост - person black sensei; 17.01.2011
comment
с синглтоном это не важно, откуда взять ACL - person SMka; 10.03.2011
comment
В каком файле должен находиться класс Smapp? - person Randell; 23.06.2011
comment
Что такое Smapp_Controller_Action? - person Randell; 23.06.2011
comment
Smapp_Controller_Action расширить Zend_Controller_Action - person SMka; 23.06.2011
comment
Smapp - это мой личный префикс библиотеки. переименуйте его как вам нужно - person SMka; 23.06.2011
comment
Эй, это действительно полезно, спасибо. Единственное, что мне непонятно, это класс Smapp. Используется ли он когда-нибудь в контексте объекта? Где это создается? Может быть, вместо обычного бутстрапа? - person DatsunBing; 22.09.2011
comment
Smapp - это название его приложения. По умолчанию имя приложения Zend - Default. - person Sonny; 04.01.2012
comment
class Smapp extends Bootstrap // class Bootstrap extends Zend_Application_Bootstrap_Bootstrap и bootstrap.path = APPLICATION_PATH "/ApplicationBootstrap.php" bootstrap.class = "Smapp" в application.ini файле конфигурации. пришло время протестировать ZF2 :) - person SMka; 16.03.2012
comment
Хм. Что делать, если у меня есть роли в БД? - person bksi; 30.07.2012
comment
Проголосуйте только за усилия, вложенные в этот ответ, я просто изучаю лучшие практики для ACL, поскольку это потребуется для проекта. Проверялось ли это каким-либо образом на проникновение? - person Dave Mackintosh; 13.12.2012