*这一系列文章来源于Fabien Potencier,基于Symfony1.4编写的Jobeet Tutirual。
实现程序的安全性
应用程序的安全性是一个两步验证的过程,它的目标是阻止用户访问他/她不应该访问到的资源。实现安全性的第一步是认证(authentication),安全认证系统会取得用户提交的一些标识符,然后通过这个标识符分辨出用户的身份。一旦系统分辨出用户的身份之后,下一步就是系统对用户进行授权(authorization)操作,这一步将决定用户能够访问哪些给定的资源(系统会检查用户是否有权限进行这个操作)。我们可以通过配置app/config目录下的security.yml文件来对应用程序的安全组件进行配置。为了实现我们应用的安全性,我们修改scurity.yml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# app/config/security.yml security: role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false secured_area: pattern: ^/ anonymous: ~ form_login: login_path: /login check_path: /login_check default_target_path: ibw_jobeet_homepage access_control: - { path: ^/admin, roles: ROLE_ADMIN } providers: in_memory: memory: users: admin: { password: adminpass, roles: 'ROLE_ADMIN' } encoders: Symfony\Component\Security\Core\User\User: plaintext |
上面的配置会对网站的/admin部分(所有以/admin作为开头的url)进行安全性保护,并且只有ROLE_ADMIN角色的用户才能访问到/admin(可以查看security.yml中的access_controller部分)。在这个例子中,admin用户被定义在security.yml中(providers部分),而且admin用户的密码没有被编码(即没有经过加密算法处理过)(encoders部分)。对于用户认证,我们需要使用一个传统的登录表单,我们需要去实现它。首先我们需要创建两个路由:一个用来显示登录表单(例如:/login),另一个则用来处理提的交登录表单(例如:/login_check):
1 2 3 4 5 6 7 8 |
# src/Ibw/JobeetBundle/Resources/config/routing.yml login: pattern: /login defaults: { _controller: IbwJobeetBundle:Default:login } login_check: pattern: /login_check # ... |
我们不需要为/login_check URL实现一个控制器,因为防火墙(firewall)会自动捕捉并处理所有提交到/login_check URL的表单。我们需要做的就是创建一个路由(/login_check),我们会在登录表单的模板中使用它,这样才能把登录表单提交到/login_check。
下一步我们来创建显示登录表单的action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// src/Ibw/JobeetBundle/Controller/DefaultController.php namespace Ibw\JobeetBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\SecurityContext; class DefaultController extends Controller { // ... public function loginAction() { $request = $this->getRequest(); $session = $request->getSession(); // get the login error if there is one if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); $session->remove(SecurityContext::AUTHENTICATION_ERROR); } return $this->render('IbwJobeetBundle:Default:login.html.twig', array( // last username entered by the user 'last_username' => $session->get(SecurityContext::LAST_USERNAME), 'error' => $error, )); } } |
当用户提交了表单,安全系统会自动处理提交的表单。如果用户提交了一个无效的用户名或者密码,那么这个操作就会从系统中读取出提交表单的错误信息,并把这个错误信息反馈给用户。我们的工作仅仅只有显示出一个登录表单和可能出现的登录错误信息,而安全系统则会对用户提交上来的用户名和密码进行验证。
最后,我们来创建模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- src/Ibw/JobeetBundle/Resources/views/Default/login.html.twig --> {% if error %} <div>{{ error.message }}</div> {% endif %} <form action="{{ path('login_check') }}" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> <label for="password">Password:</label> <input type="password" id="password" name="_password" /> <button type="submit">login</button> </form> |
现在,如果我们去访问http://jobeet.local/app_dev.php/admin/dashboard URL,我们会看到一个登录表单,我们需要输入定义在security.yml中的用户名和密码(admin/adminpass)才能进入admin部分。
User Providers
在认证过程中,用户提交了一组登录凭证(通常是用户名和密码)。认证系统的工作就是在用户数据列表中逐个匹配登录凭证。那么用户数据列表从何而来呢?
在Symfony2中,用户数据列表可以存放在任何地方,一个配置文件,一个数据库表,一个web service接口,或者是其它你能想象得到的地方。为认证系统提供一个或者多个用户的类或接口就叫做“user provider”。Symfony2 自带了两种最常见的user provider:一种是使用配置文件,另一种是使用数据库表。
在上面的例子中,我们把用户列表存放在配置文件里:
1 2 3 4 5 6 7 8 9 10 |
# app/config/security.yml # ... providers: in_memory: memory: users: admin: { password: adminpass, roles: 'ROLE_ADMIN' } # ... |
但我们更多时候是需要把用户信息存放在数据表中。为了做到这点,我们需要在数据库中创建一个user表。首先我们来为user表创建orm文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# src/Ibw/JobeetBundle/Resources/config/doctrine/User.orm.yml Ibw\JobeetBundle\Entity\User: type: entity table: user id: id: type: integer generator: { strategy: AUTO } fields: username: type: string length: 255 password: type: string length: 255 |
现在运行命令生成User实体类:
1 |
php app/console doctrine:generate:entities IbwJobeetBundle |
然后更新数据库:
1 |
php app/console doctrine:schema:update --force |
这里需要为用户类(User)实现UserInterface接口,这就意味着“用户”可是任何的东西,只要它实现了UserInterface接口。打开并修改User.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
// src/Ibw/JobeetBundle/Entity/User.php namespace Ibw\JobeetBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Doctrine\ORM\Mapping as ORM; /** * User */ class User implements UserInterface { /** * @var integer */ private $id; /** * @var string */ private $username; /** * @var string */ private $password; /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set username * * @param string $username * @return User */ public function setUsername($username) { $this->username = $username; } /** * Get username * * @return string */ public function getUsername() { return $this->username; } /** * Set password * * @param string $password * @return User */ public function setPassword($password) { $this->password = $password; } /** * Get password * * @return string */ public function getPassword() { return $this->password; } public function getRoles() { return array('ROLE_ADMIN'); } public function getSalt() { return null; } public function eraseCredentials() { } public function equals(User $user) { return $user->getUsername() == $this->getUsername(); } } |
我们需要为User类实现UserInterface接口的抽象方法:getRoles,getSalt,eraseCredentials和equals。
接下来配置一个user provider实体,它指向User类:
1 2 3 4 5 6 7 8 9 |
# app/config/security.yml ... providers: main: entity: { class: Ibw\JobeetBundle\Entity\User, property: username } encoders: Ibw\JobeetBundle\Entity\User: sha512 |
我们同样改变了encoder部分,这样就能够使用sha512算法对User类的password属性进行加密了。
所有准备都已经做好了,现在我们需要先创建一个用户。为了做到这点,我们会创建一个新的Symfony命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// src/Ibw/JobeetBundle/Command/JobeetUsersCommand.php namespace Ibw\JobeetBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Ibw\JobeetBundle\Entity\User; class JobeetUsersCommand extends ContainerAwareCommand { protected function configure() { $this ->setName('ibw:jobeet:users') ->setDescription('Add Jobeet users') ->addArgument('username', InputArgument::REQUIRED, 'The username') ->addArgument('password', InputArgument::REQUIRED, 'The password') ; } protected function execute(InputInterface $input, OutputInterface $output) { $username = $input->getArgument('username'); $password = $input->getArgument('password'); $em = $this->getContainer()->get('doctrine')->getManager(); $user = new User(); $user->setUsername($username); // encode the password $factory = $this->getContainer()->get('security.encoder_factory'); $encoder = $factory->getEncoder($user); $encodedPassword = $encoder->encodePassword($password, $user->getSalt()); $user->setPassword($encodedPassword); $em->persist($user); $em->flush(); $output->writeln(sprintf('Added %s user with password %s', $username, $password)); } } |
使用命令创建第一个用户:
1 |
php app/console ibw:jobeet:users admin admin |
上面的命令会创建一个用户名和密码都是“admin”的用户。你可以使用它登录并访问到admin部分。
登出(Logout)
防火墙(firewall)能够自动处理登出。我们需要做的就是激活logout配置参数:
1 2 3 4 5 6 7 8 9 10 |
# app/config/security.yml security: firewalls: # ... secured_area: # ... logout: path: /logout target: / # ... |
我们不需要为/logout URL实现控制器,防火墙(firewall)会自行处理。我们来创建/logout路由:
1 2 3 4 5 6 7 |
# src/Ibw/JobeetBundle/Resources/config/routing.yml # ... logout: pattern: /logout # ... |
一旦配置完成了,访问/logout后,当前用户就会被注销认证(un-authenticate)了。然后用户会被重定向到首页(这个值定义在target参数中)。
剩下没有做的事情就是为admin部分添加logout链接。我们会重载SonataAdminBundle的user_block.html.twig。在app/Resources/SonataAdminBundle/views/Core目录下创建user_block.html.twig文件:
1 2 |
<!-- app/Resources/SonataAdminBundle/views/Core/user_block.html.twig --> {% block user_block %}<a href="{{ path('logout') }}">Logout</a>{% endblock%} |
现在,当我们访问admin部分(请先清除缓存)时,我们会被要求填写用户名和密码,进入之后我们可以在右上角看到一个logout链接。
用户Session
Symfony2提供了一个非常好用的session对象,用户的每个不同请求都可以访问到它里面保存的信息。Symfony2默认是使用原生的PHP session把信息保存到一个cookie中。
我们可以在控制器中方便地取出session里保存的信息:
1 2 3 4 5 6 7 |
$session = $this->getRequest()->getSession(); // store an attribute for reuse during a later user request $session->set('foo', 'bar'); // in another controller for another request $foo = $session->get('foo'); |
可惜的是,在我们Jobeet的用户stories中(第二天的内容),并没有需要在用户session中保存信息的需求。所以我们来改改需求:为了方便Job信息的浏览,我们会在菜单栏上显示用户刚刚浏览过的三条Job信息的链接。
当用户访问一条Job页面时,那么这条Job信息对应的Job对象就会被添加到用户的浏览历史中,并且把它保存到session中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// src/Ibw/JobeetBundle/Controller/JobController.php // ... public function showAction($id) { $em = $this->getDoctrine()->getManager(); $entity = $em->getRepository('IbwJobeetBundle:Job')->getActiveJob($id); if (!$entity) { throw $this->createNotFoundException('Unable to find Job entity.'); } $session = $this->getRequest()->getSession(); // fetch jobs already stored in the job history $jobs = $session->get('job_history', array()); // store the job as an array so we can put it in the session and avoid entity serialize errors $job = array('id' => $entity->getId(), 'position' =>$entity->getPosition(), 'company' => $entity->getCompany(), 'companyslug' => $entity->getCompanySlug(), 'locationslug' => $entity->getLocationSlug(), 'positionslug' => $entity->getPositionSlug()); if (!in_array($job, $jobs)) { // add the current job at the beginning of the array array_unshift($jobs, $job); // store the new job history back into the session $session->set('job_history', array_slice($jobs, 0, 3)); } $deleteForm = $this->createDeleteForm($id); return $this->render('IbwJobeetBundle:Job:show.html.twig', array( 'entity' => $entity, 'delete_form' => $deleteForm->createView(), )); } |
在layout文件的#content div标签之前添加下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --> <!-- ... --> <div id="job_history"> Recent viewed jobs: <ul> {% for job in app.session.get('job_history') %} <li> <a href="{{ path('ibw_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">{{ job.position }} - {{ job.company }}</a> </li> {% endfor %} </ul> </div> <div id="content"> ... </div> <!-- ... --> |
flash信息
flash信息是存放在用户session中的简短信息(通常是用来作为反馈给用户的信息),它通常用在下一个请求中显示信息。它对表单很有用:当你想要重定向并请求一个新页面的同时,显示出一些指定的信息。我们已经在项目的发布Job信息的需求中使用过flash信息了:
1 2 3 4 5 6 7 8 9 10 11 |
// src/Ibw/JobeetBundle/Controller/JobController.php // ... public function publishAction($token) { // ... $this->get('session')->getFlashBag()->add('notice', 'Your job is now online for 30 days.'); // ... } |
getFlashBag()->add()函数的第一个参数是用来标志flash的键,第二个参数是需要显示的信息。我们可以为flash定义任何键名,但notic和error是最常用的两个。
为了显示flash信息,我们需要在模板中把它们包含进来。我们已经在layout.html.twig中做过了:
1 2 3 4 5 6 7 8 9 10 |
<!-- src/Ibw/JobeetBundle/Resources/views/layout.html.twig --> <!-- ... --> {% for flashMessage in app.session.flashbag.get('notice') %} <div> {{ flassMessage }} </div> {% endfor %} <!-- ... --> |
原文链接:http://www.intelligentbee.com/blog/2013/08/19/symfony2-jobeet-day-13-security/