[+]文章目录

symfony的安全系统是非常强大的,但它去设置时也可能会造成混淆。在本章中,您将学 会如何一步一步的设置应用程序的安全性,从配置防火墙和你怎样拒绝这些用户进入和获取用户对象。根据你所需要的复杂程度,可能有时初始设置也很困难。但 是,它一旦完成,symfony的安全系统是即灵活又可以(像你希望的一样)有乐趣的去工作。

由于有很多话要说,这一章主要分为以下几大内容:

1.开始设置security.yml(认证);

2.拒绝访问你的应用(授权);

3.获取当前用户对象;

其余还有很多小的部分(但仍然令人着迷),像注销和编译用户密码。

1)开始设置security.yml(认证)

这个安全系统是配置在app/config/security.yml。默认配置是这样的:

app/config/security.yml
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
# app/config/security.yml
security:
    providers:
        in_memory:
            memory: ~
 
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
 
        default:
            anonymous: ~
 

这个firewalls键是你安全系统的心脏。这个dev防火墙并不重要,他只是确保symfony开发工具-像网址/_profiler和/_wdt不会被你的安全策略所阻止。

你也可以匹配其他细节的请求(例如 host主机)。更多细节和案例请阅读How to Restrict Firewalls to a Specific Request

所 有的其他URL都可以在default防火墙中处理(没有pattern键意味着它可以匹配所有URL)。你可以想象这个防火墙如同你的安全系统,因此它 通常只有一个主要的防火墙是有意义的。但这并不意味着每一个URL都要求验证身份-该anonymous键重点用于完成这个功能。事实上,如果你现在直接 访问首页,你可以访问成功并且你会看到你的“authenticated”为anon.。不要被Authenticated旁边的“Yes”愚弄,你只是 一个匿名用户:

稍后您将学习如何拒绝用户访问某些URL或控制器。

Security是高度可配置的,并有一个安全配置手册,这个手册显示所有的选项和一些额外的解释。

A)如何让你的用户进行身份验证的配置

防火墙的主要工作是如何配置你的用户进行身份验证。他们将使用一个登录表单?基础的Http?一个api令牌?所有上面的方式?

让我们从基本的http工作开始(老旧的http弹出方式)。想要激活此功能,需要添加http_basic到firewall(防火墙)下:

 
1
2
3
4
5
6
7
8
9
# app/config/security.yml
security:
    # ...
 
    firewalls:
        # ...
        default:
            anonymous: ~
            http_basic: ~
 

简单吧!去试试吧,你需要要求这个用户去看一个页面中的登陆。为了让事情变的有趣,创建一个新页面 /admin。例如,如果你使用annotations,应该这样创建:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/AppBundle/Controller/DefaultController.php
// ...
 
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyComponentHttpFoundationResponse;
 
class DefaultController extends Controller
{
    /**
     * @Route("/admin")
     */
    public function adminAction()
    {
        return new Response('Admin page!');
    }
}
 

下一步,添加一个access_control到security.yml需要这个用户访问网址时,先登录。

 
1
2
3
4
5
6
7
8
9
# app/config/security.yml
security:
    # ...
    firewalls:
        # ...
 
    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }
 

 你需要学习更多关于ROLE_ADMIN的东西和后面会遇到的拒绝访问方式,在后面的拒绝访问,角色和其他授权章节

太好了,你现在就去访问 /admin ,你会看到HTTP Basic方式弹出登录:

但是谁能够登录?用户从哪里来呢?

你要使用一个传统的form表单吗?太好了!可以看看 How to Build a Traditional Login Form还支持什么方法?请查看 Configuration Reference或者建设我们自己的

B)怎么样配置用户

当你输入你的用户名,symfony就需要加载用户的信息。这就是所谓的“user provider”,而你要配置他。symfony有一个内置的方式可以从数据库加载用户,还可以创建自己的user provider

我们使用最简单的方式(他有很多限制),这种配置是在security.yml里用硬编码的方式来加载用户。

这就是所谓的“in memory” provider,但最好是把它作为一个“配置”提供者:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
# app/config/security.yml
security:
    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: ryanpass
                        roles: 'ROLE_USER'
                    admin:
                        password: kitten
                        roles: 'ROLE_ADMIN'
    # ...
 

像firewalls,你能够有多个providers,但你只需要一个。如果你有多个,你可以配置一个provider来使用,你防火墙下的provider健(例如provider:in_memory)。

尝试使用用户名admin和密码kitten来登录。你应该看到一个错误!

No encoder has been configured for account

“SymfonyComponentSecurityCoreUserUser”

为了解决这个问题,需要添加一个encoders 健:

 
1
2
3
4
5
6
7
# app/config/security.yml
security:
    # ...
 
    encoders:
        SymfonyComponentSecurityCoreUserUser: plaintext
    # ...
 

User providers加载用户信息并且把它变成一个User对象。如果你从数据库加载用户(load users from the database)或者使用一些其他的方式(some other source),你需要使用你自己定义的User类。当你使用“in memory” provider时,你就要给他一个SymfonyComponentSecurityCoreUserUser对象。

无论你使用什么用户类,你都需要去告诉symfony他们的密码的编码算法是什么。在本例中,这个密码使用的是明文的,但在第二个例子中,你需要改变它成bcrypt。

如果你现在刷新,你就会登录进来!网页的调试工具栏会告诉你,你是谁你的角色是什么:

由于此URL需要ROLE_ADMIN,如果你登录的事ryan,这会拒绝你的访问。后面还有(固定URL模式(ACCESS_CONTROL))。

从数据库去加载用户

如果你想通过doctrine orm加载你的用户这很容易!请参阅 How to Load Security Users from the Database (the Entity Provider)

C)加密用户密码

不管里security.yml里的用户存储在一个数据库或者一些其他的形式,你都需要去加密你的密码。最好的算法是使用bcrypt:

 
1
2
3
4
5
6
7
8
# app/config/security.yml
security:
    # ...
 
    encoders:
        SymfonyComponentSecurityCoreUserUser:
            algorithm: bcrypt
            cost: 12
 

如果你使用php5.4或者更低版本,为了能够使用bcrypt加密,你就需要通过composer去安装ircmaxell/password-compat库。

当然,你的用户密码现在需要准确的算法加密。对于硬编码用户,你能够使用一个在线工具,他会给你生成加密过的密码:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/config/security.yml
security:
    # ...
 
    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
                        roles: 'ROLE_USER'
                    admin:
                        password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
                        roles: 'ROLE_ADMIN'
 

现在一切都将像以前一样工作。但如果你有动态的用户(例如,数据库里的用户),你怎么在数据库中已编程的方式掺入加密的密码?别担心,请看后面的“动态加密密码”。

这个加密方式依赖于你的php版本,这个版本要包含hash_algos函数以及一些其他方法(例如bcrypt)算法才会正确返回。可以看看Security Reference Section中案例的encoders键。

还可以针对不同的用户,分别使用不同的算法。详细内容请查看How to Choose the Password Encoder Algorithm Dynamically

D)配置完成!

恭喜!现在您可以在security.yml文件使用http Basic和中加载用户的方式来让认证系统工作了。

你的下一步需要你来设置:

  • 配置用户的不同登录方法,例如一个登录表单或者一些完全自定义的方式;
  • 不同的方式来加载用户,例如从数据库或者一些其他的地方;
  • 学习如何拒绝访问,加载用户对象并处理角色的授权;

2)拒绝访问,角色和其他授权

现在,用户可以通过http_basic或者其他方式来登录你的应用程序。太好了!现在你需要学习如何拒绝访问并且能够与用户对象一起工作。这就要所谓的授权,他的工作是决定一个用户是否能够访问一些资源(如一个URL、一个model对象、方法调用等)。

授权过程分为两个不同的方面:

用户在登录时收到一组特定的角色(例如 ROLE_ADMIN)。

你添加的资源(例如url、控制器)需要特定“属性”(例如一个ROLE_ADMIN角色)才能够访问它。

除 了角色(例如ROLE_ADMIN),你还可以使用一些其他的属性/字符串来(例如 EDIT)保护资源或者使用voters和symfony的ACL系统去做同样意义的工作。这可能会派上用场,如果你需要检测用户A能否编辑对象B(例 如,id为5的产品)。更多信息请参见Access Control Lists (ACLs): Securing individual Database Objects

角色

当一个用户登录,他会接收到一个设置好的角色(例如 ROLE_ADMIN)。在上面的例子中,这些都被硬编码到security.yml中。如果你正在数据库中加载用户,这些都应该存储在你表的列中。

所有角色你要分给一个用户已定要以ROLE_前缀为开始。否则,他们不会被symfony的安全系统认定为正常的方式(除非你正在做一些先进的事,分配一个角色FOO给一个用户,然后检查FOO在下面的例子中是行不通的)。

角色很简单,而且基本上都是你根据需要自己创造的字符串。例如,如果你需要开始限制访问您的网站博客部分,你可以使用ROLE_BLOG_ADMIN角色来保护。这个角色不需要在其他地方定义它-你就可以开始使用了。

确保每个用户最少有一个角色,或者你的用户不需要身份验证。常见的做法就是给普通用户一个ROLE_USER角色。

你还可以设置角色的层级结构,symfony会自动知道你的其他角色。

添加代码以拒绝访问

有两种方式可以实现拒绝访问:

  • 在security.yml中使用access_control,允许使用URL的模式(例如 /admin/*).这很容易,但弹性很差;
  • 在你的代码中使用security.authorization_checker服务;

URL保护模式(ACCESS_CONTROL)

您需要保护应用程序的一部分,最基本的方法是URL模式。早些时候你可能已经看到,任何匹配正则表达式^/admin时都需要ROLE_ADMIN角色:

 
1
2
3
4
5
6
7
8
9
# app/config/security.yml
security:
    # ...
    firewalls:
        # ...
 
    access_control:
        # require ROLE_ADMIN for /admin*
        - { path: ^/admin, roles: ROLE_ADMIN }
 

它保护了这部分的安全,这很伟大,但是你可能还想保护你的控制器(secure your individual controllers )。

如果你需要,你可以定义多个URL模式-每一个都是正则表达式。但是,仅仅有一个会匹配。symfony会从顶部开始扫描,直到找到一个与access_control相匹配的URL,然后立刻结束。

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

路径使用^的意识是指定URL开始的地方去进行模式匹配。例如,一个简单的路径为/admin(不包含^)将匹配/admin/foo也将匹配/foo/admin。

了解access_control如何工作

这个access_control部分非常强大,但是如果你不明白他的工作原理,他也很危险(因为毕竟它涉及到安全性问题)。access_control出了匹配URL还可匹配IP地址、主机名和http方法。他也可以讲用户重定向到https的URL模式上。

想了解这一切,你可以看看How Does the Security access_control Work?

保护控制器和其他地方

您还可以轻松拒绝来自控制器的访问:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
 
public function helloAction($name)
{
    // The second parameter is used to specify on what object the role is tested.
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!');
 
    // Old way :
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Unable to access this page!');
    // }
 
    // ...
}
 

 这个denyAccessUnlessGranted()方法在symfony2.6被引入。在此之前和现在,你都可以直接检查访问,并抛出一个AccessDeniedException(就像上面的例子)。

这个security.authorization_checker服务在symfony2.6被引入。在此之前,你必须使用security.context服务的isGranted()方法。

这两种情况下,一个特殊的AccessDeniedException异常被抛出,这最终触发了symfony内部的一个403-http响应。

就是这样!如果尚未登录的用户,他们会被要求登录(例如重定向到登录页面)。如果它们登录,但不具备ROLE_ADMIN角色,它们会被指定到403拒绝访问页面(可以自定义)。如果它们登录并拥有正确的角色,该代码就会被执行。

多亏了SensioFrameworkExtraBundle,你能够在controller中使用annotations:

 
1
2
3
4
5
6
7
8
9
10
// ...
use SensioBundleFrameworkExtraBundleConfigurationSecurity;
 
/**
* @Security("has_role('ROLE_ADMIN')")
*/
public function helloAction($name)
{
    // ...
}
 

更多细节,请看FrameworkExtraBundle documentation.

模版中的访问控制

如果你想在模版中检测当前用户权限,可以使用内置的辅助函数:

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

如果你使用的这个函数没有在防火墙下面,它会抛出一个异常。再说,有一个防火墙覆盖所有URL并不是什么坏事(如本章前面讲到的)。

你要留心哦!这可是在你的基本布局或者在你的错误页面。因为一些symfony内部细节原因,prod环境会影响你的页面报错,你需要在这个模版中加入app.user。

 
1
{% if app.user and is_granted('ROLE_ADMIN') %}
 

保护其他服务

symfony可以像保护控制器一样保护一些其他的地方。假设你有一个服务(也就是一个php类),它的任务时发送电子邮件。不管他会在哪里使用,你都可以限制他,指定他的特定用户。

更多信息请参阅How to Secure any Service or Method in your Application.

检查用户是否登录(IS_AUTHENTICATED_FULLY)

当目前为止,你已经检查了基于角色的访问-它有ROLE_前缀并非配给用户。但是,如果你只是想检测用户是否登录。(不关心角色)那么你可以使用IS_AUTHENTICATED_FULLY:

 
1
2
3
4
5
6
7
8
9
10
// ...
 
public function helloAction($name)
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }
 
    // ...
}
 

 当然,你也可以使用access_control。

IS_AUTHENTICATED_FULLY不是一个角色,但是他的这种行为又像是一个角色,并且每个用户成功登录后都要有。事实上,有三个这样的特殊属性:

  • IS_AUTHENTICATED_REMEMBERED:所有登录用户中有这么一种,它们通过”remember me cookie”登录。尽管你没有使用remember me functionality,你依然能够检测用户是否登录。
  • IS_AUTHENTICATED_FULLY: 类似于IS_AUTHENTICATED_REMEMBERED,但更强。用户登录只是因为”remember me cookie”,他就只拥有IS_AUTHENTICATED_REMEMBERED,不会拥有IS_AUTHENTICATED_FULLY。
  • IS_AUTHENTICATED_ANONYMOUSLY:所有用户都有(甚至是匿名用户)-当白名单的URL访问时他很有用。更多细节请查看How Does the Security access_control Work?.

您还可以在模版中使用表达式:

 
1
2
3
4
5
{% if is_granted(expression(
    '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
)) %}
    <a href="...">Delete</a>
{% endif %}
 

关于表达式和安全性的更多细节,请参阅 Security: Complex Access Controls with Expressions.

Access Control Lists (ACLs):保护单个数据库对象

想象一下,你正在设计一个博客,用户可以在自己的帖子里发表评论。您还希望用户能够编辑自己的评论,不用别的用户编辑。此外,作为管理员用户,您希望能够编辑所有评论。

要做到这一点,你有两个选择:

  • Voters 允许用户使用业务逻辑(例如 用户可以编辑这篇文章,因为他们的创造者)去查明访问。可能会需要这个配置-他足够灵活,可以解决以上问题。
  • ACLs 允许你在系统中创建一个数据库结构,你可以给不同的用户分配不同的访问(例如 EDIT、VIEW)组成任意对象。你可以在你系统的管理界面使用管理员用户分配自定义访问。

在这两种情况下,你仍然要配合上面的拒绝访问方法来使用。

获取用户对象

这个security.token_storage服务在symfony2.6中被引入。在symfony2.6之前,你必须使用security.context服务的getToken()方法。

认证之后,访问当前用户的用户对象就需要调用security.token_storage服务。在控制器内,这样写:

 
1
2
3
4
5
6
7
8
9
10
11
public function indexAction()
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }
 
    $user = $this->getUser();
 
    // the above is a shortcut for this
    $user = $this->get('security.token_storage')->getToken()->getUser();
}
 

 用户将是一个对象,该对象取决于你的user provider

现在你可以在你的对象中调用任何方法。例如,调用一个用户对象的getFirstName()方法:

 
1
2
3
4
5
6
7
8
9
use SymfonyComponentHttpFoundationResponse;
// ...
 
public function indexAction()
{
    // ...
 
    return new Response('Well hi there '.$user->getFirstName());
}
 

经常检测用户登录

如果用户首次认证最重要的就是检测。如果他没有,$user就是空或者为匿名。等等,为什么呢?是的,这很怪。如果你没有登录,这个用户严格来讲应该是匿名,尽管控制器快捷方式getUser()方法会转变为null。

问题是这样的:你应该在使用用户前,经常检查用户是否登录并使用isGranted方法或者access_control去做:

 
1
2
3
4
5
6
7
8
9
// yay! Use this to see if the user is logged in
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw $this->createAccessDeniedException();
}
 
// boo :(. Never check for the User object to see if they're logged in
if ($this->getUser()) {
 
}
 

在模版中获取用户

这个对象在twig模版中可以使用app.user键:

 
1
2
3
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <p>Username: {{ app.user.username }}</p>
{% endif %}
 

注销

通常情况下,你希望用户能够注销。幸运的是,当您激活logout配置参数后,防火墙可以帮助你自动完成:

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

下一步,你需要去创建一个路由URL(但不需要控制器):

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

就是这样,用户触发/logout(或者你自定义的path路径),symfony将取消当前用户的验证信息。

一旦用户被注销,它们会被重定向到已经定义的target参数路径中(例如 homepage)。

如果你需要在注销后做一些有趣的事情,你可以通过success_handler键去添加一个登录成功后的处理程序这个将指向一个有LogoutSuccessHandlerInterface接口的服务类的id。详情请查看 Security Configuration Reference.

动态编译密码

如果,你的用户存储在数据库中,你需要用户插入的密码是加密的。不管你的用户对象使用什么算法,这些算法都要从一个控制器中确定:

 
1
2
3
4
5
6
7
// whatever *your* User object is
$user = new AppBundleEntityUser();
$plainPassword = 'ryanpass';
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);
 
$user->setPassword($encoded);
 

 security.password_encoder服务在symfony2.6版本被引入。

为了能够让他工作,你app/config/security.yml文件中的encoders键的配置必须是你的用户类(例如AppBundleEntityUser)。

这个$encoder对象也有一个isPasswordValid方法,她需要User对象作为第一个参数、要检测的明文密码作为第二个参数。

当你的一个用户去提交一个明文密码(例如 注册表单、改变密码表单),你一定有验证保证密码不能多于4096字符。详细请去阅读How to implement a simple Registration Form.

角色分级

很多角色的用户都是关联的,你可以创建一个角色的分级来定义角色的继承关系:

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

在上述结构中,用户ROLE_ADMIN也将有ROLE_USER角色。该ROLE_SUPER_ADMIN角色有ROLE_ADMINROLE_ALLOWED_TO_SWITCHROLE_USER(继承自ROLE_ADMIN)。

无状态认证

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

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

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

在依赖中检测已知的安全漏洞

这个security:check命令在symfony2.5被引入。这个命令包含SensioDistributionBundle,这必须在你的应用程序中注册才能使用次命令。

当你的symfony项目使用大量的依赖时,其中一些可能包含安全漏洞。这就是为什么symfony要包含一个security:check命令,检查你的composer.lock文件,在你安装文件时,发现所有已知的安全漏洞

 
1
$ php app/console security:check
 

定期执行此命令养成良好的习惯,发现问题尽快更新活着更换受损的依赖。在内部,这个命令使用FriendsOfPHP组织的公共security advisories database 发布。

这个security:check命令发现任何已知的安全漏洞代码就会终止。因此,你可以很容易在你的建设中集成。

最后的话

哇,很不错!你现在知道了很多安全的基础知识。最难的部分就是你需要定制的时候:像是定义一个认证策略(例如 api tokens),复杂的授权逻辑或者许多其他的事情(因为安全本来就很复杂!)。

幸运的是:在这里Security Cookbook Articles有很多文章,它描述了多种情况。此外,还可以参考Security Reference Section。许多配置没有具体的细节介绍,但是看到完整的配置树我想他可能很有帮助。

祝你好运;


« 前一篇