[原创中文翻译]symfony askeet24:第六天,安全性和表单验证。

October 3, 2007 – 12:52 am

[欢迎转载,转载请注名出处http://symfony.net.cn。本文英文版权归symfony官方网站所有]

Login form validation

login表单验证

Validation file

验证文件

The login form has a nickname and a password field. But what will happen if a user submits incorrect data? To be able to handle this case, create a login.yml file in the /frontend/modules/user/validate directory (login is the name of the action to validate). Add the following content:

登录表单有用户名和密码输入框。但是如果用户输入错误数据怎么办呢?为了处理这种情况,在/frontend/module/user/validate下创建login.yml文件(login命名是验证对应action)。加上下面内容:

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

First, under the methods header, the list of fields to be validated is defined for the methods of the form (we only define POST method here because the GET is to display the login form and does not need validation). Then, under the names header, the requirements for each of the fields to be checked are listed, along with the corresponding error message. Eventually, as the ‘nickname’ field is declared to have a specific set of validation rules, they are detailed under the corresponding header. In this example, the sfStringValidator is a symfony built-in validator that checks the format of a string (the default symfony validators are exposed in the how to validate a form of the symfony book).

首先,在method下面写的是需要验证的表单对象[nickname, password](我们这里仅定义POST方法,因为GET被用来显示login表单,不需要验证)。接下来,在name下写的是列出每一个输入框的检查标准信息,包含提示信息。幸运的是,“nickname”输入有一套详细的验证规则,下面写得很详细。在这个例子里,sfStringValidator是symfony的内建验证,用来检查字符串的格式(默认的symfony验证如何验证表单在symfony book里有详细描述)。

Error handling

错误处理机制

So what is supposed to happen if a user enters wrong data? The conditions written in the login.yml file will not be met, and the symfony controller will pass the request to the handleErrorLogin() method of the userActions class - instead of the executeLogin() method, as planned in the form_tag argument. If this method doesn’t exist, the default behaviour is to display the loginError.php template. That’s because the default handleError() method returns:

如果用户输入错误数据,应该怎么处理呢?如果用户输入的错误数据不在login.yml的错误标准范围内,symfony控制层会把请求发送到userAction类的handleErrorLogin()方法——代替executeLogin()函数,正如form_tag参数计划。如果这个方法不存在,默认的行为是显示loginError.php模板。这是因为默认的handleError()方法返回,以下代码加到user/action/action.class.php里:

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

That’s a whole new template to write. But we’d rather display the login form again, with the error messages displayed close to the problematic fields. So let’s modify the login error behaviour to display, in this case, the loginSuccess.php template:

有一个新模板需要写。但是我们最好再次显示login表单,错误信息显示在距离有问题输入框很近的地方。修改一下login错误行为来显示,例如,loginSuccess.php模板,以下代码加到user/action/action.class.php里:

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

The naming conventions that link the action name, its return value and the template file name are exposed in the view chapter of the symfony book.

更多细节请见symfony book。

Template error helpers

模板错误helpers

Once the loginSuccess.php template is called again, it is time to display the errors. We will use the form_error() helper of the Validation helper group for that purpose. Change the two form-row divs of the template to:

一旦loginSuccess.php模板被再次调用,就该显示错误了。我们会使用Validation helper组的form_error() helper来完成此目的。改变模板的两个form-row层:

<?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>

The form_error() helper will output the error message defined in the login.yml if an error is declared in the field given as a parameter.

如果错误以参数形式在login.yml中被定义过,form_error() helper会输出这些错误信息。

It is time to test the form validation by trying to enter a nickname of less than 5 characters, or by omitting one the two fields. The error messages magically display above the concerned fields:

现在输入少于5个字符的用户名来测试一下表单验证,或者少填一个输入框。错误信息像变魔术般显示在相关输入框上面:

http://www.symfony-project.com/images/askeet/1_0/login_form_error.gif

The password is now compulsory, but there is no password in the database! That doesn’t matter, as soon as you enter any password, the login will be successful. That’s not a very secure process, is it?

密码现在必须输入,但是我们的数据库里没密码啊!不要紧,你随便输入密码就能登录上。这不够安全标准,对不?

Style errors

样式错误

If you tested the form and got an error, you probably noticed that your errors are not styled the same way as the ones of the capture above. That’s because we defined the styling of the .form_error class (in web/main.css), which is the default class of the form errors generated by the form_error() helper:

如果你测试表单得到错误提示,注意错误提示的样式和上面图片的不一样。这是因为我们把样式定义在.form_error类(web/css/main.css),默认的form errors由form_error() helper生成:

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

Authenticate a user

权限用户

Custom validator

自定义验证

Do you remember yesterday’s check about the existence of an entered nickname in the login action? Well, that sounds like a form validation. This code should be taken out from the action and included into a custom validator. You think it is complicated? It really isn’t. Edit the login.yml validation file as follows:

你还记得昨天关于验证用户名输入的login动作吗?好了,听起来像个表单验证。代码应该被从动作里拿出来,封装在一个自定义验证中。你认为这是复杂的吗?事实上不是。按照下面来修改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

We just added a new validator for the nickname field, of class myLoginValidator. This validator doesn’t exist yet, but we know that it will need the password to fully authenticate the user, so it is passed as a parameter with the label password.

我们用myLoginValidator类来增加新的nickname验证。这个验证还不存在,但我们知道它将需要密码来完全验证用户,参数传递密码。

Password storage

密码储存

But wait a minute. In our data model, as well as in the test data, there is no password set. It is time to define one. But you know that storing a password in clear text, in a database, is a bad idea for security reasons. So we will store a sha1 hash of the password as well as the random key used to hash it. If you are not familiar with this ’salt’ process, check out the password cracking practices.

等等。在我们的数据模型里,也在测试数据里,是没有密码设置的。现在我们来定义一个。但是在数据库里明文保存密码,从安全性来讲不好。所以我们使用哈希随机码来保存密码。如果你不熟悉“salt”过程,看看密码方面的知识。

So open the schema.xml and add the following columns to the User table:

打开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" />

Rebuild the Propel model by a symfony propel-build-model. You should also add the two columns to the database, either manually or by using the schema.sql generated after a symfony propel-build-sql. Now open the askeet/lib/model/User.php and add this setPassword() method:

用symfony propel-build-model重建Propel model。数据库里也需要加字段,要么手动,要么用schema.sql(symfony propel-build-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));
}

This function simulates a direct password storage, but instead it stores the salt random key (a 32 characters hashed random string) and the hashed password (a 40 characters string).

这个方法模拟直接密码存储,但是存储的是salt随机码(一种32位哈希随机字符串)和哈希密码(一种40位字符串)。

Add password in the test data

测试数据中加入密码

Remember the day three test data file? It is time to add a password and an email to the test users. Open and modify the askeet/data/fixtures/test_data.yml as follows:

还记得第三天的测试数据吗?该给测试用户填加密码和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

As the setPassword() method was defined for the User class, the sfPropelData object will correctly populate the new sha1_password and salt columns defined in the schema when we call:

正如给User类定义的setPassword()方法,当我们调用时sfPropelData对象导入测试数据时——new sha1_password和salt columns时,自动进行哈希转化,这一切都定义在schema中:

$ php batch/load_data.php

Notice that the sfPropelData object is able to deal with methods that are not bind to ‘real’ database column (and now we overtake your traditional SQL dump!).

注意sfPropelData对象有能力处理那些没有和“真实”数据库没有关联的方法(现在我们代替传统的sql语句)。

If you wonder how this is possible, take a look at the database population chapter of the symfony book.

如果你想知道怎么实现的,看看symfony book数据库章节。

There is no need to define a password for the ‘Anonymous Coward’ user since we will forbid him to login. And we would really appreciate that you didn’t try the two passwords given here on our bank accounts, since they are confidential!

没有必要给匿名用户定义密码,因为我们将不允许他登录。我们也会实现用户不能在帐户上用两个密码,就好比是银行账户,因为他们是保密的。

Custom validator

自定义验证

Now it is time to write this custom myLoginValidator. You can create it in anyone of the lib/ directories that are accessible to the module (that is, in askeet/lib/, or in askeet/apps/frontend/lib/, or in askeet/apps/frontend/modules/user/lib/). For now, it is considered to be an application-wide validator, so the myLoginValidator.class.php will be created in the askeet/apps/frontend/lib/ directory:

是时候写自定义myLoginValidator验证了。你可以把它创建在任何lib/目录下,任何和module相关的(也就是,在askeet/lib/下,或者在askeet/apps/frontend/lib/下,或者在askeet/apps/frontend/modules/user/lib/下)。现在,它被认为是一个可以广泛使用的验证器,所以myLoginValidator.class.php将被创建在askeet/apps/frontend/lib/下:

[译者:按照symfony的规则,在lib目录下只要命名加上class,就会自动被调用。]

<?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;
}
}

When the validator is required - after the login form submission - the initialize() method is called first. It initiates the default value of the login_error message (’Invalid Input’) and merges the parameters (the ones under the param: header in the login.yml file) into the parameter holder object.

当验证被请求——在login表单提交后——首先调用initialize()方法。初始化login_error信息默认值(“无效输入”)和合并参数(在param下的:login.yml文件中)到parameter holder对象里。

Then the execute() method is… executed. The $password_param is the field name provided in the login.yml under the password header. It is used as a field name to retrieve a value from the request parameters. So $password contains the password entered by the user. $value takes the value of the current field - and the myLoginValidator class is called for the nickname field. So $login contains the nickname entered by the user. At last! Now the validator has all the necessary data to actually validate the user.

execute()方法是… …执行。$password_param是写在login.yml里的参数。它被当作一个参数名来取回请求参数的值。所以$password包含了用户输入的密码。$value取到当前参数的值——myLoginValidator类被调用来验证用户名参数。所以$login包含了用户输入的用户名。最后!现在validator有所有必须的数据来真实验证用户了。

The following code was taken off the login action. But in addition, the test of the password validity (previously always true) is implemented: A hash of the password entered by the user (using the salt stored in the database) is compared to the hashed password of the user.

随后的代码被从login动作中去掉。另外,密码验证的测试(以前总是true)被执行:用户的哈希密码(数据库里用salt存储)被用来和用户输入后被哈西处理的密码比较。

If the login and the password are correct, the validator returns true and the target action of the form (executeLogin()) will be executed. If not, it returns false and it’s the handleErrorLogin() that will be executed.

如果密码正确,验证返回true而且表单的目标动作(executeLogin())会执行。如果不是,返回false而且handleErrorLogin()会执行。

Remove the code from the action

从动作中删除些代码

Now that all the validation code is located inside the validator, we need to remove it from the login action. Indeed, when the action is called with the POST method, it means that the validator validated the request, so the user is correct. It means that the only thing that the action has to do in this case is to redirect to the referer page:

现在所有的验证代码写在validator里,我们需要从login动作里删除,当用POST方式调用动作时,validator验证请求。代表着action唯一可以做的是重定向到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’));
}
}

Test the modifications by trying to login with one of the test users (after clearing the cache, since we created a new validator class that needs to be autoloaded).

用测试数据里的用户名来测试一下修改结果(先清缓存,因为我们创建一个新validator类需要重新载入)。

Restrict access

限制权限部分

If you want to restrict access to an action, you just need to add a security.yml in the module config/ directory, like the following (don’t do it for now):

如果你想限制访问action,需要加个文件security.yml在module的config/,就像下面写的(现在先别弄):

all:
is_secure: on
credentials: subscriber

The actions of such a module will only be executed if the user is authenticated, and a has subscriber credential.

如果用户是被验证的,模块的动作将被执行,还有用户的证书。

In askeet, login will be required to post a new question, to declare interest about a question, and to rate a comment. All the other actions will be open to non logged users.

在askeet里,登录后被要求发布一个新问题,发表对哪些问题感兴趣和发表评论。其他所有的action都将对没有登录的用户开放。

So to restrict the access of the question/add action (yet to be written), add the following security.yml file in the askeet/apps/frontend/modules/question/config/ directory:

那么限制访问question/add动作(已经写好了),增加security.yml文件在askeet/apps/frontend/modules/question/config/下:

add:
is_secure: on
credentials: subscriber
#
all:
is_secure: off

How about a bit of refactoring?

来整理一下代码?

The day is almost finished, but we would like to play our favorite game for a little while: The move-the-code-to-an-unlikely-place game.

今天快结束了,但是我们还需要玩点小游戏:The move-the-code-to-an-unlikely-place game。

The four lines of code that are executed when the password is validated grant access to the user and save his id for future requests. You could see it as a method of the myUser class (the session class, not the User class corresponding to the User column). That’s easy to do. Add the following methods to the askeet/apps/frontend/lib/myUser.php class:

用户密码被验证成功并保存用户的id以备未来使用,这四行代码就能够实现。你可以把它当成myUser类的方法(session类,用session实现,不是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();
}

Now, change the four lines starting by $this->getContext()->getUser() in the myLoginValidator class with:

现在,把以myLoginValidator类中从$this->getContext()->getUser()开始的四行删除掉,用下面这一行代替即可:

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

And also change the user/logout action (did you forget about this one?) by:

也改改user/logout动作(没忘记这个吧)为:

public function executeLogout()
{
$this->getUser()->signOut();

$this->redirect(‘@homepage’);
}

The subscriber_id and nickname session attributes could also be abstracted through a getter method. Still in the myUser class, add the three following methods:

subscriber_id和nickname session属性也会被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');
}

You can use one of these new methods in the layout.php: change the line

你可以用新方法中任意一个用在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>

Don’t forget to test the modifications. The same login process as previously should still work - but now with better code.

别忘了对修改内容进行测试。先前的登录功能仍然好用——但不是最佳代码。

  1. 2 Responses to “[原创中文翻译]symfony askeet24:第六天,安全性和表单验证。”

  2. 你好,当我完成第6天的学习后,觉得新加在ask_user表中的sha1_password与salt字段的值不正确

    就是用完php batch/load_data.php这个命令后,我的sha1_password里面的值是有了,不过都是明码,并且salt字段里面的值都是NULL,不知道为什么?希望得到解答,谢谢!

    By Chester on Apr 4, 2009

  3. 先看看这个,

    As the setPassword() method was defined for the User class, the sfPropelData object will correctly populate the new sha1_password and salt columns defined in the schema when we call:

    正如给User类定义的setPassword()方法,当我们调用时sfPropelData对象导入测试数据时——new sha1_password和salt columns时,自动进行哈希转化,这一切都定义在schema中

    你的导入程序没有实现加密功能,再尝试一下。

    By hluan on Apr 7, 2009

Post a Comment