在Zend Framework上处理类似登录页面的最佳方法是什么? (为什么我的实现会爆炸?)

时间:2011-03-04 03:52:35

标签: php zend-framework zend-application

编辑:抱歉这里有大量代码;我不确定到底发生了什么,所以我加入更多是为了安全。

我目前有一个登录页面,该页面可以转移到中央身份验证服务。我想对用户进行权限检查。如果用户未登录,我想将它们重定向到登录页面,并让登录页面重定向它们以执行它们最初执行的任何操作,再次运行访问检查。如果他们没有权限,我想将他们重定向到拒绝访问的页面。

这是我到目前为止所做的:

将此行添加到我的application.ini

resources.frontController.actionHelperPaths.Cas_Controller_Action_Helper = APPLICATION_PATH "/controllers/helpers"

创建了文件$/application/controllers/helpers/PermissionRequire.php

<?php
/**
 * This class is used in order to require that a user have a given privilege before continuing.
 *
 * @copyright 2011 Case Western Reserve University, College of Arts and Sciences
 * @author Billy O'Neal III (bro4@case.edu)
 */

class Cas_Controller_Action_Helper_PermissionRequire extends Zend_Controller_Action_Helper_Abstract
{
    /**
     * Cleans up the supplied list of privileges. Strings are turned into the real privilege objects (Based on name),
     * privilege objects are left alone.
     *
     * @static
     * @param array|Privilege|string $privileges
     * @return array
     */
    private static function CleanPrivileges($privileges)
    {
        if (!is_array($privileges))
        {
            $privileges =
                    array
                    (
                        $privileges
                    );
        }
        $strings = array_filter($privileges, 'is_string');
        $objects = array_filter($privileges, function($o)
        {
            return $o instanceof Privilege;
        });
        $databaseObjects = PrivilegeQuery::create()->filterByName($strings)->find();
        return array_combine($objects, $databaseObjects);
    }

    /**
     * Generic implementation for checking whether a user can visit a page.
     * @param Privilege|string|array $privileges Any number of privileges which are required to access the given
     *                                           page. If ANY privilege is held by the user, access is allowed.
     * @param AccessControlList The acl which is being checked. Defaults to the application.
     */
    public function direct($privileges, $acl = null)
    {
        $privileges = self::CleanPrivileges($privileges);
        if ($acl === null)
        {
            $acl = AccessControlListQuery::getApplication();
        }
        $redirector = $this->getActionController()->getHelper('redirector');
        /** @var Zend_Controller_Action_Helper_Redirector $redirector */
        $redirector->setCode(307);
        if (Cas_Model_CurrentUser::IsLoggedIn() && (!Cas_Model_CurrentUser::AccessCheck($acl, $privileges)))
        {
            $redirector->gotoSimple('accessdenied', 'login');
        }
        else
        {
            $returnData = new Zend_Session_Namespace('Login');
            $returnData->params = $this->getRequest()->getParams();
            $redirector->setGotoSimple('login', 'login');
            $redirector->redirectAndExit();
        }
    }
}

这是LoginController:

<?php

/**
 * LoginController - Controls login access for users
 */

require_once 'CAS.php';

class LoginController extends Zend_Controller_Action
{
    /**
     * Logs in to the system, and redirects to the calling action.
     *
     * @return void
     */
    public function loginAction()
    {
        //Authenticate with Login.Case.Edu.
        phpCAS::client(CAS_VERSION_2_0, 'login.case.edu', 443, '/cas', false);
        phpCAS::setNoCasServerValidation();
        phpCAS::forceAuthentication();

        $user = CaseIdUser::createFromLdap(phpCAS::getUser());
        Cas_Model_CurrentUser::SetCurrentUser($user->getSecurityIdentifier());

        $returnData = new Zend_Session_Namespace('Login');
        /** @var array $params */
        $redirector = $this->_helper->redirector;
        /** @var Zend_Controller_Action_Helper_Redirector $redirector */
        $redirector->setGotoRoute($returnData->params, 'default', true);
        $returnData->unsetAll();
        $redirector->redirectAndExit();
    }

    /**
     * Logs the user out of the system, and redirects them to the index page.
     *
     * @return void
     */
    public function logoutAction()
    {
        Cas_Model_CurrentUser::Logout();
        $this->_helper->redirector->gotoRoute('index','index', 'default', true);
    }

    /**
     * Returns an access denied view.
     *
     * @return void
     */
    public function accessdeniedAction()
    {
        //Just display the view and punt.
    }
}

问题是,在登录控制器准备将用户重定向到的URL时,似乎“params”为null。此外,当调用$this->_helper->permissionRequire(SOME PRIVILEGE)的控制器的POST数据时,这将不起作用。

是否有更好的方法来存储请求的整个状态,并咳出与该请求完全匹配的重定向?

P.S。哦,这是一个使用该帮助器的示例控制器:

<?php

/**
 * Serves as the index page; does nothing but display views.
 */

class IndexController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $renderer = $this->getHelper('ViewRenderer');
        /** @var $renderer Zend_Controller_Action_Helper_ViewRenderer */
        if (Cas_Model_CurrentUser::IsLoggedIn())
        {
            $this->_helper->permissionRequire(Cas_Model_Privilege::GetLogin());
            $this->render('loggedin');
        }
        else
        {
            $this->render('loggedout');
        }
    }
}

4 个答案:

答案 0 :(得分:2)

由于你非常热衷于保存请求的POST状态,并且因为我自己一直在玩这个相同的想法,所以如下所示。它仍然没有经过测试,所以我很乐意听到设置这样保存的请求是否真的按预期工作的结果。 (暂时懒得测试一下,对不起)。

在你的配置中:

resources.frontController.plugins[] = "Cas_Controller_Plugin_Authenticator"

这是插件:

class Cas_Controller_Plugin_Authenticator
    extends Zend_Controller_Plugin_Abstract
{
    public function routeStartup( Zend_Controller_Request_Abstract $request )
    {
        if( Zend_Auth::getInstance()->hasIdentity() )
        {
            if( null !== $request->getParam( 'from-login', null ) && Zend_Session::namespaceIsset( 'referrer' ) )
            {
                $referrer = new Zend_Session_Namespace( 'referrer' );
                if( isset( $referrer->request ) && $referrer->request instanceof Zend_Controller_Request_Abstract )
                {
                    Zend_Controller_Front::getInstance()->setRequest( $referrer->request );
                }
                Zend_Session::namespaceUnset( 'referrer' );
            }
        }
        else
        {
            $referrer = new Zend_Session_Namespace( 'referrer' );
            $referrer->request = $this->getRequest();
            return $this->_redirector->gotoRoute(
                array(
                    'module' => 'default',
                    'controller' => 'user',
                    'action' => 'login'
                ),
                'default',
                true
            );
        }
    }
}

插件应检查routeStartup用户是否经过身份验证;

  • 如果用户不是:它将当前请求对象保存在会话中并重定向到UserController::loginAction()。 (见下文)
  • 如果用户IS:它从会话中检索保存的请求对象(如果可用,如果用户刚刚登录则为AND)并替换frontController中的当前请求对象(代理到我认为的路由器)。 / LI>

总而言之,如果您想要更灵活地确定哪些模块/控制器/操作参数需要身份验证和授权(我想您想要),您可能希望将一些检查移动到另一个钩子而不是{{1} }:即routeStartuprouteShutdowndispatchLoopStartup。因为到那时应该知道行动参数。作为额外的安全措施,您可能还需要比较原始请求的操作参数(模块/控制器/操作)和替换请求,以确定您是否处理了正确的已保存请求。

此外,您可能需要在新请求对象中的某些或所有挂钩中设置preDispatch。但不完全确定:见docs

这是一个示例登录控制器:

$request->setDispatched( false )

出于安全原因,您可能希望设置一个到期跳数为1的会话变量,而不是“from-login”查询字符串变量。

最后,说了这一切;你可能想彻底考虑一下你是否真的想要这种行为。正如您所知,POST请求通常会禁止敏感状态更改操作(创建,删除等)。我不确定用户在登录后是否正常期望这种行为(在他们的会话刚刚过期之后)。此外,您可能想要考虑可能导致应用程序本身出现意外行为的可能情况。我现在想不出任何具体细节,但如果我多考虑一下,我相信我能想出一些。

HTH

修改
忘记在登录过程后添加正确的重定向操作

答案 1 :(得分:1)

当它是GET请求时,它非常简单:在缺少auth时,将请求URL保存在会话中并重定向到登录。登录后,如果有保存的请求网址,请将其发送给他。对于成功的身份验证,但失败的访问权限,立即发送给他访问被拒绝。

粘性点是POST请求。仅保存请求url会错过帖子中的数据。即使您还在会话中保存了这些参数和请求类型('POST'),登录后重定向是否可以作为POST请求运行?对我来说,这就是问题的本质。

一种解决方案可能是将登录后重定向到包含表单(带method="post")的转换页面,其中所有原始帖子数据都隐藏在隐藏字段中。然后,运行一些隐藏提交按钮并提交表单的onload javascript。在没有javascript的情况下,它会相对优雅地降级:用户只需单击提交按钮。

那会有用吗?

答案 2 :(得分:1)

这不是一个编码的答案,但可能有所帮助。当我使用Zend_ACL和Zend_Auth使用MySQL后端实现时,我遇到了类似的问题。我遇到过几次我需要一个通常无法访问控制器操作的用户才能获得某种情况。 Zend已经考虑过这一点,因此您可以使用Assertions来允许访问通常被拒绝的访问。

我接下来遇到的问题是,我设置了登录超时限制 - 所以如果用户离开计算机并且有人出现他们就无法做任何事情,因为系统会被注销。

问题在于,如果他们填写了表单(有一些巨大的表单)并且花费的时间超过允许的登录时间,当他们提交表单时,他们将登陆登录页面 - 如果花了30分钟来完成表格,那会很烦人。因此,我需要将用户重定向到接受原始表单的操作,但不知何故将原始数据发布为POST请求。 My original question on this.

所以我是如何解决这个问题的,也许不是最好的解决方案,但它有效。 Inspiration came from here

  • 当用户到达登录页面时,检查其 POST  
  • 序列化 POST 数据,并将其放入包含所请求网址的会话中。  
  • 成功验证 FORWARD 对会话中保存的URL的请求。  
  • 现在在正确的位置,检查任何序列化数据并取消序列化,将$ _POST设置为数据。

    这对我有用的原因是,当用户提交登录表单时,您检查用户详细信息,执行登录所需的操作,然后转发它们而不是重定向它们,这样请求仍然是POST - 所以当您将$ _POST数据更改为未序列化的原始数据时,页面将正常工作,甚至检查$this->getRequest()->isPost()
    我发现问题的地方是,我还没有尝试解决它,如果用户按下后退按钮,他们会进入登录表单,他们不允许这样做现在已经登录了,所以收到一条消息,告知他们不允许在那里,如果他们回到原始表单会更好 - 所以它会在登录时发生。

    如果您认为这会有所帮助,我可以尝试删除一些我用来准确了解我是如何做到这一点的代码吗?

  • 答案 3 :(得分:0)

    我已经在我的项目中发布了我正在使用的内容,以根据用户是否已记录来保护一个控制器。检查登录时,以下代码也可用于检查特殊权限。如果用户没有适当的权限,请重定向到错误控制器或显示错误视图。

    此代码提供了相关问题的示例 - 在重定向到另一个控制器时将当前URL保留在参数中,并在成功时重定向回相同的URL。

    class ProtectedController extends Zend_Controller_Action {
    
        public function indexAction() {
    
        if (Zend_Auth::getInstance()->hasIdentity()) {
                   //Show the content
           } else {
                    $this->_redirect('user/login/?success=' . $this->getRequest()->getRequestUri());
                }
            }
    
        }
    

    UserController

    class UserController extends Zend_Controller_Action {
    
        public function loginAction() {
                $returnURL = $this->_getParam('success', '/');
                if (checkAuthenticity()) {
                    $this->_redirect($returnURL);
                }
          }
    }