zoukankan      html  css  js  c++  java
  • 安全与表单验证

     回顾

    在我们第五天的学习中,我们已经习惯于操作模板与动作:表单与分页对于我们而言已不在神秘。但是在构建登陆表单之后,我们也许希望演示一下如何限制非授权用户对特定功能的访问。这就是我们今天所要学习的内容,以及一些表单验证的内容。因为我们要使用自定义的类来扩展程序,所以我们会对Symfony一书的自定义扩展一节的内容有更深的理解。

    登陆表单验证

    验证规则

    登陆表单有一个nickname与password域。但是如果用户提交了不正确的数据时会发生什么情况呢?为了能够处理这种情况,在/frontend/modules/user/validate目录下(login是要验证的动作名)创建一个login.yml文件,并且添加下面的内容:

    methods:
      post: [nickname, password]

    names:
      nickname:
        required:     true
        required_msg: your nickname is required
        validators:   nicknameValidator

      password:
        required:     true
        required_msg: your password is required

    nicknameValidator:
        class:        sfStringValidator
        param:
          min:        5
          min_error:  nickname must be 5 or more characters

    首先,在methods头下,定义了表单方法要进行验证的域列表(在这里我们只定义了POST方法,因为GET方法用于显示登陆表单而不需要验证)。然后,在names头下,列出了要检测的每一个表单域的需求,同时列出了相应的错误信息。实际上,因为nickname域声明为具有一个特殊的验证规则集合,所以在相应的头部下进行详细描述。在这个例子中,sfStringValidator是一个Symfony内建的验证器,用来检测一个字符串的格式(默认的Symfony验证器在Symfony一书的如何验证表单一节进行详细描述)。

    错误处理

    那么当用户输入错误的数据时会发生什么呢?如果并不满足login.yml文件中所写的条件,那么Symfony控制器就会将这个请求传递给userActions类的handleErrorLogin()方法,而不是form_tag参数中所设计的executeLogin()方法。如果这个方法不存在,默认行为则会显示loginError.php模板。这是因为默认的handleError()方法返回:

    public function handleError()
    {
      return sfView::ERROR;
    }

    这是要编写的一个全新的模板。但是我们更希望重新显示登陆表单,并且将错误信息显示在相关的表单域附近。所以我们要修改显示的登陆错误行为,在我们这个例子中,loginSuccess.php模板为:

    public function handleErrorLogin()
    {
      return sfView::SUCCESS;
    }

    模板错误帮助器

    一旦再次调用loginSuccess.php模板,则会显示错误。我们将会使用验证帮助器组的form_error()帮助器。将模板的两个form-row div层改为下面的内容:

    <?php use_helper('Validation') ?>
     
    <div class="form-row">
      <?php echo form_error('nickname') ?>
      <label for="nickname">nickname:</label>
      <?php echo input_tag('nickname', $sf_params->get('nickname')) ?>
    </div>
     
    <div class="form-row">
      <?php echo form_error('password') ?>
      <label for="password">password:</label>
      <?php echo input_password_tag('password') ?>
    </div>

    如果发生错误,form_error()帮助器就会输出在login.yml文件中定义的错误。现在我们可以来测试一下表单验证了,我们可以输入一个小于5个字符的nickname,或是留空两个表单域来进行相应的测试。

    现在密码是必须的,但是在数据中并没有密码。这并没有关系,只要我们输入密码,登陆就会成功。这并不是一个安全的过程,不是吗?

    格式化错误

    如果我们测试表单并且发生了错误,我们也许会注意到我们的错误信息格式并不是如上图所示的样子。这是因为我们定义了.form_error类格式,这是由form_error()帮助器所产生的表单错误的默认类:

    .form_error
    {
      padding-left: 85px;
      color: #d8732f;
    }

    授权用户

    自定义验证器

    我们是否还记得昨天在登陆动作中对所输入的用户是否存在的检测?是的,那看起来就是一个表单验证。这段代码应从这个动作中移出,并且包含在一个自定义验证器中。我们会认为这很复杂?事实上一点都不。编辑login.yml验证文件如下:

    ...
    names:
      nickname:
        required:      true
        required_msg:  your nickname is required
        validators:    [nicknameValidator, userValidator]
    ...
    userValidator:
        class:         myLoginValidator
        param:
          password:    password
          login_error: this account does not exist or you entered a wrong password

    我们只是为nickname域添加了一个新的验证器,myLoginValidator。这个验证器还不存在,但是我们知道对于完全授权的用户是需要密码的,所以他使用标签password作为参数传递。

    密码存储

    但是我们需要停留一下。在我们的数据模型以及测试数据中,并没有密码集合。现在是确定一个的时候了。但是我们知道从安全的角度来说,将密码以明文的形式存储在文本以及数据中是一个糟糕的主意。所以我们会使用随机值对必码进行哈希处理,并且存储密码的sha1哈希值。

    所以我们要打开schema.xml文件,并且在User表中添加下面的列:

    <column name="email" type="varchar" size="100" />
    <column name="sha1_password" type="varchar" size="40" />
    <column name="salt" type="varchar" size="32" />

    使用symfony propel-build-model命令重新构建Propel模块。我们同时也应将这两列添加到数据库中,可以手动或者是使用symfony propel-build-sql命令后生成的lib.model.schema.sql。现在打开askeet/lib/model/User.php文件,并且添加下面的setPassword()方法:

    public function setPassword($password)
    {
      $salt = md5(rand(100000, 999999).$this->getNickname().$this->getEmail());
      $this->setSalt($salt);
      $this->setSha1Password(sha1($salt.$password));
    }

    这个函数模拟一个直接的密码存储,但是所不同的是他存储的是随机键(一个32位的哈希化的随机字符串)与哈希化的密码(一个40位的字符串)。

    在测试数据中添加密码

    还记得第三天的测试数据文件吗?现在需要向测试用户中添加一个密码与一个email。打开并修改askeet/data/fixtures/test_data.yml文件如下:

    User:
      ...
      fabien:
        nickname:   fabpot
        first_name: Fabien
        last_name:  Potencier
        password:   symfony
        email:      fp@example.com

      francois:
        nickname:   francoisz
        first_name: François
        last_name:  Zaninotto
        password:   adventcal
        email:      fz@example.com

    因为我们已经为user类定义了setPassword()方法,所以当我们调用下面的命令时sfPropelData对象将会正确的处理sha1_password与salt列:

    $ php batch/load_data.php

    自定义验证器

    现在需要编写我们自已的自定义myLoginValidator了。我们可以在模块可以访问的任何lib/目录下创建(也就是说在askeet/lib/,或是askeet/apps/frontend/lib/,或是askeet/apps/frontend/modules/user/lib/下)。就目前而言,我们认为这是一个程序相关的验证器,所以我们会在askeet/apps/frontend/lib/目录下创建myLoginValidator.class.php文件。

    <?php
     
    class myLoginValidator extends sfValidator
    {    
      public function initialize($context, $parameters = null)
      {
        // initialize parent
        parent::initialize($context);
     
        // set defaults
        $this->setParameter('login_error', 'Invalid input');
     
        $this->getParameterHolder()->add($parameters);
     
        return true;
      }
     
      public function execute(&$value, &$error)
      {
        $password_param = $this->getParameter('password');
        $password = $this->getContext()->getRequest()->getParameter($password_param);
     
        $login = $value;
     
        // anonymous is not a real user
        if ($login == 'anonymous')
        {
          $error = $this->getParameter('login_error');
          return false;
        }
     
        $c = new Criteria();
        $c->add(UserPeer::NICKNAME, $login);
        $user = UserPeer::doSelectOne($c);
     
        // nickname exists?
        if ($user)
        {
          // password is OK?
          if (sha1($user->getSalt().$password) == $user->getSha1Password())
          {
            $this->getContext()->getUser()->setAuthenticated(true);
            $this->getContext()->getUser()->addCredential('subscriber');
     
            $this->getContext()->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');
            $this->getContext()->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');
     
            return true;
          }
        }
     
        $error = $this->getParameter('login_error');
        return false;
      }
    }

    当验证器被调用时--在登陆表单提交之后--initialize()方法会被首先调用。他会被始化login_error信息的默认值('Invalid input'),并且将参数(login.yml文件中param:头部下的部分)组合到一个参数保持对象中。

    然后execute()方法会被执行。$password_param是在login.yml文件中password头下面提供的。他用作一个由请求参数中获取值的域名字。所以$password中包含用户所输入的密码。$value为当前域的值--而myLoginValidator类是为被nickname域所调用的。所以$login包含用户所输入的用户名。最后,这个验证器包含验证一个用户所必须的所有数据了。

    下面的代码会由登陆动作中移除。但是,密码的验证测试还没有实现:用户输入密码的哈希值与用户的哈希密码进行对比。

    如果登陆名与密码是正确的,验证器就会返回真,而表单的目标动作(executeLogin())就会执行。否则,他会返回假,而handleErrorLogin()会被执行。

    由动作中移除代码

    现在所有的验证代码都位于验证器中了,我们需要将其从登陆动作中移除。确实,当使用POST方法调用动作时,这就意味着验证器验证这个请求,所以用户是正确的。这就意味着在这个例子中动作所需要做的唯一的事情就是重定向到referer页面:

    public function executeLogin()
    {
      if ($this->getRequest()->getMethod() != sfRequest::POST)
      {
        // display the form
        $this->getRequest()->getParameterHolder()->set('referer', $this->getRequest()->getReferer());
     
        return sfView::SUCCESS;
      }
      else
      {
        // handle the form submission
        // redirect to last page
        return $this->redirect($this->getRequestParameter('referer', '@homepage'));
      }
    }

    现在我们可以使用测试用户进行登陆来测试修改(在清除缓存之后,因为我们创建了一个新需要自动装入的验证器)。

    限制访问

    如果我们需要限制到一个动作的访问,我们只需要在模块config/目录下添加一个security.yml文件,如下所示(但是现在先不要做):

    all:
      is_secure:   on
      credentials: subscriber

    这要只有当用户被授权时,这个模块的动作才会被执行。

    在askeet中,只有当发表一个新问题,声明对一个问题的兴趣或者是评价时才需要登陆。而所有其他的动作都会对非登陆用户开放。

    所以要限制对question/add动作的访问,在askeet/apps/frontend/modules/question/config/目录下添加下面的security.yml文件:

    add:
      is_secure:   on
      credentials: subscriber

    all:
      is_secure:   off

    重构

    当验证密码为用户分配权限时执行了四行代码。我们可以将其看作myUser类的一个方法(会话类,而不是与User列相关的User类)。这很容易做到。将下面的代码添加到askeet/apps/frontend/lib/myUser.php类中:

    public function signIn($user)
    {
      $this->setAttribute('subscriber_id', $user->getId(), 'subscriber');
      $this->setAuthenticated(true);
     
      $this->addCredential('subscriber');
      $this->setAttribute('nickname', $user->getNickname(), 'subscriber');
    }
     
    public function signOut()
    {
      $this->getAttributeHolder()->removeNamespace('subscriber');
     
      $this->setAuthenticated(false);
      $this->clearCredentials();
    }

    现在将myLoginValidator类中由$this->getContext()->getUser()启动的四行代码改为:

    $this->getContext()->getUser()->signIn($user);

    同时将user/logout动作代码改为:

    public function executeLogout()
    {
      $this->getUser()->signOut();
     
      $this->redirect('@homepage');
    }

    subscriber_id与nickname会话属性同时也可以通过一个getter方法来进行抽象。仍然是在myUser类中,添加下面三个方法:

    public function getSubscriberId()
    {
      return $this->getAttribute('subscriber_id', '', 'subscriber');
    }
     
    public function getSubscriber()
    {
      return UserPeer::retrieveByPk($this->getSubscriberId());
    }
     
    public function getNickname()
    {
      return $this->getAttribute('nickname', '', 'subscriber');
    }

    我们可以在layout.php文件中使用这些新方法,将下面的代码行:

    <li><?php echo link_to($sf_user->getAttribute('nickname', '', 'subscriber').' profile', 'user/profile') ?></li>

    替换为

    <li><?php echo link_to($sf_user->getNickname().' profile', 'user/profile') ?></li>

    不要忘记测试修改。

    明天见
  • 相关阅读:
    20131001国庆作业例2-10,2-11
    20131001国庆作业例2-7,2-8,2-9
    20131001国庆作业例2-4,2-5,2-6
    20131001国庆作业第二章例2-1,2-2,2-3
    20131001国庆作业第一章例1-1
    20130930C语言作业基础练习
    编程心得4
    编程心得3
    编程心得1
    714
  • 原文地址:https://www.cnblogs.com/dyllove98/p/2462010.html
Copyright © 2011-2022 走看看