安全是个两步过程,它的目的就是防止用户访问他/她无权访问的资源。

过程的第一步,安全系统识别用户是谁,要求该用户提供一些有序的验证。它被称为认证,它的意思是系统尝试发现你是谁。

一旦系统知道你是谁之后,下一步就是决定你是否可以访问指定的资源。过程的这一部分称为授权,它的意思是系统正在检查,看看你是否有执行某些动作的权利。

Symfony2Book11:安全_Symfony2

因为最好的学习方法是看例子,让我们来看看:

Symfony2的安全组件作为一个独立的PHP库可以用于任何PHP项目中。

基础示例:HTTP认证

安全组件可以通过你的应用程序配置来配置。实际上,最标准的安全设置只是使用了正常的配置。下列配置告诉Symfony2对于任何匹配/admin/*的URL都要求安全,要使用基本HTTP认证询问用户证书(如老派的用户名/密码对话框)

  1. # app/config/config.yml 
  2. security: 
  3.     firewalls: 
  4.         secured_area: 
  5.             pattern:    ^/ 
  6.             anonymous: ~ 
  7.             http_basic: 
  8.                 realm: "Secured Demo Area" 
  9.  
  10.     access_control: 
  11.         - { path: ^/admin, roles: ROLE_ADMIN } 
  12.  
  13.     providers: 
  14.         in_memory: 
  15.             users: 
  16.                 ryan:  { password: ryanpass, roles: 'ROLE_USER' } 
  17.                 admin: { password: kitten, roles: 'ROLE_ADMIN' } 
  18.  
  19.     encoders: 
  20.         Symfony\Component\Security\Core\User\User: plaintext 

一个标准的Symfony2发布将安全配置单独放入单个文件(如:app/config/security.yml)。如果你没有单独的安全文件,你可以直接将配置放入你的主配置文件中(如app/config/config.yml)。

该配置的最终结果是全功能的安全系统,如下所示:

  • 系统中有两个用户 (ryan和admin);
  • 用户通过基本HTTP认证来认证他们自己;
  • 任何匹配/admin/*的URL都是安全的,只有admin用户可以访问它;
  • 任何不匹配/admin/*的URL可以被所有用户访问(永远不会提示用户登录)。

让我们简要地看看安全是如何工作的,以及配置的每一部分是如何一起工作的。

安全是如何工作:认证和授权

Symfony2安全系统的工作就是确定用户是谁(认证)然后检查看看用户是否有访问特定资源或URL的权限。

防火墙(认证)

当一个用户将一个请求发向一个被防火墙保护的URL时,安全系统就被激活了。防火墙的工作就是要确保用户是不是需要被认证,如果需要的话,发送一个响应返回给用户去启动认证过程。

当传入请求的URL匹配防火墙的正则表达式模式时,防火墙被激活。在本例中,模式(^/)将匹配任何传入的请求。然而,其实防火墙被激活也没有意义,任何URL都不会使HTTP认证用户名和密码对话框出现。例如,任何用户都可以访问/foo,而不会提示要去认证。

Symfony2Book11:安全_安全_02

之所以这样首先是因为通过匿名配置参数,防火墙允许匿名用户通过。换句话说,防火墙不要求用户立刻进行认证。其次是因为没有特定的规则需要访问/foo(下面的access_control节),请求甚至可以在没有要求用户认证的情况下完成。

如果你删除了匿名关键词,那么防火墙总是要求用户立即认证的。

访问控制(授权)

然而如果用户要求/admin/foo,那么处理过程是不同的。这是因为access_control配置段说任何匹配正则表达式^/admin(如/admin或任何匹配/admin/*的URL)都需要ROLE_ADMIN角色。角色是大多数授权的基础:如果用户拥有ROLE_ADMIN角色,用户可以访问/admin/foo。

Symfony2Book11:安全_Security_03

象前面一样,当用户最初发送请求时,防火墙并不要求任何身份。然而当访问控制层拒绝用户访问(因为匿名用户没有ROLE_ADMIN用户)时,防火墙接管该过程,并启动认证进程。认证依赖你所用的认证机制。例如,如果你使用表单登录认证方式,用户将被重定向到登录页面。如果你使用的是HTTP认证,用户将被发送HTTP的401响应,以便用户可以看到用户名密码对话框。

用户现在有机会提交它的证书给应用程序。如果证书是有效的,那么原始请求会被重发。

Symfony2Book11:安全_Symfony2Book_04

在本例中,用户ryan成功通过防火墙认证。但因为ryan没有拥有ROLE_ADMIN角色,它仍然被拒绝访问/admin/foo。最终这意味着用户将看到消息说明访问被拒绝。

当Symfony2拒绝用户访问时,用户可以看到一个错误页面并收到一个HTTP的403状态码(Forbidden)。你可以根据食谱(cookbook)错误页面中的自定义403错误页内容来自定义拒绝访问页。

最后,如果admin用户请求/admin/foo,相拟的进程会发生。但是在认证之后,访问控制层将让请求通过:

Symfony2Book11:安全_Security_05

当用户请求一个受限资源时产生的请求流是十分简单的,但相当灵活。正如你稍后将看到的那样,认证可以通过多样方式来处理,包括通过表单登录、X.509证书或通过Twitter来认证用户。无论认证的模式如何,请求流总是相同的:

  1. 用户访问受限资源;
  2. 应用程序将用户重定向给登录表单;
  3. 用户提交它的证书(如:用户名/密码);
  4. 防火墙认证用户;
  5. 认证用户重发原始请求。

确切的过程实际取决于你所使用的认证机制。举个例子,当使用表单登录时,用户提交它的证书到一个处理表单的URL(如 /login_check),然后重定向到最初请求的URL(如 /admin/foo)。但是如果是HTTP认证,用户将直接提供它的证书到原始URL(如 /admin/foo),然后在同一请求中将页面返回给用户(如:不进行重定向)

这些特质不会引起任何问题,但记住它们是有好处的。

你也将在稍后学到在Symfony2中如何保证其它事物的安全,包括特定的控制器、对象、甚至是PHP方法。

使用传统的登录表单

在目前为止,你已经看到如何将你的应用程序放置在防火墙下,然后根据规则限制访问某些区域。通过使用HTTP认证,你可以毫不费力地进入所有浏览器都可以提供的用户名/密码框。然而,Symfony2支持许多对话框以外的认证机制。所有这一切的细节,可参见安全配置参考。

在本节中,你将让用户通过一个传统的HTML登录表单来增强这一过程。

首先,在你的防火墙下启动表单登录:

  1. # app/config/config.yml 
  2. security: 
  3.     firewalls: 
  4.         secured_area: 
  5.             pattern:    ^/ 
  6.             anonymous: ~ 
  7.             form_login: 
  8.                 login_path:  /login 
  9.                 check_path:  /login_check 

如果你不需要自定义你的login_path或check_path的值(这里的值是缺省值),你可以缩写你的配置:

  1. form_login: ~ 

现在,当安全系统启动认证过程时,它将用户重定向到登录表单(缺省是/login)。实现这个登录表单。首先创建两个路由:一个显示登录表单(如:/login),一个将处理登录表单的提交(如:/login_check):

  1. # app/config/routing.yml 
  2. login: 
  3.     pattern:   /login 
  4.     defaults:  { _controller: AcmeSecurityBundle:Security:login } 
  5. login_check: 
  6.     pattern:   /login_check 

你不需要为/login_check的URL实现控制器,因为防火墙会自动捕捉和处理这一URL上的任何表单提交。它是可选但却有用,创建路由以便你可以用它在接下来的登录模板中生成表单提交URL。

注意login路由名是不重要的,主要的是路由(/login)的URL匹配check_pach的配置值,因为在那里安全系统将需要登录的用户进行重定向。

下一步,创建显示登录表单的控制器:

  1. // src/Acme/SecurityBundle/Controller/Main; 
  2. namespace Acme\SecurityBundle\Controller; 
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller; 
  5. use Symfony\Component\Security\Core\SecurityContext; 
  6.  
  7. class SecurityController extends Controller 
  8.     public function loginAction() 
  9.     { 
  10.         // get the login error if there is one 
  11.         if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { 
  12.             $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); 
  13.         } else { 
  14.             $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); 
  15.         } 
  16.  
  17.         return $this->render('AcmeSecurityBundle:Security:login.html.twig', array( 
  18.             // last username entered by the user 
  19.             'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), 
  20.             'error'         => $error, 
  21.         )); 
  22.     } 

不要让这个控制器迷惑你。正如你将稍后看到的那样,当用户提交表单时,安全系统自动为你处理表单提交。如果用户提交了一个非法的用户名或密码,控制器会从安全系统中读到表单提交的错误,以便返回给用户显示。

换句话说,你的工作是显示登录表单以及可能发生的错误,但安全系统自身检查提交的用户名和密码,并对该用户进行认证。

最后,创建相应的表单:

  1. {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #} 
  2. {% if error %} 
  3.     <div>{{ error.message }}</div> 
  4. {% endif %} 
  5.  
  6. <form action="{{ path('login_check') }}" method="post"> 
  7.     <label for="username">Username:</label> 
  8.     <input type="text" id="username" name="_username" value="{{ last_username }}" /> 
  9.  
  10.     <label for="password">Password:</label> 
  11.     <input type="password" id="password" name="_password" /> 
  12.  
  13.     {# 
  14.         If you want to control the URL the user is redirected to on success (more details below) 
  15.         <input type="hidden" name="_target_path" value="/account" /> 
  16.     #} 
  17.  
  18.     <input type="submit" name="login" /> 
  19. </form> 

被送入模板的错误变量是AuthenticationException的实例。它也许包含更多关于认证失败的信息,甚至是敏感信息。所以它使用的非常广泛!

表单有着非常少的要求。首先,通过提交表单到/login_check(通过login_check路由),安全系统自动为你截取表单提交并进行表单处理。其次,安全系统预期被提交的表单项是_username和_password(这些表单项名可以被配置)。

这是这样!当你提交表单时,安全系统将自动检查用户的证书,要么认证通过用户,要么将用户重定向到登录表单以显示错误信息。

让我们回顾一下整个过程:

  1. 用户尝试访问受限用户;
  2. 防火墙通过将用户重定向到登录表单(/login)并启动认证过程;
  3. 在本例中,/login页通过创建路由和控制器来渲染登录页面;
  4. 用户提交登录表单到/login_check;
  5. 安全系统拦截请求,检查用户提交的证书,如果正确的话就认证通过用户,反之则将用户送回登录页面。

缺省状况下,如果提交的证书是正确的,用户将被重定向到被请求的原始页(如:/admin/foo)。如果用户最初是直接访问的login页面,它将被重定向到主页。这可以允许你去定制,举个例子,重定向用户到指定的URL。

关于这个的更多细节,以及如何自定义表单登录过程,请参见如何自定义你的表单登录。

避开常见陷阱

当设置你的登录表单时,小心一些常见的陷阱。

1. 创建正确的路由

首先,确保你已经正确地定义了/login和/login_check路由,它们对应着login_path和check_path配置值。这里的一个错误配置可能会将你重定向到404页,而非登录页面,或者提交登录表单之后什么事情也没发生(你只是一遍又一遍地看到登录页面)。

2. 确保登录页面是不安全的

同样,也需要确保登录页面是不要求任何角色就可以查看的。例如,下面的配置,为所有的URL要求ROLE_ADMIN角色(包括/login的URL),会引起循环重定义的:

  1. access_control: 
  2.     - { path: ^/, roles: ROLE_ADMIN } 

在/login的URL上删除访问控制以修复该问题:

  1. access_control: 
  2.     - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } 
  3.     - { path: ^/, roles: ROLE_ADMIN } 

同样,如果你的防火墙不允许匿名用户的话,你也需要创建一个特殊的防火墙来让匿名用户使用登录页:

  1. firewalls: 
  2.     login_firewall: 
  3.         pattern:    ^/login$ 
  4.         anonymous:  ~ 
  5.     secured_area: 
  6.         pattern:    ^/ 
  7.         login_form: ~ 

3. 确保“/login_check”在防火墙之后

接下来,确保你check_path的URL(如:/login_check)在你登录表单的防火墙之后(在本例中,单个防火墙匹配所有URL,包含/login_check)。如果/login_check没有匹配任何防火墙,你将得到不能为路径“/login_check"找到控制器的异常。

4. 多个防火墙不能共享安全内容

如果你使用多重防火墙,并且你在针对一个防火墙进行认证,那么你将不会再自动针对其它的防火墙进行认证。不同的防火墙就象不同的安全系统。这也是为什么对于大多数应用程序而言,有一个主要的防火墙就足够了的原因。

授权

安全的第一个总是认证:确认用户是谁的过程。在Symfony2中,认证可以用任何方式实现,可以通过表单登录、基本HTTP认证甚至通过Facebook。

一旦用户被认证,授权就开始了。授权提供一个标准、强大的方式去决定用户是否可以访问资源(URL、模型对象、方法调用...)。它是通过对每个用户指定一个特定的角色,然后要求不同角色对应不同的资源。

授权过程有两个不同的方法:

  1. 用户有一个指定的角色集;
  2. 资源需要能够访问它的特定角色。

在本节,你将关注如何保护不同角色对不同资源(如URL、方法调用等)的安全。稍后,你将学习到更多如何创建角色并将其指定给用户。

保护特定的URL模式

保护应用程序的最好方式就是保护URL模式。你已经在本章的第一个例子中看到了这一点,任何匹配正则表达式模式^/admin请求的URL都要求ROLE_ADMIN角色。

你可以定义许多的你所需的URL模式,每个都是一个正则表达式。

  1. # app/config/config.yml 
  2. security: 
  3.     # ... 
  4.     access_control: 
  5.         - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } 
  6.         - { path: ^/admin, roles: ROLE_ADMIN } 

路径前的^ 确保只有以该URL开头的模式可以被匹配。举个例子,简单的/admin,可以匹配/admin/foo,也可以匹配/foo/admin。

对于每个输入请求而言,Symfony2尝试找出一个匹配的访问控制规则(第一个赢)。如果用户还没有认证,那么授权过程被启动(如用户得到一个登录的机会)。然而,如果用户通过认证,但没有拥有要求的角色,一个拒绝访问异常被抛出,你可以对其进行处理并转入“拒绝访问”错误页。更多信息参见如何自定义错误页。

因为Symfony2只使用它匹配到的第一个访问控制规则,类似/admin/users/new的URL将被匹配,而且仅要求ROLE_SUPER_ADMIN角色。一些类似/admin/blog的URL将匹配第2条规则,并要求ROLE_ADMIN角色。

你也可以通过一条access_control条目来强制使用HTTP或HTTPS。更多信息参见如何为不同的URL强制使用HTTP或HTTPS。

保护控制器

基于URL模式来保护你的应用程序是容易的,但在某种情况下控制粒度不够。在必要时,你可以很容易地在控制器内部强制授权:

  1. use Symfony\Component\Security\Core\Exception\AccessDeniedException 
  2. // ... 
  3.  
  4. public function helloAction($name) 
  5.     if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) { 
  6.         throw new AccessDeniedException(); 
  7.     } 
  8.  
  9.     // ... 

你也可以选择安装和使用可选的SecurityExtraBundle,可以通过注释来保护你的控制器:

  1. use JMS\SecurityExtraBundle\Annotation\Secure; 
  2.  
  3. /** 
  4.  * @Secure(roles="ROLE_ADMIN") 
  5.  */ 
  6. public function helloAction($name) 
  7.     // ... 

更多信息,参见SecurityExtraBundle文档。如果你已经使用了Symfony2的标准发布,这个Bundle缺省就是可用的。如果没有的话,你也可以很容易地下载并安装它。

保护其它服务

实际上,保护Symfony中的任何东西都可以使用与上一节相似的策略。例如,假设你有一个服务(如PHP类),它的工作是从一个用户发送邮件到另一个。你可以限制该类的使用,无需关心发送用户,但接收用户需要拥有特定的角色。

关于你可以使用安全组件去保护你应用程序不同的服务和方法的更多信息,查看如何在你应用程序中保护服务和方法。

访问控制列表(ACLs): 保护单个数据库对象

想象一下,你正在设计一个博客系统,你的用户可以在你的博文上发表评论。现在,你想一个用户能够编辑他自己的评论,而不是其他人的评论。还有,作为管理用户,只要你想,你就可以去编辑所有的评论。

安全组件附带了一个可选的访问控制列表(ACL)系统,当你需要控制你系统中的某个对象实例时你可以用它。没有ACL,你可以保护你的系统,通常只是某个用户可以编辑博客评论。但有了ACL,你就可以限制和允许一个评论一个评论地去限制或允许访问。

更多信息,可以参见食谱(cookbook)文件:访问控制列表

用户

在上一节,你学会了如何保护要求一组角色权限的不同资源。在这节,你们将了解授权的另一部分:用户。

用户从哪里来? (User Providers)

在认证期间,用户提交一组证书(通常是用户名和密码)。认证系统的工作是从一些用户池中匹配那些证书。那么这个用户列表是从哪里来的呢?

在symfony2中,用户可以来自任何地方,配置文件、数据表、WEB服务或你能想到的任何东西。提供一个或更多用户到认证系统的被称为“user provider"。Symfony2标配有两个常用的用户提供器:一个是从配置文件中引导,一个是从数据表中引导。

在配置文件中指定用户

指定用户最容易的方式是在配置文件中直接指定。实际上,你已经在本章的示例中看到过。

  1. # app/config/config.yml 
  2. security: 
  3.     # ... 
  4.     providers: 
  5.         default_provider: 
  6.             users: 
  7.                 ryan:  { password: ryanpass, roles: 'ROLE_USER' } 
  8.                 admin: { password: kitten, roles: 'ROLE_ADMIN' } 

这种用户提供器被称为"in-memory"用户提供器,因为用户没有保存在数据库中。实际的用户对象是通过Symfony2(User)提供的。

任何用户提供器都可以直接从配置中引导用户,通过指定用户配置参数并列出它之下的用户。

如果你的用户名完全是数字(如77)或包含短横线(如user-name),那么当在YAML中指定用户时,你需要使用另一种语法:

  1. users: 
  2.     - { name: 77, password: pass, roles: 'ROLE_USER' } 
  3.     - { name: user-name, password: pass, roles: 'ROLE_USER' } 

对于小的网站,这个方式可以快速方便地进行设置。对于更复杂的系统,你将想从数据库中引导你的用户。

从数据库中引导用户

如果你想要通过Doctrine ORM来引导你的用户,那么你可以通过创建一个User类,配置实体提供器来很方便地实现这一功能。

使用这种方式,你首先需要创建你自己的用户类,该类将被保存在数据库中。

  1. // src/Acme/UserBundle/Entity/User.php 
  2. namespace Acme\UserBundle\Entity; 
  3.  
  4. use Symfony\Component\Security\Core\User\UserInterface; 
  5. use Doctrine\ORM\Mapping as ORM; 
  6.  
  7. /** 
  8.  * @ORM\Entity 
  9.  */ 
  10. class User implements UserInterface 
  11.     /** 
  12.      * @ORM\Column(type="string", length="255") 
  13.      */ 
  14.     protected $username; 
  15.  
  16.     // ... 

对于安全系统而言,唯一地要求就是要你的自定义User类要实现UserInterface接口。这意味着你“user“的概念可以是任何东西,只要你实现了这个接口。

用户对象将被序列化,并在请求期间保存在会话(Session)中。因此,建议你在你的User对象中实现Serializable接口

接下来,配置一个实体用户提供器,并将其指向你的User类:

  1. # app/config/security.yml 
  2. security: 
  3.     providers: 
  4.         main: 
  5.             entity: { class: Acme\UserBundle\Entity\User, property: username } 

根据这个新提供器的介绍,认证系统将尝试从数据库中通过该类的username来引导用户对象。

本例只是想向你展示实体提供器的基本思路。要得到一个完全可以工作的示例,请参见如何引导从数据库引导用户(实体提供器)

更多关于创建自定义提供器的信息(如:你是否需要通过WEB服务来导引用户),请参见如何创建一个自定义的用户提供器

加密用户的密码

到目前为止,为了简单起见,所有示例都是用纯文本来保存用户密码的(无论这些用户是保存在配置文件还是数据库中)。当然,在实际应用程序中,为了安全起见你会加密用户的密码。将你的User类映射到几个内建“加密器”之一上是很容易实现的。举个例子,为了将你的用户保存在内存中,并通过sha1加密它们的密码,可以如下所示:

  1. # app/config/config.yml 
  2. security: 
  3.     # ... 
  4.     providers: 
  5.         in_memory: 
  6.             users: 
  7.                 ryan:  { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' } 
  8.                 admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' } 
  9.  
  10.     encoders: 
  11.         Symfony\Component\Security\Core\User\User: 
  12.             algorithm:   sha1 
  13.             iterations: 1 
  14.             encode_as_base64: false 

通过将迭代设为1、encode_as_base64设为false,密码可以简单地一次通过sha1算法,而无须添加任何其它的加密算法。你现在可以通过编辑(如:hash('sha1','ryanpass'))或通过一些如functions-online.com之类的在线工具来计算密码的哈希值。

如果你正在动态创建你的用户(并将其保存在数据库中),你甚至可以强制使用哈希算法,然后依靠一个实际的密码加密对象来帮助你加密密码。举个例子,假设你的User对象是Acme\UserBundle\Entity\User(如上例所示)。首先,为用户配置加密器:

  1. # app/config/config.yml 
  2. security: 
  3.     # ... 
  4.  
  5.     encoders: 
  6.         Acme\UserBundle\Entity\User: sha512 

在本例中,你使用更为健壮的sha512算法。此外,由于你简单地指定sha512算法作为字符串,所以系统将缺省将你的密码哈希5000次,然后对其做base64加密。换句话说,该密码已经足够安全,以致于无法进行解密(如你不能从被哈希过的密码中算出原密码来)。

如果你已经拥有了用户的注册表单,那么你将需要确定被哈希的密码,以便你可以为你的用户进行设置。无论你为你的User对象配置为何种算法,被哈希的密码总是可以从控制器得到:

  1. $factory = $this->get('security.encoder_factory'); 
  2. $user = new Acme\UserBundle\Entity\User(); 
  3.  
  4. $encoder = $factory->getEncoder($user); 
  5. $password = $encoder->encodePassword('ryanpass', $user->getSalt()); 
  6. $user->setPassword($password); 

检索User对象

在认证之后,当前用户的User对象可以通过security.context服务来访问。从控制器内部来看,如下所示:

After authentication, the User object of the current user can be accessed via the security.context service. From inside a controller, this will look like:

  1. public function indexAction() 
  2.     $user = $this->get('security.context')->getToken()->getUser(); 

匿名用户可以在技术上被认证,也就是说匿名用户对象的isAuthenticated()方法将返回true。要检查你的用户实际上是否被认证,请检查IS_AUTHENTICATED_FULL角色。

使用多个用户提供器

每个认证机制(如:HTTP认证、表单登录等)只使用一个用户提供器,并且缺省情况下只使用第1个声明的用户提供器。但你是否想指定一些用户通过配置认证,而剩下的用户是在数据库中呢?通过创建一个新的提供器,并将两者串在一起是可能实现的:

  1. # app/config/security.yml 
  2. security: 
  3.     providers: 
  4.         chain_provider: 
  5.             providers: [in_memory, user_db] 
  6.         in_memory: 
  7.             users: 
  8.                 foo: { password: test } 
  9.         user_db: 
  10.             entity: { class: Acme\UserBundle\Entity\User, property: username } 

现在所有的认证机器都将使用chain_provider,因为它是第一个被指定的。chain_provider将反过来尝试从in_memony和user_db提供器中引导用户。

如果你没有理由从你的user_db用户中分隔出in_memory用户来,那么你甚至可以通过将两个源合并成单个的提供器来更方便地实现它:

  1. # app/config/security.yml 
  2. security: 
  3.     providers: 
  4.         main_provider: 
  5.             users: 
  6.                 foo: { password: test } 
  7.             entity: { class: Acme\UserBundle\Entity\User, property: username } 

你也可以配置防火墙或者单个认证机制来使用特定的提供器。同样,除非指定一个提供器,否则总是使用第1个提供器:

  1. # app/config/config.yml 
  2. security: 
  3.     firewalls: 
  4.         secured_area: 
  5.             # ... 
  6.             provider: user_db 
  7.             http_basic: 
  8.                 realm: "Secured Demo Area" 
  9.                 provider: in_memory 
  10.             form_login: ~

在本例中,如果用户尝试通过HTTP认证登录,认证系统将使用in_memory用户提供器。但如果使用尝试通过表单登录,那么user_db提供器将被使用(因为它对整个防火墙来说是缺省的)。

更多关于用户提供器和防火墙配置的信息,参见安全配置参考

角色

"role"的概念是授权过程的关键。每个用户都被指定了一组角色,然后每个资源要求一个或更多的角色。如果用户拥有要求的角色,那么被授予访问权限。否则访问将被拒绝。

角色非常简单,基本上都是字符串,你可以在必要时找到并使用(虽然角色在内部也是对象)。举个例子,如果你需要在你网站的博客管理部分开始受限的访问,你可以使用ROLE_BLOG_ADMIN角色来保护该部分。该角色无须在某处定义,你只需要使用即可。

所有角色必须以ROLE_做为前缀,以便被Symfony2管理。如果你通过专用的Role类(更为高级)来定义你的角色,那么不要使用ROLE_前缀。

角色继承

为了避免为用户关联许多的角色,你可以通过创建一个角色继承来定义角色继承规则:

  1. # app/config/security.yml 
  2. security: 
  3.     role_hierarchy: 
  4.         ROLE_ADMIN:       ROLE_USER 
  5.         ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] 

在上面的配置里,有着ROLE_ADMIN角色的用户也拥有ROLE_USER角色。ROLE_SUPER_ADMIN角色则拥有ROLE_ADMIN、ROLE_ALLOWED_TO_SWITCH和 ROLE_USER(继承自ROLE_ADMIN)等角色

注销

通常,你也希望你的用户能够注销。幸运地是,防火墙可以为你自动处理这一过程,你只需要激活注销配置参数即可:

  1. # app/config/config.yml 
  2. security: 
  3.     firewalls: 
  4.         secured_area: 
  5.             # ... 
  6.             logout: 
  7.                 path:   /logout 
  8.                 target: / 
  9.     # ... 

一旦在你防火墙下配置完成,发送用户到/logout(或者无论何时你将path配置成去那),将注销当前用户。用户然后被发送到首页(通过target参数定义)。path和target配置参数缺省是指向这儿的。换句话说,除非你要自定义它们,否则你可以整个忽略它们并减缩你的配置:

  1. logout: ~ 

注意,你不需要为/logout的URL去实现控制器,防火墙完成了这一切。然而,你也许希望创建一个路由,以便你可以用它来生成URL:

  1. # app/config/routing.yml 
  2. logout: 
  3.     pattern:   /logout 

一旦用户被注销,无论path被定义成什么,他将会被重定向到通过上面的target参数指定的路径(如:homepage)。更多关于注销的配置,请参见安全配置指南

模板中的访问控制

如果你想在模板中检查当前用户是否拥有角色,那么使用内建的帮手函数:

  1. {% if is_granted('ROLE_ADMIN') %} 
  2.     <a href="...">Delete</a> 
  3. {% endif %} 

如果你使用该函数,且不是防火墙激活的URL,一个异常将被抛出。而且使用一个主防火墙去覆盖所有的URL(在本章中被显示)总是个好主意。

控制器中的访问控制

如果你希望在控制器中检查当前用户是否拥有角色,使用security.context的isGranted方法:

  1. public function indexAction() 
  2.     // show different content to admin users 
  3.     if($this->get('security.context')->isGranted('ADMIN')) { 
  4.         // Load admin content here 
  5.     } 
  6.     // load other regular content here 

当isGranted方法被调用时防火墙必须是激活的,否则将抛出一个异常。参见上面关于模板部分的说明以得到更多细节

冒充一个用户

有时,下面情形是有用的,从一个用户切换到另一个用户,而无须注销再登录(例如当你正在调试或尝试理解一个就该用户而言无法重现的错误时)。通过激活防火墙监听器switch_user是很容易实现的:

  1. # app/config/security.yml 
  2. security: 
  3.     firewalls: 
  4.         main: 
  5.             # ... 
  6.             switch_user: true 

要切换到另一个用户,只需要为当前URL添加一个带有_switch_user参数的查询字符串,且该参数的值是用户值:

而要切回最初的用户,指定_exit用户名。

当然,这一功能有必要提供给一小部分用户。缺省情况下,权限会被限制到拥有ROLE_ALLOWED_TO_SWITCH角色的用户。该角色的用户可以通过角色设置来修改。为了得到更多的安全,你也可以通过参数设置来改变查询参数名:

  1. # app/config/security.yml 
  2. security: 
  3.     firewalls: 
  4.         main: 
  5.             // ... 
  6.             switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user } 

无状态认证

缺省情况下,Symfony2依赖cookie(会话)去持久化用户的安全上下文。但如果你使用证书或HTTP认证,持久化不需要每个请求都有证书可用。在那种情况下,如果不需要在两个请求之间保存什么的话,你可以激活无状态认证(意思是没有cookie被Symfony2创建):

  1. # app/config/security.yml 
  2. security: 
  3.     firewalls: 
  4.         main: 
  5.             http_basic: ~ 
  6.             stateless:  true 

如果你使用表单登录的话,即使你将stateless设置true,Symfony2也将创建一个cookie。

最后的话

要在你应用程序中正确地解决安全,可是个深刻而复杂的问题。幸运的是,Symfony2的安全组件遵循一个久经考验的安全模型,该模型是基于认证和授权的。认证总是最先发生,它被防火墙处理,防火墙的工作是通过几种不同的方式(如HTTP认证、登录表单等)来确定用户的身份。在食谱(cookbook)中,你会找到使用其它方式来处理认证的例子,包括如何实现“记住我”cookie的功能。

一旦用户被认证了,授权层就可以决定用户是否有访问特定资源的权限了。通常情况下,角色被应用到URL、类或方法上。如果当前用户没有该角色,那么访问被拒绝。然而,授权层更为深入,它遵循表决系统以便多方确定当前用户是否有权访问指定的资源。在食谱(cookbook)中可以找到更多关于这个和其它的主题。