<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Symfony中文教程 &#187; Cookbook</title>
	<atom:link href="http://www.newlifeclan.com/symfony/archives/category/cookbook/feed" rel="self" type="application/rss+xml" />
	<link>http://www.newlifeclan.com/symfony</link>
	<description>站在巨人肩膀上的phpweb框架</description>
	<lastBuildDate>Fri, 12 Dec 2025 00:58:27 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.0.38</generator>
	<item>
		<title>(表单)如何使用数据转换器(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/978</link>
		<comments>http://www.newlifeclan.com/symfony/archives/978#comments</comments>
		<pubDate>Sun, 15 May 2016 04:33:20 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=978</guid>
		<description><![CDATA[<p>数据转换适用于将一个字段数据格式转换成表单里显示的数据格式（并且可以重复提交）。在symfony内部已经有了很 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/978">(表单)如何使用数据转换器(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>数据转换适用于将一个字段数据格式转换成表单里显示的数据格式（并且可以重复提交）。在symfony内部已经有了很多这样的字段类型。举例，DateType类型在input文本框中被渲染成yyyy-MM-dd格式。在内部，一个数据转换器将开始的DateTime字段的值转换成yyyy-MM-dd字符串渲染到form，并在提交时返回DateTime对象。<span id="more-978"></span></p>
<blockquote><p>注意：当一个表单字段设置了inherit_data配置时，数据转换器将不会应用到这一字段。</p></blockquote>
<h2>简单例子：清除用户输入的HTML</h2>
<p>假设你有一个Task表单，有一个textarea类型的description字段：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

// ...
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder-&gt;add('description', TextareaType::class);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver-&gt;setDefaults(array(
            'data_class' =&gt; 'AppBundle\Entity\Task',
        ));
    }

    // ...
}</pre><p>但，这里有两个复杂的地方：</p>
<p>1.你的用户可能会输入一些html标签，你可能不都需要它：在表单提交之后，你需要调用<a class="reference external" title="strip_tags" href="http://php.net/manual/en/function.strip-tags.php">strip_tags</a>方法。</p>
<p>2.为了友好，在渲染表单时你可能想要把&lt;br /&gt;标签转换成换行符\n，使得文本编辑起来更加人性化。</p>
<p>&nbsp;</p>
<p>这是一个将自定义数据转换到<tt class="docutils literal"><code>description</code></tt> 字段的好机会。使用 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Form\CallbackTransformer" href="http://api.symfony.com/3.0/Symfony/Component/Form/CallbackTransformer.html">CallbackTransformer</a></code></tt>这个方法很容易去做到：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ...

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder-&gt;add('description', TextareaType::class);

        $builder-&gt;get('description')
            -&gt;addModelTransformer(new CallbackTransformer(
                // transform &lt;br/&gt; to \n so the textarea reads easier
                function ($originalDescription) {
                    return preg_replace('#&lt;br\s*/?&gt;#i', "\n", $originalDescription);
                },
                function ($submittedDescription) {
                    // remove most HTML tags (but not br,p)
                    $cleaned = strip_tags($submittedDescription, '&lt;br&gt;&lt;br/&gt;&lt;p&gt;');

                    // transform any \n to real &lt;br/&gt;
                    return str_replace("\n", '&lt;br/&gt;', $cleaned);
                }
            ))
        ;
    }

    // ...
}</pre><p>CallbackTransformer类用两个回调函数作为参数。第一个函数将原始的值转化为一个能在表单中渲染的格式。第二个函数做了相反的事情：他将提交后获取的值转化为你代码中需要的格式。</p>
<blockquote><p>提示：这个addModelTransformer()方法接受任何实现<a class="reference external" title="Symfony\Component\Form\DataTransformerInterface" href="http://api.symfony.com/3.0/Symfony/Component/Form/DataTransformerInterface.html">DataTransformerInterface</a>接口的对象- 这样你能够创建属于我们自己的类，而不是在表单中放入所有的逻辑（看下一章）。</p></blockquote>
<p>当添加字段略微改变格式，你也可以添加转换器(transformer)：</p><pre class="crayon-plain-tag">use Symfony\Component\Form\Extension\Core\Type\TextareaType;

$builder-&gt;add(
    $builder-&gt;create('description', TextareaType::class)
        -&gt;addModelTransformer(...)
);</pre><p>&nbsp;</p>
<h3>复杂的例子：将Issue编号转化成Isuse实体</h3>
<p>比如说你有一个Task实体和一个Issue实体他们是 many-to-one(多对一)的映射关系（好像每一个任务都有一些关联的问题）。添加所有问题到一个listbox，他会变得很长，而且加载时间也变长了。你可以添加一个textbox让用户输入一些问题的编号来解决。</p>
<p>开始我们设置一个文本字段就和平时一样：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

// ...
class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('description', TextareaType::class)
            -&gt;add('issue', TextType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver-&gt;setDefaults(array(
            'data_class' =&gt; 'AppBundle\Entity\Task'
        ));
    }

    // ...
}</pre><p>一个好的开始！如果你停止在这里，并提交表单，你的Task的issue属性就会是一个字符串(例如 55 )。你怎么把他变成一个实体提交呢？</p>
<p>&nbsp;</p>
<h3>创建转换器</h3>
<p>你应该像之前一样使用<tt class="docutils literal"><code>CallbackTransformer。但是由于这个逻辑有些复杂，创建一个转换器将会使得<tt class="docutils literal">TaskType表单类更加简单。</tt></code></tt></p>
<p>创建一个<tt class="docutils literal"><code>IssueToNumberTransformer类：他将会负责相互转化Issue编号和Issue实体：</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php
namespace AppBundle\Form\DataTransformer;

use AppBundle\Entity\Issue;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;

class IssueToNumberTransformer implements DataTransformerInterface
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this-&gt;manager = $manager;
    }

    /**
     * Transforms an object (issue) to a string (number).
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($issue)
    {
        if (null === $issue) {
            return '';
        }

        return $issue-&gt;getId();
    }

    /**
     * Transforms a string (number) to an object (issue).
     *
     * @param  string $issueNumber
     * @return Issue|null
     * @throws TransformationFailedException if object (issue) is not found.
     */
    public function reverseTransform($issueNumber)
    {
        // no issue number? It's optional, so that's ok
        if (!$issueNumber) {
            return;
        }

        $issue = $this-&gt;manager
            -&gt;getRepository('AppBundle:Issue')
            // query for the issue with this id
            -&gt;find($issueNumber)
        ;

        if (null === $issue) {
            // causes a validation error
            // this message is not shown to the user
            // see the invalid_message option
            throw new TransformationFailedException(sprintf(
                'An issue with number "%s" does not exist!',
                $issueNumber
            ));
        }

        return $issue;
    }
}</pre><p>就像第一个例子，转换器有两个方法。这个transform()负责将你代码中的数据转换为一个form表单渲染的数据格式（如：一个Issue对象转换为一个id字符串）。这个reverseTransform()方法正好相反：他将提交的数据转换成你代码想要的数据（如：把一个id转换为Issue对象）。</p>
<p>如果验证发生错误，你可以抛出<a href="http://api.symfony.com/2.7/Symfony/Component/Form/Exception/TransformationFailedException.html" rel="nofollow">TransformationFailedException</a>。但是这个异常信息就不要给你的用户看了。你使用invalid_message来设置消息（详见下面）。</p>
<blockquote><p>当null被传递到transform()方法时，你的转换器应该返回一个和它类型相等的值（例如：一个空字符串，整型的0，或者是浮点数0.0）。</p></blockquote>
<p>&nbsp;</p>
<h3>使用这个转换器</h3>
<p>下一步，你将在TaskType中实例化你的<tt class="docutils literal"><code>IssueToNumberTransformer类并添加他到issue字段。要做到这一点，你将需要一个实体管理（entity manager）（因为<tt class="docutils literal">IssueToNumberTransformer</tt>需要他）。</code></tt></p>
<p>没有问题！仅仅给TaskType添加一个__construct()函数并把它注册为一个服务传入entity管理即可：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

// ...
class TaskType extends AbstractType
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this-&gt;manager = $manager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('description', TextareaType::class)
            -&gt;add('issue', TextType::class, array(
                // validation message if the data transformer fails
                'invalid_message' =&gt; 'That is not a valid issue number',
            ));

        // ...

        $builder-&gt;get('issue')
            -&gt;addModelTransformer(new IssueToNumberTransformer($this-&gt;manager));
    }

    // ...
}</pre><p>在你的配置文件中定义一个表单类型作为一个服务：</p><pre class="crayon-plain-tag"># src/AppBundle/Resources/config/services.yml
services:
    app.form.type.task:
        class: AppBundle\Form\Type\TaskType
        arguments: ["@doctrine.orm.entity_manager"]
        tags:
            - { name: form.type }</pre><p></p>
<blockquote><p> 提示：更多表单类型注册为服务的信息，请阅读 <a class="reference internal" href="http://symfony.com/doc/current/book/forms.html#form-as-services">register your form type as a service</a>.</p></blockquote>
<p>现在，你能够很容易的使用你的TaskType：</p><pre class="crayon-plain-tag">// e.g. in a controller somewhere
$form = $this-&gt;createForm(TaskType::class, $task);

// ...</pre><p>酷，你完成了！你的用户将能够在text字段输入一个issue编号来把他转换成一个Issue对象。这意味着，在成功的提交之后，表单组件将会向 <strong>Task::setIssue()</strong> 传递一个真正的 <strong>Issue</strong> 对象而不是问题数字。</p>
<p>如果issue没有被找到的话，一个表单字段错误将会产生，并且invalid_message这个字段能够控制错误信息。</p>
<p>注意：当你添加一个转换器时你要小心。举例，下面代码是错误的，由于转换器将会被用于整个表单而不是仅仅这个字段：</p><pre class="crayon-plain-tag">// THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
// see above example for correct code
$builder-&gt;add('issue', TextType::class)
    -&gt;addModelTransformer($transformer);</pre><p>&nbsp;</p>
<p>&nbsp;</p>
<h3>创建一个可以重复使用的 issue_selector 字段：</h3>
<p>在上面的例子中，你转换了一个普通的text字段。但如果你要做很多这样的转换，最好是创建一个自定义的表单类型，他就可以自动完成。</p>
<p>首先，创建一个自定义的字段类型类：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/IssueSelectorType.php
namespace AppBundle\Form;

use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class IssueSelectorType extends AbstractType
{
    private $manager;

    public function __construct(ObjectManager $manager)
    {
        $this-&gt;manager = $manager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new IssueToNumberTransformer($this-&gt;manager);
        $builder-&gt;addModelTransformer($transformer);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver-&gt;setDefaults(array(
            'invalid_message' =&gt; 'The selected issue does not exist',
        ));
    }

    public function getParent()
    {
        return TextType::class;
    }
}</pre><p>好！他将像一个text字段一样渲染（getParent()做了指定），但他自动有一个数据转换器并默认配置invalid_message。</p>
<p>接下来，将你的类型注册为一个服务并标注form.type标签，这样他就被认定为是一个自定义的字段类型了：</p><pre class="crayon-plain-tag"># app/config/services.yml
services:
    app.type.issue_selector:
        class: AppBundle\Form\IssueSelectorType
        arguments: ['@doctrine.orm.entity_manager']
        tags:
            - { name: form.type }</pre><p>现在，无论什么使用你需要使用你的特殊issue_selector字段类型，他都非常的容易：</p><pre class="crayon-plain-tag">// src/AppBundle/Form/TaskType.php
namespace AppBundle\Form\Type;

use AppBundle\Form\DataTransformer\IssueToNumberTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
// ...

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            -&gt;add('description', TextareaType::class)
            -&gt;add('issue', IssueSelectorType::class)
        ;
    }

    // ...
}</pre><p>&nbsp;</p>
<p>&nbsp;</p>
<h3>关于Model（模型）和View Transformers(视图转换器)</h3>
<p>上面的例子的转换器是一个“Model”转换器。实时上，共有两种类型的转换器，又有三种不同类型的基础数据。</p>
<p><img class="alignnone size-full wp-image-984" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2016/05/DataTransformersTypes.png" alt="DataTransformersTypes" width="640" height="480" /></p>
<p>在任何表单中，都有三种不同类型的数据：</p>
<p>1. Model data （模型数据）- 这个数据在你的应用程序内部使用（例如一个Issue对象）。如果你调用Form::getData()或者Form::setData()，你就可以处理模型数据。</p>
<p>2.Norm Data (普通数据) &#8211; 这是一个你的普通版本数据，并且这个数据和你的modle数据一样常见（尽管我们的例子中没有）。她通常不会被直接应用。</p>
<p>3.View Data （视图数据） &#8211;  这是表单字段自动填充的数据格式。用户也很有可能提交这种格式的数据。当你调用 <strong>Form::submit($data)</strong>时，<strong>$data</strong> 就是“视图”格式的数据。</p>
<p>这两种不同类型的转换器可以帮助我们相互转换这些类型数据：</p>
<p><strong>Model transformers</strong>:</p>
<ul class="first last simple">
<li><tt class="docutils literal"><code>transform</code></tt>: &#8220;model data&#8221; =&gt; &#8220;norm data&#8221;</li>
<li><tt class="docutils literal"><code>reverseTransform</code></tt>: &#8220;norm data&#8221; =&gt; &#8220;model data&#8221;</li>
</ul>
<p><strong>View transformers</strong>:</p>
<ul class="first last simple">
<li><tt class="docutils literal"><code>transform</code></tt>: &#8220;norm data&#8221; =&gt; &#8220;view data&#8221;</li>
<li><tt class="docutils literal"><code>reverseTransform</code></tt>: &#8220;view data&#8221; =&gt; &#8220;norm data&#8221;</li>
</ul>
<p>你需要使用那种转换器取决于你的实际情况。</p>
<p>如果你想使用视图转换器（view transformer）就调用addViewTransformer。</p>
<p>&nbsp;</p>
<h3>为什么在这里要使用模型转换器？</h3>
<p>在这个例子中，字段类型是一个text，同时一个text字段总是比较简单，这个格式在“norm”和“view”中。因为在这里model转换器做适合转换（转换表单格式&#8212;-字符串issue编号&#8212;-模型格式&#8212;-Issue对象）。</p>
<p>转换器的区别是微妙的，你应该考虑‘norm’数据字段是什么样子。举例来说，text字段的普通数据就是一个字符串，但是一个date字段就是一个DataTime对象。</p>
<blockquote><p>提示：一个普遍的规律，规范化的数据应当包含尽可能多的信息。</p></blockquote>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/978">(表单)如何使用数据转换器(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/978/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)如何使用多个Guard验证器(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/966</link>
		<comments>http://www.newlifeclan.com/symfony/archives/966#comments</comments>
		<pubDate>Tue, 03 May 2016 02:51:18 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=966</guid>
		<description><![CDATA[<p>注意：Guard组件在symfony2.8被引入。这个Guard验证组件允许你一次轻松的使用许多不同的验证器。 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/966">(Security)如何使用多个Guard验证器(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>注意：Guard组件在symfony2.8被引入。这个Guard验证组件允许你一次轻松的使用许多不同的验证器。<span id="more-966"></span></p>
<p>Entry Point是一个服务id（你的验证器）start()方法被调用去启动验证过程。</p>
<h2>多个验证器分享这个Entry Point</h2>
<p>有时你想要去给你用户提供不同的验证机制，像一个登录表单还有一个facebook登录，Entry Point会将用户定向到登录页面。那么，你要在你的配置中告诉Entry Point，你要使用的机制。</p>
<p>这就是你配置的安全：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
     # ...
    firewalls:
        default:
            anonymous: ~
            guard:
                authenticators:
                    - app.form_login_authenticator
                    - app.facebook_connect_authenticator
                entry_point: app.form_login_authenticator</pre><p>对于这个方法这里是一个限制 &#8211; 你必须使用一个Entry Point。</p>
<p>&nbsp;</p>
<h2>多个验证器使用单独的Entry Points</h2>
<p>然而，有些案例需要你的验证器保护应用程序的不同部分。例如，你有一个登录表单保护你应用程序的前台部分安全区域还有api tokens保护api。你的一个防火墙只能配置一个Entry Point，解决方案是将配置分为两个独立的防火墙：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...
    firewalls:
        api:
            pattern: ^/api/
            guard:
                authenticators:
                    - app.api_token_authenticator
        default:
            anonymous: ~
            guard:
                authenticators:
                    - app.form_login_authenticator
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: ROLE_API_USER }
        - { path: ^/, roles: ROLE_USER }</pre><p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/966">(Security)如何使用多个Guard验证器(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/966/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Console)怎样去创建一个Console Command(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/876</link>
		<comments>http://www.newlifeclan.com/symfony/archives/876#comments</comments>
		<pubDate>Tue, 23 Feb 2016 05:37:46 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=876</guid>
		<description><![CDATA[<p>这个组件部分的Console页面（Console组件）介绍了如何创建一个Console Command。这篇文 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/876">(Console)怎样去创建一个Console Command(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>这个组件部分的Console页面（Console组件）介绍了如何创建一个Console Command。这篇文章涉及到在symfony框架中创建console commands。<span id="more-876"></span></p>
<h3>自动注册Commands</h3>
<p>为了能使symfony中的Console commands自动有效，来在你的bundle中创建一个Command目录并为每个Command创建一个php文件Command.php。例如，如果你想扩展AppBundle去生成你自己的命令行，创建GreetCommand.php并添加如下代码：</p><pre class="crayon-plain-tag">// src/AppBundle/Command/GreetCommand.php
namespace AppBundle\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;

class GreetCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            -&gt;setName('demo:greet')
            -&gt;setDescription('Greet someone')
            -&gt;addArgument(
                'name',
                InputArgument::OPTIONAL,
                'Who do you want to greet?'
            )
            -&gt;addOption(
                'yell',
                null,
                InputOption::VALUE_NONE,
                'If set, the task will yell in uppercase letters'
            )
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $name = $input-&gt;getArgument('name');
        if ($name) {
            $text = 'Hello '.$name;
        } else {
            $text = 'Hello';
        }

        if ($input-&gt;getOption('yell')) {
            $text = strtoupper($text);
        }

        $output-&gt;writeln($text);
    }
}</pre><p>现在，这个命令已经提供了，并可以运行：</p><pre class="crayon-plain-tag">$ php bin/console demo:greet Fabien</pre><p>&nbsp;</p>
<h3>在服务容器中注册Command</h3>
<p>就像控制器，命令可以声明为服务。请查看 <em><a class="reference internal" href="http://symfony.com/doc/current/cookbook/console/commands_as_services.html">dedicated cookbook entry</a>了解更多详情。</em></p>
<p>&nbsp;</p>
<h3>从服务容器中获取服务</h3>
<p>通过使用 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand" href="http://api.symfony.com/3.0/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.html">ContainerAwareCommand</a>这个基类作为command（而不是基于command），你可以直接访问服务容器。换句话说，你可以访问任何配置的服务：</code></tt></p><pre class="crayon-plain-tag">protected function execute(InputInterface $input, OutputInterface $output)
{
    $name = $input-&gt;getArgument('name');
    $logger = $this-&gt;getContainer()-&gt;get('logger');

    $logger-&gt;info('Executing command for '.$name);
    // ...
}</pre><p>&nbsp;</p>
<h3>调用其它命令</h3>
<p><a class="reference internal" href="http://symfony.com/doc/current/components/console/introduction.html#calling-existing-command">Calling an Existing Command</a>，如果你需要去实现运行一个命令，并关联其他命令。</p>
<p>&nbsp;</p>
<h3>测试命令</h3>
<p>当使用的测试命令作为框架的一部分，<a class="reference external" title="Symfony\Bundle\FrameworkBundle\Console\Application" href="http://api.symfony.com/3.0/Symfony/Bundle/FrameworkBundle/Console/Application.html">Symfony\Bundle\FrameworkBundle\Console\Application</a>应该替代<tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Console\Application" href="http://api.symfony.com/3.0/Symfony/Component/Console/Application.html">Symfony\Component\Console\Application</a></code></tt>:</p><pre class="crayon-plain-tag">use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use AppBundle\Command\GreetCommand;

class ListCommandTest extends \PHPUnit_Framework_TestCase
{
    public function testExecute()
    {
        // mock the Kernel or create one depending on your needs
        $application = new Application($kernel);
        $application-&gt;add(new GreetCommand());

        $command = $application-&gt;find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester-&gt;execute(
            array(
                'name'    =&gt; 'Fabien',
                '--yell'  =&gt; true,
            )
        );

        $this-&gt;assertRegExp('/.../', $commandTester-&gt;getDisplay());

        // ...
    }
}</pre><p></p>
<blockquote><p> 在特殊情况下，这个name参数和这个&#8211;yell选项不是强制性让命令去工作，但在显示时，你可以看到调用命令时如何自定义他们。</p></blockquote>
<p>为了能够在你的console test中使用全部服务容器，你能够从KernelTestCase中继承你的测试：</p><pre class="crayon-plain-tag">use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use AppBundle\Command\GreetCommand;

class ListCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = $this-&gt;createKernel();
        $kernel-&gt;boot();

        $application = new Application($kernel);
        $application-&gt;add(new GreetCommand());

        $command = $application-&gt;find('demo:greet');
        $commandTester = new CommandTester($command);
        $commandTester-&gt;execute(
            array(
                'name'    =&gt; 'Fabien',
                '--yell'  =&gt; true,
            )
        );

        $this-&gt;assertRegExp('/.../', $commandTester-&gt;getDisplay());

        // ...
    }
}</pre><p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/876">(Console)怎样去创建一个Console Command(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/876/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)ACL高级概念(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/766</link>
		<comments>http://www.newlifeclan.com/symfony/archives/766#comments</comments>
		<pubDate>Wed, 06 Jan 2016 06:42:34 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=766</guid>
		<description><![CDATA[<p>本章的目的是给出一个更深入的 ACL 系统的观点,并解释其背后的一些设计理念。 设计概念 Symfony2实例 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/766">(Security)ACL高级概念(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>本章的目的是给出一个更深入的 ACL 系统的观点,并解释其背后的一些设计理念。<span id="more-766"></span></p>
<h2 id="87eacb7825c3e44887cc2430c487919f">设计概念</h2>
<p>Symfony2实例化的安全功能对象是建立在访问控制列表概念之上的。每个域对象实例都有他们自己的ACL。ACL实例有着一个访问控制项的详细列表（ACEs），该列表用来指定访问权限。</p>
<p>Symfony2的ACL系统专注于两个主要目标：</p>
<ul>
<li>为你的域对象提供一个有效的方法去检索和更改大量的ACLs/ACEs。</li>
<li>提供一个方法，可以方便地确定用户是否被允许在一个域对象上具备执行相关操作的权限。</li>
</ul>
<p>第一点明确表明，Symfony2中ACL系统的主要功能之一就是高效检索ACLs/ACEs。这非常重要，因为每个ACL有多条ACEs、同时它还以类树型的方式从其他ACL中继承。因此，虽然我们没有指定ORM，但是默认直接使用Doctrine的DBAL与你的连接实现交互。</p>
<h3></h3>
<h3>Object Identities（对象标识）</h3>
<p>ACL系统是完全与你的域对象分离的。它们甚至不需要保存在同一个数据库或同一台主机中。为了实现这种分离，在ACL系统里你的对象被认为是对象标识。任何时候，在你想检索域对象的ACL时，ACL系统都会事先为你的域对象创建一个对象标识，然后将该对象标识传递到ACL提供器作进一步处理。</p>
<p>&nbsp;</p>
<h3>Security Identities（安全标识）</h3>
<p>与对象标识类似，但表现为你应用程序中的用户或角色。每个角色或用户都拥有各自的安全标识。</p>
<p>&nbsp;</p>
<h2>Database Table Structure（数据表结构）</h2>
<p>（ACL系统）使用下列五个数据表来实现。在一个典型的应用程序中这些表按记录数从小到大排列。</p>
<ul>
<li><em>acl_security_identities：该表记录所有拥有ACEs的安全标识（SID）。默认实现附带了两个安全标识：RoleSecurityIdentity和UserSecurityIdentity。</em></li>
<li><em>acl_classes</em>: 该表将类名映射成唯一id，该id可以被其他数据表引用。</li>
<li><em>acl_object_identities</em>:数据表中的每条记录都表示一个单独的域对象实例。</li>
<li><em>acl_object_identity_ancestors</em>: 该表允许我们用一种非常高效的方式去确定一条ACL的所有父类，也就是可以迭代地确定该ACL继承了哪些ACL。</li>
<li><em>acl_entries</em>:该数据表包含所有的ACEs。该表通常拥有最多的记录。在包含数千万条记录的情况下不会显著影响性能。</li>
</ul>
<p>&nbsp;</p>
<h2>访问控制项范围</h2>
<p>访问控制项在应用时有不同的范围。在Symfony2中我们有两个基本的范围。</p>
<ul>
<li>Class-Scope:  类范围：这些项应用于拥有相同类的所有对象上。</li>
<li>Object-Scope: 对象范围：在前面的章节中我们使用过这个范围，它仅用于指定的对象。</li>
</ul>
<p>有时候，你只能将ACE应用到对象的特定字段里。比如说，你想ID只能被管理员而不是客户服务查看。那么要解决这个问题，我们需要添加两个额外的子范围：</p>
<ul>
<li>Class-Field-Scope: 类字段范围：这些项应用于拥有相同类的所有对象上，但仅仅是对象的特定字段。</li>
<li>Object-Field-Scope: 对象字段范围：这些项应用于指定对象，但仅限于该对象的特定字段。</li>
</ul>
<p>&nbsp;</p>
<h2>Pre-Authorization Decisions（预授权决策）</h2>
<p>预授权决策是指任何方法或安全动作执行前就已判断，这证明AccessDecisionManager服务被使用，该服务也用于基于角色的达到授权决策。像角色一样，ACL系统添加一些新的属性去检查不同的权限。</p>
<h3>内建权限映射</h3>
<table class="docutils">
<thead valign="bottom">
<tr class="row-odd">
<th class="head">Attribute<br />
（属性）</th>
<th class="head">Intended Meaning<br />
（代表的意思）</th>
<th class="head">Integer Bitmasks<br />
（整数位掩码）</th>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even">
<td>VIEW</td>
<td>Whether someone is allowed to view the domain object.<br />
某人是否被允许查看域对象</td>
<td>VIEW, EDIT, OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-odd">
<td>EDIT</td>
<td>Whether someone is allowed to make changes to the domain object.<br />
某人是否被允许修改域对象</td>
<td>EDIT, OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-even">
<td>CREATE</td>
<td>Whether someone is allowed to create the domain object.<br />
某人是否被允许创建域对象</td>
<td>CREATE, OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-odd">
<td>DELETE</td>
<td>Whether someone is allowed to delete the domain object.<br />
某人是否被允许删除域对象</td>
<td>DELETE, OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-even">
<td>UNDELETE</td>
<td>Whether someone is allowed to restore a previously deleted domain object.<br />
某人是否被允许回复先前被删除的域对象</td>
<td>UNDELETE, OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-odd">
<td>OPERATOR</td>
<td>Whether someone is allowed to perform all of the above actions.<br />
某人是否被允许执行上述的操作。</td>
<td>OPERATOR, MASTER, or OWNER</td>
</tr>
<tr class="row-even">
<td>MASTER</td>
<td>Whether someone is allowed to perform all of the above actions, and in addition is allowed to grant any of the above permissions to others.<br />
某人是否被允许执行上述所有的操作，同时也具备将上述的任何一个权限授权给其他人。</td>
<td>MASTER, or OWNER</td>
</tr>
<tr class="row-odd">
<td>OWNER</td>
<td>Whether someone owns the domain object. An owner can perform any of the above actions <em>and</em>grant master and owner permissions.<br />
某人是否拥有该域对象。拥有者能执行上述所有的操作。</td>
<td>OWNER</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<h3>Permission Attributes vs. Permission Bitmasks（权限属性vs权限位掩码）</h3>
<p>AccessDecisionManager使用属性和使用角色是一样的。通常，这些属性其实是用一串整型位掩码的总和来表示的。另一方面在ACL系统内部，整数位掩码被用来在数据库中保存用户权限，并且使用位掩码操作执行权限检查非常快速。</p>
<h3>Extensibility（扩展性）</h3>
<p>上述权限映射并不一成不变，理论上完全可以随意替代。然而，它将涵盖你所遇到的绝大多数问题，并且可以与其它Bundle相互协作。因此，我们还是鼓励你，坚持我们对它的期望。</p>
<h3>Post Authorization Decisions（后授权决策）</h3>
<p>后授权决策是发生在安全方法执行之后，通常还包含该方法返回的域对象。在调用提供器之后照样允许去修改或过滤返回前的域对象。</p>
<p>由于PHP语言的限制，已授权功能还未被纳入到核心安全组件中。尽管如此，实验性质的 <a class="reference external" href="https://github.com/schmittjoh/JMSSecurityExtraBundle">JMSSecurityExtraBundle</a> 已经添加了这些功能。查看它的文档以便进一步了解它是怎么实现的。</p>
<h3>Process for Reaching Authorization Decisions （达到授权决策的过程）</h3>
<p>ACL类提供两种方法去判断一个安全标识是否具有要求的掩码，isGranted和isFieldGranted。当ACL从这两种方法之一接收到授权请求，它就把这个请求委派给PermissionGrantingStrategy的实现。这样就允许你替换掉已达到但未实际更改的ACL类自身的权限决策方式。</p>
<p>PermissionGrantingStrategy首先检查所有对象范围的ACEs，如果没有匹配，则检查类范围的ACEs，如果还未匹配，将会重复在父ACL上的ACEs的检查过程。如果父ACL不存在，则抛出异常。</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/766">(Security)ACL高级概念(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/766/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)怎样去使用Access Control Lists-ACLs(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/760</link>
		<comments>http://www.newlifeclan.com/symfony/archives/760#comments</comments>
		<pubDate>Wed, 30 Dec 2015 10:00:34 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=760</guid>
		<description><![CDATA[<p>在复杂的应用中，你可能经常面临访问权限决策不仅仅取决于请求者本人（Token）还牵涉到了被申请的对象的问题。这 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/760">(Security)怎样去使用Access Control Lists-ACLs(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>在复杂的应用中，你可能经常面临访问权限决策不仅仅取决于请求者本人（<strong>Token</strong>）还牵涉到了被申请的对象的问题。这也是 ACLs 系统被制作出来的原因所在。<span id="more-760"></span></p>
<h3>ACL的替代选择</h3>
<blockquote><p>使用ACL并不平凡，而对于较简单的情况下，他可能矫枉过正。如果你的许可逻辑可以通过一些简单的代码来完成（例如，检查博客是不是当前用户的）那么可以考虑使用voter。一个voter可以决定对象的访问，你能够使用它来完成复杂的决定，有效的实现自己的ACL。此外，强制授权（比如 isGranted 部分）就会看起来和你在本章所看到的这个条目极其相似，但是你的 voter 类就会在幕后控制判定逻辑了，而不是 ACL 系统。</p></blockquote>
<p>想象一下，你正在设计一个博客系统，而看你文章的人可以评论它。现在，你希望用户能够编辑自己的评论，而不是其他用户的；此外，你自己希望能够编辑所有人的评论。在这种情况下，你要对comment域对象限制访问。使用symfony你可以用几种方式来做到这一点，其中两个基本的方式是（非详尽）：</p>
<p>在你的逻辑方法中实现安全：基本上这意味着，评论最为参考，比对所有用户谁有权访问，并比对用户提供的Token。</p>
<p>角色来实现安全：这种方法是，为每一个评论对象添加一个角色，即ROLE_COMMENT_1，ROLE_COMMENT_2 等。</p>
<p>这两种方法都非常有效。然而，把你的验证代码和逻辑代码放到一起，会使他不能重用，并增加单元测试的难度。此外，如果很多用户会访问一个域对象，你的代码可能会遇到性能问题。</p>
<p>幸运的是，这里有一种更好的方式，你在下面就可以看到。</p>
<p>&nbsp;</p>
<h3>准备</h3>
<p>现在，在你终于要才去行动之前，你需要做一些准备。首先，你需要配置ACL系统的连接以供使用：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    acl:
        connection: default</pre><p></p>
<blockquote><p>标注：这个ACL系统必须有一个链接，链接可以是Doctrine DBAL（默认使用）也可以是Doctrine MongoDB（用于<a class="reference external" href="https://github.com/IamPersistent/MongoDBAclBundle">MongoDBAclBundle</a>）。然而，这并不意味着你必须使用Doctrine ORM或者ODM来映射你的域（domain）对象。你可以用你喜欢的任何方式来映射对象，比如 DoctrineORM，MongoDB ODM，Propel，rawSQL 等等。选择权在你手里。</p></blockquote>
<p>在连接方式确定好之后，你必须导入数据机构。幸运的是，这个任务，只需要一行命令：</p><pre class="crayon-plain-tag">$ php bin/console init:acl</pre><p>&nbsp;</p>
<h3>开始入门</h3>
<p>上面的场景，现在你就可以用acl去实现它。</p>
<p>一旦ACL被创建，你可以通过创建一个Access Control Entry（ACE）来巩固实体和你的用户之间的关系，来访问对象。</p>
<p>&nbsp;</p>
<p><strong>创建一个ACL并添加一个ACE</strong></p><pre class="crayon-plain-tag">// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;

class BlogController extends Controller
{
    // ...

    public function addCommentAction(Post $post)
    {
        $comment = new Comment();

        // ... setup $form, and submit data

        if ($form-&gt;isValid()) {
            $entityManager = $this-&gt;getDoctrine()-&gt;getManager();
            $entityManager-&gt;persist($comment);
            $entityManager-&gt;flush();

            // creating the ACL
            $aclProvider = $this-&gt;get('security.acl.provider');
            $objectIdentity = ObjectIdentity::fromDomainObject($comment);
            $acl = $aclProvider-&gt;createAcl($objectIdentity);

            // retrieving the security identity of the currently logged-in user
            $tokenStorage = $this-&gt;get('security.token_storage');
            $user = $tokenStorage-&gt;getToken()-&gt;getUser();
            $securityIdentity = UserSecurityIdentity::fromAccount($user);

            // grant owner access
            $acl-&gt;insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
            $aclProvider-&gt;updateAcl($acl);
        }
    }
}</pre><p>在这段代码中，有几个重点。在这里我着重讲两处：</p>
<p>首先，你可能会注意到-&gt;createAcl()没有直接传入域对象，但他实现了ObjectIdentityInterface。这个间接的步骤使你在没有实际的域对象实例时，也可以让ACL工作。这在你想要检查一大批对象的权限，将会起到很大的作用。</p>
<p>另一个有趣的部分就是调用-&gt;insertObjectAce()。在这个例子中，你允许当前登录用户去操作自己的评论。<strong>MaskBuilder::MASK_OWNER</strong> 是一个提前定义好的整数位掩码；不必担心掩码生成器会抽象大部分细节，但你能够在一个数据库行存储很多不同的权限，从性能上给予很多提升。</p>
<blockquote><p>提示：ACEs 的检查顺序是很有意义的，作为一项通用的准则，你应该在最开始设立更多的入口。</p></blockquote>
<p>&nbsp;</p>
<h3>访问检查</h3>
<p></p><pre class="crayon-plain-tag">// src/AppBundle/Controller/BlogController.php

// ...

class BlogController
{
    // ...

    public function editCommentAction(Comment $comment)
    {
        $authorizationChecker = $this-&gt;get('security.authorization_checker');

        // check for edit access
        if (false === $authorizationChecker-&gt;isGranted('EDIT', $comment)) {
            throw new AccessDeniedException();
        }

        // ... retrieve actual comment object, and do your editing here
    }
}</pre><p>在这个例子中，你检查用户是否有EDIT(编辑)权限。在内部，symfony映射权限到多个整数位掩码，并检查用户是否拥有这些码。</p>
<blockquote><p>你可以建立32位的基本权限-位掩码（根据你操作系统php会有所不同在30-32之间）。此外，你还可以定义累加权限。</p></blockquote>
<h2></h2>
<h2 id="899c7352bf19f01dfbe4e0ab1243e9e3">累加权限</h2>
<p>在上面的第一个例子中，你只授予用户<tt class="docutils literal"><code>OWNER权限。尽管这样会使用户有效的执行任意的操作，例如查看，编辑域对象等。但在某些情况下，您可能想要明确的指定这些权限。</code></tt></p>
<p>该<tt class="docutils literal"><code>MaskBuilder就能够很容易的创建这些位掩码，并把几个权限结合在一起：</code></tt></p><pre class="crayon-plain-tag">$builder = new MaskBuilder();
$builder
    -&gt;add('view')
    -&gt;add('edit')
    -&gt;add('delete')
    -&gt;add('undelete')
;
$mask = $builder-&gt;get(); // int(29)</pre><p>此整数位掩码可以被用来授予用户您在上面添加的基础权限：</p><pre class="crayon-plain-tag">$identity  =  new  UserSecurityIdentity ( 'johannes' ,  'AppBundle\Entity\User' ); 
$acl -&gt; insertObjectAce ( $identity ,  $mask );</pre><p>现在的用户可以查看，编辑，删除，并取消删除的对象。</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/760">(Security)怎样去使用Access Control Lists-ACLs(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/760/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)如何使用Voter检查用户权限(3.0)</title>
		<link>http://www.newlifeclan.com/symfony/archives/754</link>
		<comments>http://www.newlifeclan.com/symfony/archives/754#comments</comments>
		<pubDate>Tue, 29 Dec 2015 04:05:31 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=754</guid>
		<description><![CDATA[<p>在symfony中，你可以使用ACL模块来检测用户访问数据的权限，但是他过于复杂。你可以使用一个更简单的方式就 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/754">(Security)如何使用Voter检查用户权限(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>在symfony中，你可以使用<a href="http://symfony.com/doc/current/cookbook/security/acl.html">ACL模块</a>来检测用户访问数据的权限，但是他过于复杂。你可以使用一个更简单的方式就是自定义voter，他就像简单的条件语句。<span id="more-754"></span></p>
<blockquote><p>看看 <a href="http://symfony.com/doc/current/components/security/authorization.html" rel="nofollow">authorization</a> 这一章可以对 voter 有更深刻的理解。</p></blockquote>
<h3></h3>
<h3>Symfony如何使用voter</h3>
<p>为了使用voter，你应该了解symfony与voter的互动机制。所有的voter都要使用isGranted()方法调用，并在symfony接受授权检查（这个security.authorization_checker服务）。voter来决定用户是否可以访问这些资源。</p>
<p>根本上，symfony从所有的voter中获取响应，并根据应用程序的策略做最后的决定（允许或拒绝访问这些资源），策略主要有：affirmative, consensus和unanimous.</p>
<p>更多信息请查看<a class="reference internal" href="http://symfony.com/doc/current/components/security/authorization.html#components-security-access-decision-manager">the section about access decision managers</a>.</p>
<p>&nbsp;</p>
<h3>Voter Interface（接口）</h3>
<p>自定义一个voter需要实现接口 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Authorization\Voter\VoterInterface" href="http://api.symfony.com/3.0/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.html">VoterInterface</a>或者继承<tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Authorization\Voter\Voter" href="http://api.symfony.com/3.0/Symfony/Component/Security/Core/Authorization/Voter/Voter.html">Voter</a>，他们可以让创建voter变得简单。</code></tt></code></tt></p><pre class="crayon-plain-tag">abstract class Voter implements VoterInterface
{
    abstract protected function supports($attribute, $subject);
    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}</pre><p>在symfony2.8新增了Voter这个辅助类。他和早期版本的AbstractVoter类类似。</p>
<p>&nbsp;</p>
<h3>设置：在控制器检查访问</h3>
<p>假设你有一个post对象并且你需要决定当前用户是否可以编辑或是查看对象。在你的controller中，你需要检查访问，代码如下：</p><pre class="crayon-plain-tag">// src/AppBundle/Controller/PostController.php
// ...

class PostController extends Controller
{
    /**
     * @Route("/posts/{id}", name="post_show")
     */
    public function showAction($id)
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "view" access: calls all voters
        $this-&gt;denyAccessUnlessGranted('view', $post);

        // ...
    }

    /**
     * @Route("/posts/{id}/edit", name="post_edit")
     */
    public function editAction($id)
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "edit" access: calls all voters
        $this-&gt;denyAccessUnlessGranted('edit', $post);

        // ...
    }
}</pre><p>这个denyAccessUnlessGranted()方法（以及，简单的isGranted()方法）来唤起“voter”系统。现在，没有voter能够决定用户是否可以访问或者编辑一个Post。但是你可以使用自己的逻辑去创建你自己的voter来决定你想要的。</p>
<blockquote><p>denyAccessUnlessGranted()函数和这个isGranted()函数都是快捷方式他们都调用security.authorization_checker服务的<tt class="docutils literal"><code>isGranted()</code></tt></p></blockquote>
<p>&nbsp;</p>
<h3>创建一个自定义的Voter</h3>
<p>假设用普通的逻辑写法来决定用户是否可以访问或者编辑一个post对象是非常复杂的。例如，一个用户可以随时查看或者编辑他自己的post。或者这个post标记为公开，任何人都可以访问。但用一个voter就只需要这样做：</p><pre class="crayon-plain-tag">// src/AppBundle/Security/PostVoter.php
namespace AppBundle\Security;

use AppBundle\Entity\Post;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // these strings are just invented: you can use anything
    const VIEW = 'view';
    const EDIT = 'edit';

    protected function supports($attribute, $subject)
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
            return false;
        }

        // only vote on Post objects inside this voter
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token-&gt;getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        // you know $subject is a Post object, thanks to supports
        /** @var Post $post */
        $post = $subject;

        switch($attribute) {
            case self::VIEW:
                return $this-&gt;canView($post, $user);
            case self::EDIT:
                return $this-&gt;canEdit($post, $user);
        }

        throw new \LogicException('This code should not be reached!');
    }

    private function canView(Post $post, User $user)
    {
        // if they can edit, they can view
        if ($this-&gt;canEdit($post, $user)) {
            return true;
        }

        // the Post object could have, for example, a method isPrivate()
        // that checks a boolean $private property
        return !$post-&gt;isPrivate();
    }

    private function canEdit(Post $post, User $user)
    {
        // this assumes that the data object has a getOwner() method
        // to get the entity of the user who owns this data object
        return $user === $post-&gt;getOwner();
    }
}</pre><p>就是这样完成了！接下来，我们来配置他；</p>
<p>回顾一下，这里是上面的两个抽象方法：</p>
<p><strong>Voter::supports($attribute, $subject)</strong></p>
<p>当isGranted()（或者denyAccessUnlessGranted()）被调用的时候，他的首个参数是<tt class="docutils literal"><code>$attribute（如 ROLE_USER，edit），第二个参数（如果有的话）为$subject（如 null，一个Post对象）。你的工作就是确定你的voter的vote（票）的attribute/subject组合。如果你返回true，voteOnAttribute()将会被调用。否则，你的voter完成后，一些其他的voter会继续这个过程。在本例中，如果attribue是<tt class="docutils literal"><code>view</code></tt>或<tt class="docutils literal"><code>edit并且object是一个Post实例，那么</code></tt>你将返回true。</code></tt></p>
<p><strong>voteOnAttribute($attribute, $subject, TokenInterface $token)</strong></p>
<p>如果你从support()中返回true，这个方法就会被调用。你的工作很简单：返回true允许访问或者返回false拒绝访问。这个$token能够获取到当前的用户对象（如果有的话）。在本实例中，包含了所有复杂的业务逻辑，最终确定是否可以访问。</p>
<p>&nbsp;</p>
<h3>配置这个voter</h3>
<p>去注入这个voter进入security，你一定要把他生成为一个服务并且标记他为security.voter：</p><pre class="crayon-plain-tag"># app/config/services.yml
services:
    app.post_voter:
        class: AppBundle\Security\PostVoter
        tags:
            - { name: security.voter }
        # small performance boost
        public: false</pre><p>完成了！现在，当你传入view/edit和一个Post对象给isGranted(),你的voter将被执行并能够控制访问了。</p>
<p>&nbsp;</p>
<h3>在voter中检查角色（Roles）</h3>
<blockquote><p><tt class="docutils literal"><code>AccessDecisionManager是在2.8被引入的：在他之前使用会引起CircularReferenceException异常。在早期版本中，你一定要注入service_container，并且获取security.authorization_checker来使用isGranted().</code></tt></p></blockquote>
<p>如果你想从你的voter内调用isGranted() &#8211; 例如，你想看看当前用户是否有ROLE_SUPER_ADMIN角色。在这里你可以在你的voter中注入<tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Authorization\AccessDecisionManager" href="http://api.symfony.com/3.0/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.html">AccessDecisionManager</a>。你可以这样使用它，例如，始终允许有ROLE_SUPER_ADMIN的用户访问。</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Security/PostVoter.php

// ...
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class PostVoter extends Voter
{
    // ...

    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this-&gt;decisionManager = $decisionManager;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // ...

        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this-&gt;decisionManager-&gt;decide($token, array('ROLE_SUPER_ADMIN'))) {
            return true;
        }

        // ... all the normal voter logic
    }
}</pre><p>下一步，编辑services.yml注入security.access.decision_manager服务</p><pre class="crayon-plain-tag"># app/config/services.yml
services:
    app.post_voter:
        class: AppBundle\Security\PostVoter
        arguments: ['@security.access.decision_manager']
        public: false
        tags:
            - { name: security.voter }</pre><p>就是这样！调用<tt class="docutils literal"><code>AccessDecisionManager的decide()本质上和调用控制器或其他地方的isGranted()是一样的（他有点低级但是是voter所必须的）。</code></tt></p>
<blockquote><p>这个security.access.decision_manager是私有的。这意味着你不能直接从控制器访问：你只能将其注入到其他的服务中。那好吧：所有情况下都可以使用 <tt class="docutils literal"><code>security.authorization_checker除了voter。</code></tt></p></blockquote>
<p>&nbsp;</p>
<h3>更改这个Access Decision Strategy</h3>
<p>通常情况下，在任何时间里只有一个voter将投票vote（其他的将“弃权”，意味着supports()将返回false）。但在理论上，你可以创建多个voters分别投票（vote）给一个action或者对象。比如说，你有一个 voter 用来检测一个站点的会员是否已经超过了 18 岁。</p>
<p>为了处理这样的情况，access decision manager将使用access decision strategy。你可以根据你的需求配置。这里有三种可用的策略：</p>
<p><strong><tt class="docutils literal"><code>affirmative （default）</code></tt></strong></p>
<p>一个voter授权访问时，给予授权。</p>
<p><strong>consensus</strong></p>
<p>当大多数的voter都允许访问时，给予授权。</p>
<p><strong>unanimous</strong></p>
<p>只有所有 voters 都允许授权的时候给予授权。</p>
<p>&nbsp;</p>
<p>考虑上面的情况，也就是说我们所有的voter都允许访问才能允许授权，来读取post。在这种情况下，默认策略应该就不会在生效了，而是被<strong>unanimous</strong>所取代。你可以在安全配置里设置此参数：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    access_decision_manager:
        strategy: unanimous</pre><p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/754">(Security)如何使用Voter检查用户权限(3.0)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/754/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)如何使用Guard创建自定义的身份验证系统(2.8)</title>
		<link>http://www.newlifeclan.com/symfony/archives/716</link>
		<comments>http://www.newlifeclan.com/symfony/archives/716#comments</comments>
		<pubDate>Tue, 01 Dec 2015 03:59:55 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=716</guid>
		<description><![CDATA[<p>无论你是构建一个传统的登录表单，还是一个api token验证系统或者是你需要集成一些专有的单点登录系统，Gu [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/716">(Security)如何使用Guard创建自定义的身份验证系统(2.8)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>无论你是构建一个传统的登录表单，还是一个api token验证系统或者是你需要集成一些专有的单点登录系统，Guard组件能够很容易的做这些事情&#8230;.并且很有趣！<span id="more-716"></span></p>
<p>在这个例子中，你需要构建一个api token验证系统并且学习如何使用Guard来完成工作。</p>
<h3>创建一个User和一个User Provider</h3>
<p>不管你如何验证，你都需要创建一个User类并实现UserInterface并配置一个 <a class="reference internal" href="http://symfony.com/doc/2.8/cookbook/security/custom_provider.html"><em>user provider</em></a>.在本例中，用户使用doctrine存储在数据库中，并且每个用户都有一个apikey属性，使用这个api来访问他们的账户：</p><pre class="crayon-plain-tag">// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", unique=true)
     */
    private $username;

    /**
     * @ORM\Colum(type"string", unique=true)
     */
    private $apiKey;

    public function getUsername()
    {
        return $this-&gt;username;
    }

    public function getRoles()
    {
        return ['ROLE_USER'];
    }

    public function getPassword()
    {
    }
    public function getSalt()
    {
    }
    public function eraseCredentials()
    {
    }

    // more getters/setters
}</pre><p>该用户没有密码，如果你想允许这个用户登录时使用密码，你可以添加一个password属性（在表单登陆中会用到）</p>
<p>你的User类不一定要存储在Doctrine：看你的需要。</p>
<p>下一步，确保你配置了用户的&#8221;user provider&#8221;：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    providers:
        your_db_provider:
            entity:
                class: AppBundle:User

    # ...</pre><p>然而！你想了解关于这些的更多信息，请查阅：</p>
<ul class="simple">
<li><a class="reference internal" href="http://symfony.com/doc/2.8/cookbook/security/entity_provider.html"><em>How to Load Security Users from the Database (the Entity Provider)</em></a></li>
<li><a class="reference internal" href="http://symfony.com/doc/2.8/cookbook/security/custom_provider.html"><em>How to Create a custom User Provider</em></a></li>
</ul>
<p>&nbsp;</p>
<h3>步骤1）创建Authenticator类</h3>
<p>假设你有一个api，你的客户端将针对每个请求发送一个X-AUTH-TOKEN头并跟随API token。你的任务就是读取他们并找到相关用户（如果他们有的话）。</p>
<p>创建一个自定义验证系统，仅仅需要创建一个类并让他实现 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Guard\GuardAuthenticatorInterface" href="http://api.symfony.com/2.8/Symfony/Component/Security/Guard/GuardAuthenticatorInterface.html">GuardAuthenticatorInterface</a></code></tt>.或者继承这个更简单的<tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Guard\AbstractGuardAuthenticator" href="http://api.symfony.com/2.8/Symfony/Component/Security/Guard/AbstractGuardAuthenticator.html">AbstractGuardAuthenticator</a></code></tt>.他需要你去实现六个方法。</p><pre class="crayon-plain-tag">// src/AppBundle/Security/TokenAuthenticator.php
namespace AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Doctrine\ORM\EntityManager;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    private $em;

    public function __construct(EntityManager $em)
    {
        $this-&gt;em = $em;
    }

    /**
     * Called on every request. Return whatever credentials you want,
     * or null to stop authentication.
     */
    public function getCredentials(Request $request)
    {
        if (!$token = $request-&gt;headers-&gt;get('X-AUTH-TOKEN')) {
            // no token? Return null and no other methods will be called
            return;
        }

        // What you return here will be passed to getUser() as $credentials
        return array(
            'token' =&gt; $token,
        );
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $apiToken = $credentials['token'];

        // if null, authentication will fail
        // if a User object, checkCredentials() is called
        return $this-&gt;em-&gt;getRepository('AppBundle:User')
            -&gt;findOneBy(array('apiToken' =&gt; $apiToken));
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        // check credentials - e.g. make sure the password is valid
        // no credential check is needed in this case

        // return true to cause authentication success
        return true;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // on success, let the request continue
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $data = array(
            'message' =&gt; strtr($exception-&gt;getMessageKey(), $exception-&gt;getMessageData())

            // or to translate this message
            // $this-&gt;translator-&gt;trans($exception-&gt;getMessageKey(), $exception-&gt;getMessageData())
        );

        return new JsonResponse($data, 403);
    }

    /**
     * Called when authentication is needed, but it's not sent
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $data = array(
            // you might translate this message
            'message' =&gt; 'Authentication Required'
        );

        return new JsonResponse($data, 401);
    }

    public function supportsRememberMe()
    {
        return false;
    }
}</pre><p>干的好！下面有每种方法的解释（Guard 验证方法）</p>
<p>&nbsp;</p>
<p>步骤2）配置这个验证</p>
<p>要完成他，需要注册这个类为一个服务：</p><pre class="crayon-plain-tag"># app/config/services.yml
services:
    app.token_authenticator:
        class: AppBundle\Security\TokenAuthenticator
        arguments: ['@doctrine.orm.entity_manager']</pre><p>最后，在security.yml中配置你的firewalls键来开启并使用这个验证：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    firewalls:
        # ...

        main:
            anonymous: ~
            logout: ~

            guard:
                authenticators:
                    - app.token_authenticator

            # if you want, disable storing the user in the session
            # stateless: true

            # maybe other things, like form_login, remember_me, etc
            # ...</pre><p>你做到了！你现在有一个有效的api token 验证系统了。如果你的首页需要ROLE_USER,那么你可以在不同条件下进行测试：</p><pre class="crayon-plain-tag"># test with no token
curl http://localhost:8000/
# {"message":"Authentication Required"}

# test with a bad token
curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/
# {"message":"Username could not be found."}

# test with a working token
curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/
# the homepage controller is executed: the page loads normally</pre><p>现在，开始了解每一个方法。</p>
<p>&nbsp;</p>
<h3>Guard 验证方法</h3>
<p>每个验证都需要一下方法：</p>
<p><strong>getCredentials(Request $request)</strong></p>
<p>他会获得每一个请求，你要从中获得token（或者任何你身份验证的信息）并将其返回。如果返回null，其余的验证部分就会被跳过。否则getUser()将会被调用并且返回值会作为第一个参数。</p>
<p><strong>getUser($credentials, UserProviderInterface $userProvider)</strong></p>
<p>如果getCredentials()返回一个非空值，则此方法会被调用，并传入这个返回值<tt class="docutils literal"><code>$credentials作为参数。你的任务是实现UserInterface并返回一个对象。如果你这么做那么checkCredentials()就会被调用。如果返回null（或者抛出<a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/guard-authentication.html#guard-customize-error">AuthenticationException</a>）验证宣告失败。</code></tt></p>
<p><strong>checkCredentials($credentials, UserInterface $user)</strong></p>
<p>如果getUser成功返回一个User对象，该方法就会被调用。你的任务是验证credentials是否正确。对于登陆表单，在这里会检查用户的密码是否正确。如果通过验证，则返回true。如果你返回其他的东西（或者抛出<a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/guard-authentication.html#guard-customize-error">AuthenticationException</a>）验证失败。</p>
<p><strong>onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)</strong></p>
<p>这个会在成功授权之后调用，你的任务是返回一个Response对象并发送到客户端，或者返回null继续这个请求（例如，通常会允许请求的路由器/控制器继续被调用）。由于这个是一个api，每个请求都会自我验证，你可能回去返回null。</p>
<p><strong>onAuthenticationFailure(Request $request, AuthenticationException $exception)</strong></p>
<p>如果验证失败会返回这个方法。你的工作是返回一个Response对象并发送到客户端。这个 <tt class="docutils literal"><code>$exception将告诉你什么验证过程出的错。</code></tt></p>
<p><strong>start</strong></p>
<p>如果客户端访问一个需要验证的URI/资源他就会被调用，但没有发送验证细节信息（例如，你从getCredentials()返回null）。你的任务是返回一个Response对象帮助用户验证（例如 一个401相应，上面写着“令牌丢了”）。</p>
<p><strong>supportsRememberMe</strong></p>
<p>如果你希望支持“remember me”功能，这个方法就要返回true。你仍需要在防火墙下启用rememebe_me才能工作。由于这是一个无状态的api，你在这个例子中就不需要支持remember me功能了。</p>
<p>&nbsp;</p>
<p><strong>自定义错误信息</strong></p>
<p>当onAuthenticationFailure()被调用，他就会传入一个<tt class="docutils literal"><code>AuthenticationException，可以使用$e-&gt;getMessageKey()（或$e-&gt;getMessageData()）方法来描述你怎样验证失败的。这些信息将基于不同的验证失败产生（例如getUser与checkCredentials()）。</code></tt></p>
<p>但是，你也可以很容易的返回一个自定义的消息使用<a class="reference external" title="Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException" href="http://api.symfony.com/3.0/Symfony/Component/Security/Core/Exception/CustomUserMessageAuthenticationException.html">CustomUserMessageAuthenticationException</a>。你能够抛出由<tt class="docutils literal"><code>getCredentials()</code></tt>, <tt class="docutils literal"><code>getUser()</code></tt> 和 <tt class="docutils literal"><code>checkCredentials()导致的失败：</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Security/TokenAuthenticator.php
// ...

use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    // ...

    public function getCredentials(Request $request)
    {
        // ...

        if ($token == 'ILuvAPIs') {
            throw new CustomUserMessageAuthenticationException(
                'ILuvAPIs is not a real API key: it\'s just a silly phrase'
            );
        }

        // ...
    }

    // ...
}</pre><p>这种情况下，由于&#8221;ILuvAPIs&#8221;是一个荒唐的api键，如果有人试图这样你可以返回一个“复活节的彩蛋”消息</p><pre class="crayon-plain-tag">curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/
# {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"}</pre><p>&nbsp;</p>
<h3>常见问题</h3>
<p><strong>我们可以有多个验证吗？</strong></p>
<p>可以！但是当你这样做时，你需要选择一个验证的“entry_point”.这意味着当一个匿名用户试图访问受保护的资源时，你需要选择验证应该调用的start方法。例如，假设你有一个app.form_login_authenticator来处理传统的登陆。当匿名用户访问受保护的页面时，你就要使用form authenticator的start()方法，并将其重定向到登陆页面（而不是返回JOSN响应）：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    firewalls:
        # ...

        main:
            anonymous: ~
            logout: ~

            guard:
                authenticators:
                    - app.token_authenticator

            # if you want, disable storing the user in the session
            # stateless: true

            # maybe other things, like form_login, remember_me, etc
            # ...</pre><p><strong>我可以使用“form_login”吗？</strong></p>
<p>可以！form_login是一种验证方式，所以你可以使用它，并还可以添加更多的验证。使用一个guard authenticator并不与其他的验证方法相冲突。</p>
<p><strong>你可以使用FOSUserBundle？</strong></p>
<p>可以！其实，FOSUserBundle不处理安全：他只是给你一个用户对象和一些路由和控制器，以帮助你完成登陆，注册，忘记密码等，当你使用FOSUserBundle，您通常使用form_login来验证身份。你可以继续这样做(参考前面的问题)，或使用FOSUserBundle的User对象并创建我们自己的验证（就像本章一样）。</p>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/716">(Security)如何使用Guard创建自定义的身份验证系统(2.8)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/716/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)怎样创建一个自定义的Authentication Provider(2.7)</title>
		<link>http://www.newlifeclan.com/symfony/archives/710</link>
		<comments>http://www.newlifeclan.com/symfony/archives/710#comments</comments>
		<pubDate>Fri, 27 Nov 2015 09:38:13 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=710</guid>
		<description><![CDATA[<p>创建一个自定义的验证系统很难，本章将引导您来完成这一过程。但根据您的需求，也许能让你的问题解决起来简单一些，或 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/710">(Security)怎样创建一个自定义的Authentication Provider(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>创建一个自定义的验证系统很难，本章将引导您来完成这一过程。但根据您的需求，也许能让你的问题解决起来简单一些，或者使用一些社区的bundle：<span id="more-710"></span></p>
<ul class="last simple">
<li><a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/custom_password_authenticator.html"><em>How to Create a Custom Form Password Authenticator</em></a></li>
<li><a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/api_key_authentication.html"><em>How to Authenticate Users with API Keys</em></a></li>
<li>如果使用第三方服务OAuth 如：Google, Facebook or Twitter, 你可以尝试使用 <a class="reference external" href="https://github.com/hwi/HWIOAuthBundle">HWIOAuthBundle</a> .</li>
</ul>
<p>如果您已经也读了book的安全一章，您就已经理解了在symfony中实现安全的验证和授权的不同。本章将讨论在身份验证过程中所涉及的核心类以及如何实现一个自定义的身份验证提供者Authentication Provider。由于身份验证和授权是不同的概念，此扩展为未知的user-provider，并且会跟您应用程序的user providers一同运行，并且他们可能基于内存，数据库，或者您希望保存的其他地方。</p>
<h3>符合 WSSE</h3>
<p>下面的章节将演示如何创建一个自定义的authentication provider来完成WSSE验证。这个WSSE安全协议提供了几个好处：</p>
<p>1. 用户名/密码 加密</p>
<p>2. 安全的防范再次攻击</p>
<p>3. 不需要服务器配置</p>
<p>WSSE来保护像SOAP 和REST这样的web服务是非常有用的。</p>
<p>目前有很多关于<a href="http://www.xml.com/pub/a/2003/12/17/dive.html">WSSE</a>的文档，但在这章中我们的焦点不在安全协议上，而是把一个自定义的协议添加到我们自己的symfony应用程序中。WSSE基础：在一个请求头（request header）中检查加密凭证，使用timestamp和 <a class="reference external" href="https://en.wikipedia.org/wiki/Cryptographic_nonce">nonce</a>来核实，并且使用密码报文来为发出请求的用户进行身份验证。</p>
<blockquote><p>WSSE 还支持应用程序密钥验证，这对 web 服务非常有用，但是该内容超出了本章的介绍范围.</p></blockquote>
<h3></h3>
<h3>Token</h3>
<p>这个token在symfony的security context中起到重要角色。token代表当前请求用户验证数据。一旦请求被认证，token会保留用户的数据，并传递这些数据到security context。首先，你需要创建你的token类。这将允许传递所有的相关信息到你的验证提供者（authentication provider）。</p><pre class="crayon-plain-tag">// src/AppBundle/Security/Authentication/Token/WsseUserToken.php
namespace AppBundle\Security\Authentication\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class WsseUserToken extends AbstractToken
{
    public $created;
    public $digest;
    public $nonce;

    public function __construct(array $roles = array())
    {
        parent::__construct($roles);

        // If the user has roles, consider it authenticated
        $this-&gt;setAuthenticated(count($roles) &gt; 0);
    }

    public function getCredentials()
    {
        return '';
    }
}</pre><p></p>
<blockquote><p> 这个WsseUserToken类继承自安全组件AbstractToken类，来提供基础的token功能。实现TokenInterface上的任何类作为一个token。</p></blockquote>
<p>&nbsp;</p>
<h3>监听器</h3>
<p>接下来，您需要一个监听器来监听防火墙。这个监听器负责监听向防火墙发来的请求，并调用authentication provider。一个监听器必须是一个 <a href="http://api.symfony.com/2.7/Symfony/Component/Security/Http/Firewall/ListenerInterface.html" rel="nofollow">ListenerInterface</a>的实例。一个安全监听应该处理 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\HttpKernel\Event\GetResponseEvent" href="http://api.symfony.com/2.7/Symfony/Component/HttpKernel/Event/GetResponseEvent.html">GetResponseEvent</a>事件，如果验证成功，设置一个验证token保存在token storage中。</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Security/Firewall/WsseListener.php
namespace AppBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use AppBundle\Security\Authentication\Token\WsseUserToken;

class WsseListener implements ListenerInterface
{
    protected $tokenStorage;
    protected $authenticationManager;

    public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager)
    {
        $this-&gt;tokenStorage = $tokenStorage;
        $this-&gt;authenticationManager = $authenticationManager;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event-&gt;getRequest();

        $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
        if (!$request-&gt;headers-&gt;has('x-wsse') || 1 !== preg_match($wsseRegex, $request-&gt;headers-&gt;get('x-wsse'), $matches)) {
            return;
        }

        $token = new WsseUserToken();
        $token-&gt;setUser($matches[1]);

        $token-&gt;digest   = $matches[2];
        $token-&gt;nonce    = $matches[3];
        $token-&gt;created  = $matches[4];

        try {
            $authToken = $this-&gt;authenticationManager-&gt;authenticate($token);
            $this-&gt;tokenStorage-&gt;setToken($authToken);

            return;
        } catch (AuthenticationException $failed) {
            // ... you might log something here

            // To deny the authentication clear the token. This will redirect to the login page.
            // Make sure to only clear your token, not those of other authentication listeners.
            // $token = $this-&gt;tokenStorage-&gt;getToken();
            // if ($token instanceof WsseUserToken &amp;&amp; $this-&gt;providerKey === $token-&gt;getProviderKey()) {
            //     $this-&gt;tokenStorage-&gt;setToken(null);
            // }
            // return;
        }

        // By default deny authorization
        $response = new Response();
        $response-&gt;setStatusCode(Response::HTTP_FORBIDDEN);
        $event-&gt;setResponse($response);
    }
}</pre><p>这个监听器检查这个请求的预期头X-WSSE，比对这个值并返回WSSE信息，使用这个信息创建一个token，并且传递这个token到验证管理。如果没有提供正确的信息，或身份验证管理抛出一个 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Exception\AuthenticationException" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/Exception/AuthenticationException.html">AuthenticationException</a>，将会返回一个403页面。</code></tt></p>
<blockquote><p>在上面的过程中没有使用到 <a href="http://api.symfony.com/2.7/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.html" rel="nofollow">AbstractAuthenticationListener</a> 类，它是一个非常有用并且为安全性扩展插件提供了常用功能的基类。其中包括在 session 中维持令牌功能，提供成功 / 失败的处理程序、 登录表单的 URL，以及更多的功能。因为 WSSE 不需要在 session 中 保持身份验证或登录表单，所以在本实例中没有用到它。</p>
<p>你如果你想链接验证提供者你可以让监听早一点返回（例如允许匿名用户）。如果您想要禁止匿名用户访问，并且能够较好地展示 403 错误，则应在返回结果之前设置响应的状态码。</p></blockquote>
<p>&nbsp;</p>
<h2>Authentication Provider</h2>
<p>这个authentication provider需要做的就是核实WsseUserToken。 也就是说，这个provider （验证器）要在5分钟之内验证Created头部的值是否有效，在五分钟里Nonce头的值是唯一的，并且<tt class="docutils literal"><code>PasswordDigest头的值要匹配用户密码。</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Security/Authentication/Provider/WsseProvider.php
namespace AppBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use AppBundle\Security\Authentication\Token\WsseUserToken;
use Symfony\Component\Security\Core\Util\StringUtils;

class WsseProvider implements AuthenticationProviderInterface
{
    private $userProvider;
    private $cacheDir;

    public function __construct(UserProviderInterface $userProvider, $cacheDir)
    {
        $this-&gt;userProvider = $userProvider;
        $this-&gt;cacheDir     = $cacheDir;
    }

    public function authenticate(TokenInterface $token)
    {
        $user = $this-&gt;userProvider-&gt;loadUserByUsername($token-&gt;getUsername());

        if ($user &amp;&amp; $this-&gt;validateDigest($token-&gt;digest, $token-&gt;nonce, $token-&gt;created, $user-&gt;getPassword())) {
            $authenticatedToken = new WsseUserToken($user-&gt;getRoles());
            $authenticatedToken-&gt;setUser($user);

            return $authenticatedToken;
        }

        throw new AuthenticationException('The WSSE authentication failed.');
    }

    /**
     * This function is specific to Wsse authentication and is only used to help this example
     *
     * For more information specific to the logic here, see
     * https://github.com/symfony/symfony-docs/pull/3134#issuecomment-27699129
     */
    protected function validateDigest($digest, $nonce, $created, $secret)
    {
        // Check created time is not in the future
        if (strtotime($created) &gt; time()) {
            return false;
        }

        // Expire timestamp after 5 minutes
        if (time() - strtotime($created) &gt; 300) {
            return false;
        }

        // Validate that the nonce is *not* used in the last 5 minutes
        // if it has, this could be a replay attack
        if (file_exists($this-&gt;cacheDir.'/'.$nonce) &amp;&amp; file_get_contents($this-&gt;cacheDir.'/'.$nonce) + 300 &gt; time()) {
            throw new NonceExpiredException('Previously used nonce detected');
        }
        // If cache directory does not exist we create it
        if (!is_dir($this-&gt;cacheDir)) {
            mkdir($this-&gt;cacheDir, 0777, true);
        }
        file_put_contents($this-&gt;cacheDir.'/'.$nonce, time());

        // Validate Secret
        $expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));

        return StringUtils::equals($expected, $digest);
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof WsseUserToken;
    }
}</pre><p></p>
<blockquote><p>  <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/Authentication/Provider/AuthenticationProviderInterface.html">AuthenticationProviderInterface</a>需要一个authenticate方法验证用户token，和一种能够告诉authentication manager是否为给定的token使用provider的<tt class="docutils literal">supports</tt>方法。在众多提供程序中，身份验证管理器会根据列表依次移动到每个提供程序。</code></tt></p>
<p>预期的比较和提供的报文会使用<tt class="docutils literal"><code>StringUtils类 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\Util\StringUtils::equals()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/Util/StringUtils.html#method_equals">equals()</a>方法提供的恒定的时间比较。它的作用是用来减少可能的 <a class="reference external" href="https://en.wikipedia.org/wiki/Timing_attack">timing attacks</a>。</code></tt></code></tt></p></blockquote>
<p>&nbsp;</p>
<h3>工厂模式</h3>
<p>你已经创建一个自定义的token，定义了监听，并且定义了provider（提供者）。现在你需要把他们放到一起。如何做才能为每个防火墙提供一个独特的provider程序呢？答案是通过使用工厂。工厂是你在安全组件之前处理（hook），告诉他你的提供者名称和它的所有配置选项。首先，你一定要创建一个类并实现<tt class="docutils literal"><code><a class="reference external" title="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface" href="http://api.symfony.com/2.7/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html">SecurityFactoryInterface</a></code></tt>.</p><pre class="crayon-plain-tag">// src/AppBundle/DependencyInjection/Security/Factory/WsseFactory.php
namespace AppBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;

class WsseFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.wsse.'.$id;
        $container
            -&gt;setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider'))
            -&gt;replaceArgument(0, new Reference($userProvider))
        ;

        $listenerId = 'security.authentication.listener.wsse.'.$id;
        $listener = $container-&gt;setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'wsse';
    }

    public function addConfiguration(NodeDefinition $node)
    {
    }
}</pre><p>&nbsp;</p>
<p><a href="http://api.symfony.com/2.7/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.html" rel="nofollow">SecurityFactoryInterface</a> 需要下列方法:</p>
<p><strong>create</strong> 方法</p>
<p>这个方法把这个监听和authentication provider添加到security context适合的依赖容器。</p>
<p><strong>getPosition</strong> 方法</p>
<p>当provider被调用时返回。他能够返回一个<tt class="docutils literal"><code>pre_auth</code></tt>,<tt class="docutils literal"><code>form</code></tt>, <tt class="docutils literal"><code>http</code></tt>或<tt class="docutils literal"><code>remember_me</code></tt>.</p>
<p><strong>getKey</strong> 方法</p>
<p>该方法的定义是用来引用防火墙配置中的提供程序的配置键。</p>
<p><strong>addConfiguration</strong> 方法</p>
<p>该方法用于定义您安全配置中的配置键下的配置选项。在后面将要介绍设置配置选项。</p>
<blockquote><p>在本例中，一直没有使用 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory" href="http://api.symfony.com/2.7/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.html">AbstractFactory</a>类，他是一个非常有用的基类，并且他为安全工厂模式提供了常用的功能。当定义不同类型的验证提供者（authentication provider）时，它非常有用。</code></tt></p></blockquote>
<p>现在，您已经创建了一个工厂类，在您的安全配置中wsse键可以作为一个防火墙。</p>
<p>你可能会问，“您为什么需要一个特殊的工厂类，将监听和提供者添加到依赖注入容器呢？”这是一个非常好的问题。原因是，你可以多次使用你的防火墙，来保护您应用程序的各个部分。正因为如此，每次使用防火墙时，再依赖容器中便会创建一个新服务。工厂的作用就是创建这些新服务。</p>
<h3>配置</h3>
<p>是时候该看看你的authentication provider在action中的程序。为了让他工作，你需要做几件事。你要做的第一件事就是将上面的服务添加到依赖容器。这个工厂类中的服务id还不存在： <tt class="docutils literal"><code>wsse.security.authentication.provider和</code></tt>d<tt class="docutils literal"><code>wsse.security.authentication.listener。现在是时候来定义这些服务了。</code></tt></p><pre class="crayon-plain-tag"># src/AppBundle/Resources/config/services.yml  -----
services:
    wsse.security.authentication.provider:
        class: AppBundle\Security\Authentication\Provider\WsseProvider
        arguments: ["", "%kernel.cache_dir%/security/nonces"]

    wsse.security.authentication.listener:
        class: AppBundle\Security\Firewall\WsseListener
        arguments: ["@security.token_storage", "@security.authentication.manager"]</pre><p>到现在，您的服务已经定义好了，现在可以把您的bundle类中的工厂告诉您的安全环境:</p><pre class="crayon-plain-tag">// src/AppBundle/AppBundle.php
namespace AppBundle;

use AppBundle\DependencyInjection\Security\Factory\WsseFactory;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $extension = $container-&gt;getExtension('security');
        $extension-&gt;addSecurityListenerFactory(new WsseFactory());
    }
}</pre><p>这样我们就完成了配置！您现在可以在 WSSE 的保护下以定义您的应用程序了。</p><pre class="crayon-plain-tag"># ------
security:
    firewalls:
        wsse_secured:
            pattern:   /api/.*
            stateless: true
            wsse:      true</pre><p>祝贺您！您已经完成了您的定义安全身份验证提供程序的编写！</p>
<p>&nbsp;</p>
<h3>补充</h3>
<p>为什么不让你的WSSE authentication provider 更加精彩呢？可能性是很多的。你为什么不加入一些闪烁的光芒呢？</p>
<p>&nbsp;</p>
<h3>配置</h3>
<p>您可以在您的安全配置中的 wsse 键下添加自定义选项。举个例子，允许Created头部项的默认时间是5分钟。使用配置，可以让不同的防火墙有不同的超时时间。</p>
<p>首先，你需要编辑WsseFactory并且在addConfiguration方法中添加新选项；</p><pre class="crayon-plain-tag">class WsseFactory implements SecurityFactoryInterface
{
    // ...

    public function addConfiguration(NodeDefinition $node)
    {
      $node
        -&gt;children()
        -&gt;scalarNode('lifetime')-&gt;defaultValue(300)
        -&gt;end();
    }
}</pre><p>现在，在工厂的create方法中，这个$config参数将包含一个lifetime键，设置为5分钟（300秒）除非在配置中设置了其他时间。然后向您的身份验证提供程序（authentication provider）传递此参数来使用它。</p><pre class="crayon-plain-tag">class WsseFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.wsse.'.$id;
        $container
            -&gt;setDefinition($providerId,
              new DefinitionDecorator('wsse.security.authentication.provider'))
            -&gt;replaceArgument(0, new Reference($userProvider))
            -&gt;replaceArgument(2, $config['lifetime']);
        // ...
    }

    // ...
}</pre><p></p>
<blockquote><p> 您还需要为 wsse.security.authentication.provider 服务配置添加第三个参数，它可以是空白的，但必须在工厂的生存期内填充。WsseProvider类的构造函数还需要第三个参数-lifetime &#8211; 它用来替代硬编码的300秒。这两个步骤都没有在这里显示。</p></blockquote>
<p>每个WSSE请求的生命周期都是可配置的，并且可以被设置为每个防火墙都期望的值。</p><pre class="crayon-plain-tag">security:
    firewalls:
        wsse_secured:
            pattern:   /api/.*
            stateless: true
            wsse:      { lifetime: 30 }</pre><p>剩下的就靠你了！在工厂中你可以定义任何相关的配置，在容器中消耗或传递到其他类。</p>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/710">(Security)怎样创建一个自定义的Authentication Provider(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/710/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)安全系统如何从数据库中读取用户-Entity Provider(2.7)</title>
		<link>http://www.newlifeclan.com/symfony/archives/702</link>
		<comments>http://www.newlifeclan.com/symfony/archives/702#comments</comments>
		<pubDate>Thu, 26 Nov 2015 06:59:30 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=702</guid>
		<description><![CDATA[<p>symfony的安全系统可以从任何地方加载用户 &#8211; 一个数据库和一个OAuth服务等。这一章告诉我 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/702">(Security)安全系统如何从数据库中读取用户-Entity Provider(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>symfony的安全系统可以从任何地方加载用户 &#8211; 一个数据库和一个OAuth服务等。这一章告诉我们如何使用Doctrine entity从数据库加载用户信息。<span id="more-702"></span></p>
<p>概述：再开始之前，你应该看看FOSUserBundle。这个外部的bundle允许你从数据库加载用户（就像本文要讲的），并为您提供了内置的路由和控制器等，来完成登陆，注册，忘记密码。但是，如果你的用户系统需要大量功能或者你想学学他是如何工作的，本教程正好讲解这些。</p>
<p>通过Doctrine entity 加载用户需要2个步骤：</p>
<p>1.创建你自己的用户实体（User Entity）</p>
<p>2. 配置 security.yml 加载你的实体（Entity）</p>
<p>之后，您还可以了解更多例如：禁止不活跃的用户，使用自定义查询和把用户序列化到session</p>
<p>&nbsp;</p>
<h3>1）创建你自己的用户实体（User Entity）</h3>
<p>假设你在<tt class="docutils literal"><code>AppBundle中</code></tt>已经有了一个User实体，它含有一下字段：id，username，password，email和isActive：</p><pre class="crayon-plain-tag">// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Table(name="app_users")
 * @ORM\Entity(repositoryClass="AppBundle\Entity\UserRepository")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=25, unique=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=64)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=60, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive;

    public function __construct()
    {
        $this-&gt;isActive = true;
        // may not be needed, see section on salt below
        // $this-&gt;salt = md5(uniqid(null, true));
    }

    public function getUsername()
    {
        return $this-&gt;username;
    }

    public function getSalt()
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return null;
    }

    public function getPassword()
    {
        return $this-&gt;password;
    }

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function eraseCredentials()
    {
    }

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize(array(
            $this-&gt;id,
            $this-&gt;username,
            $this-&gt;password,
            // see section on salt below
            // $this-&gt;salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this-&gt;id,
            $this-&gt;username,
            $this-&gt;password,
            // see section on salt below
            // $this-&gt;salt
        ) = unserialize($serialized);
    }
}</pre><p>为了让事情变得精简，一些getter和setter方法就没有显示。但是你可以用下面的命令生成：</p><pre class="crayon-plain-tag">$ php app/console doctrine:generate:entities AppBundle/Entity/User</pre><p>下面，创建数据库表</p><pre class="crayon-plain-tag">$ php app/console doctrine:schema:update --force</pre><p>&nbsp;</p>
<h3>什么是UserInterface？</h3>
<p>到目前为止，他只是一个普通的实体。但是为了他能够在安全系统中使用就必须要实现UserInterface。这迫使我们要有以下五种方法：</p>
<ul class="simple">
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface::getRoles()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html#method_getRoles">getRoles()</a></code></tt></li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface::getPassword()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html#method_getPassword">getPassword()</a></code></tt></li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface::getSalt()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html#method_getSalt">getSalt()</a></code></tt></li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface::getUsername()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html#method_getUsername">getUsername()</a></code></tt></li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface::eraseCredentials()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html#method_eraseCredentials">eraseCredentials()</a></code></tt></li>
</ul>
<p>想了解更多，就要查看 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html">UserInterface</a></code></tt>.</p>
<p>&nbsp;</p>
<h3>什么是序列化和反序列化方法呢？</h3>
<p>在每一个请求结束，用户对象会被序列化到session。在下一个请求，他会反序列化。要帮助 PHP 正确做到这一点，您需要实现Serializable。但你不必序列化所有东西：你只需要几个字段（如果你想在这个基础上加一些额外的东西，你就要实现 <a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/entity_provider.html#security-advanced-user-interface"><em>AdvancedUserInterface</em></a>）。对于每个请求，这个id用来从数据库中查询User对象。</p>
<p>想要去了解更多吗？请查看下面的 《了解序列化和用户是怎样保存在session中的》</p>
<p>&nbsp;</p>
<p>2）配置 security.yml 加载你的实体</p>
<p>现在，你有一个User实体实现了UserInterface，你需要用security.yml文件告诉symfony安全系统。在这个例子中，用户将输入用户名和密码通过HTTP基本验证。symfony会查询和用户名匹配的User实体，然后检查密码（通常检查密码的用时较短）：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User:
            algorithm: bcrypt

    # ...

    providers:
        our_db_provider:
            entity:
                class: AppBundle:User
                property: username
                # if you're using multiple entity managers
                # manager_name: customer

    firewalls:
        default:
            pattern:    ^/
            http_basic: ~
            provider: our_db_provider

    # ...</pre><p>首先，encoders部分告诉symfony数据库中的密码使用bcrypt方式编码。第二，providers部分创建了一个叫our_db_provider的”user provider“，他知道username属性是AppBundle:User实体的。这个our_db_provider名称并不重要：她仅仅是防火墙要匹配<tt class="docutils literal"><code>provider键的一个值。或者，如果您没有在防火墙下设置 <tt class="docutils literal">provider</tt> 键，第一个 “user provider” 会被自动使用。</code></tt></p>
<p>如果你使用的是php5.4或者更低，想要使用<tt class="docutils literal"><code>bcrypt方式编码，</code></tt>你需要通过composer安装ircmaxell/password-compat库：</p><pre class="crayon-plain-tag">{
    "require": {
        ...
        "ircmaxell/password-compat": "~1.0.3"
    }
}</pre><p>&nbsp;</p>
<h3>创建第一个用户</h3>
<p>要添加用户，你需要实现一个<a href="http://symfony.com/doc/current/cookbook/doctrine/registration_form.html">注册表单</a>或者用fixtures添加一些。这是一个正常的实体，所以没有什么猫腻，只是你需要对每个用户的密码进行加密。不过不用担心，symfony会给你一个能做此事的服务（service ），查看 <em><a class="reference internal" href="http://symfony.com/doc/current/book/security.html#security-encoding-password">Dynamically Encoding a Password</a>。</em></p>
<p>下面是从 MySQL 中导出的 app_users 表，包含了用户 admin 和密码 admin （密码是加密过的）。</p><pre class="crayon-plain-tag">$ mysql&gt; SELECT * FROM app_users;
+----+----------+--------------------------------------------------------------+--------------------+-----------+
| id | username | password                                                     | email              | is_active |
+----+----------+--------------------------------------------------------------+--------------------+-----------+
|  1 | admin    | $2a$08$jHZj/wJfcVKlIwr5AvR78euJxYK7Ku5kURNhNx.7.CSIJ3Pq6LEPC | admin@example.com  |         1 |
+----+----------+--------------------------------------------------------------+--------------------+-----------+</pre><p></p>
<blockquote><p> 注释：是不是还要添加一个Salt属性？不需要，因为你使用bcrypt。所有的密码必须用一个 salt 进行哈希处理，但是 bcrypt 内部做了这件事。由于本教程使用 bcrypt ，User 中的 getSalt() 方法只能返回空值（它没有被使用）。如果你使用了一个不同的算法，您需要在User对象中取消对 salt 行的注释，并且添加一个持久的salt 属性。</p></blockquote>
<p>&nbsp;</p>
<h3>禁止不活跃的用户（AdvancedUserInterface）</h3>
<p>如果用户的isActive属性被设置成false（即is_active在数据库中就是0），用户仍然可以正常登陆网站。这很容易解决。</p>
<p>禁止不活跃的用户，需要你的User类修改为实现AdvancedUserInterface。他继承了 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserInterface.html">UserInterface</a>，所以你只需要新的接口方法。</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Entity/User.php

use Symfony\Component\Security\Core\User\AdvancedUserInterface;
// ...

class User implements AdvancedUserInterface, \Serializable
{
    // ...

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this-&gt;isActive;
    }

    // serialize and unserialize must be updated - see below
    public function serialize()
    {
        return serialize(array(
            // ...
            $this-&gt;isActive
        ));
    }
    public function unserialize($serialized)
    {
        list (
            // ...
            $this-&gt;isActive
        ) = unserialize($serialized);
    }
}</pre><p>这个<tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\AdvancedUserInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/AdvancedUserInterface.html">AdvancedUserInterface</a>接口需要添加四个额外的方法来验证账户状态：</code></tt></p>
<ul class="simple">
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\AdvancedUserInterface::isAccountNonExpired()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/AdvancedUserInterface.html#method_isAccountNonExpired">isAccountNonExpired()</a></code></tt> 检查用户账户是否到期;</li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\AdvancedUserInterface::isAccountNonLocked()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/AdvancedUserInterface.html#method_isAccountNonLocked">isAccountNonLocked()</a></code></tt> 检查用户是否被锁定;</li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\AdvancedUserInterface::isCredentialsNonExpired()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/AdvancedUserInterface.html#method_isCredentialsNonExpired">isCredentialsNonExpired()</a></code></tt> 检查用户凭证（密码）是否已过期;</li>
<li><tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\AdvancedUserInterface::isEnabled()" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/AdvancedUserInterface.html#method_isEnabled">isEnabled()</a></code></tt> 检查用户是否已启用.</li>
</ul>
<p>如果他们都返回false，用户将不能允许登录进来。你可以选择持久化所有的这些属性或者挑选你需要的（在这个例子中，数据库中只有isActive）。<br />
那么，他们方法之间的区别是什么？每个方法都会返回一个不同的错误信息（你在登录模板进一步定制这些信息时，这些信息都可以被翻译）。</p>
<blockquote><p>如果你使用AdvancedUserInterface，你还需要把这些属性（例如isActive）添加到serialize()和unserialize()方法中去。如果你不这样做，您的用户可能无法从每个请求上的session中正确反序列化。</p></blockquote>
<p>恭喜，你已经完成了从数据库中加载数据到安全系统的所有配置！接下来，如果你像添加一个真正的 <a href="http://symfony.com/doc/current/cookbook/security/form_login.html">login form</a> 来代替http basic，就需要阅读别的文章了。</p>
<p>&nbsp;</p>
<h3>使用自定义查询加载用户</h3>
<p>如果一个用户能够使用用户名或者邮箱登录那就太好了，因为二者在数据库中都是唯一的。不幸的是，原生的entity provider仅仅只能通过单个用户属性来处理查询。</p>
<p>想要二者都可以登陆，就需要你的UserRepository去实现一个特殊的 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserProviderInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserProviderInterface.html">UserProviderInterface</a></code></tt>.这个接口需要三个方法：<tt class="docutils literal"><code>loadUserByUsername($username)</code></tt>, <tt class="docutils literal"><code>refreshUser(UserInterface $user)</code></tt>, 和<tt class="docutils literal"><code>supportsClass($class)</code></tt>:</p><pre class="crayon-plain-tag">// src/AppBundle/Entity/UserRepository.php
namespace AppBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $user = $this-&gt;createQueryBuilder('u')
            -&gt;where('u.username = :username OR u.email = :email')
            -&gt;setParameter('username', $username)
            -&gt;setParameter('email', $username)
            -&gt;getQuery()
            -&gt;getOneOrNullResult();

        if (null === $user) {
            $message = sprintf(
                'Unable to find an active admin AppBundle:User object identified by "%s".',
                $username
            );
            throw new UsernameNotFoundException($message);
        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this-&gt;supportsClass($class)) {
            throw new UnsupportedUserException(
                sprintf(
                    'Instances of "%s" are not supported.',
                    $class
                )
            );
        }

        return $this-&gt;find($user-&gt;getId());
    }

    public function supportsClass($class)
    {
        return $this-&gt;getEntityName() === $class
            || is_subclass_of($class, $this-&gt;getEntityName());
    }
}</pre><p>有关这些方法的详细信息，请参阅 <tt class="docutils literal"><code><a class="reference external" title="Symfony\Component\Security\Core\User\UserProviderInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/UserProviderInterface.html">UserProviderInterface</a></code></tt>.</p>
<blockquote><p>别忘了将 repository 类添加到实体并映射 <a class="reference internal" href="http://symfony.com/doc/current/book/doctrine.html#book-doctrine-custom-repository-classes"><em>mapping definition of your entity</em></a>.</p></blockquote>
<p>只需在 security.yml 中移除用户提供者的 property 键值。</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    providers:
        our_db_provider:
            entity:
                class: AppBundle:User</pre><p>告诉symfony不要自动查询User。相反，当有人登录时，<tt class="docutils literal"><code><span class="goog-text-highlight">UserRepository的loadUserByUsername()方法将会被调用。</span></code></tt></p>
<p>&nbsp;</p>
<h3>了解序列化和如何保持用户到session</h3>
<p>如果你关心在User类中 serialize() 方法的重要性和如何将用户对象序列化或反序列化，那么这一节适合于你。如果你不关心，那么你就可以跳过这一章。</p>
<p>一旦用户登录，整个用户对象会序列化到session。在接下来的请求，用户对象反序列化。然后，<strong>id</strong> 属性的值是用来从数据库中查询一个新的用户对象。最后，新的用户对象与反序列化的用户对象进行比较，以确保它们表示相同的用户。例如，如果由于某种原因，两个用户对象上的username不匹配，则出于安全原因，该用户将被注销。</p>
<p>尽管这一切都自动触发，但也有一些副作用。</p>
<p>首先， <tt class="docutils literal"><code><a class="reference external" title="Serializable" href="http://php.net/manual/en/class.serializable.php">Serializable</a>接口和他自己的<tt class="docutils literal">serialize、</tt><tt class="docutils literal"><code>unserialize方法都被添加到允许的User类，并完成序列化到session。这可能是，也可能不是根据您的设置来完成的，但是他是一个好主意。从理论上讲，只有<tt class="docutils literal">id才需要序列化，因为<tt class="docutils literal"><a class="reference external" title="Symfony\Bridge\Doctrine\Security\User\EntityUserProvider::refreshUser()" href="http://api.symfony.com/2.7/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html#method_refreshUser">refreshUser()</a>方法通过id刷新每个请求</tt></tt></code></tt></code><code><tt class="docutils literal"><code><tt class="docutils literal"><tt class="docutils literal">(如上所述)。这给我们一个 "fresh" 用户对象。</tt></tt></code></tt></code></tt></p>
<p>但是在symfony中，他还使用 <tt class="docutils literal"><code>username</code></tt>, <tt class="docutils literal"><code>salt</code></tt>, 和<tt class="docutils literal"><code>password验证用户请求之间没有改变(如果这样实现，它也会调用你的<strong>AdvancedUserInterface</strong> 方法)。未能序列化，这些可能会导致你被注销。如果您的User实现了<tt class="docutils literal"><a class="reference external" title="Symfony\Component\Security\Core\User\EquatableInterface" href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/User/EquatableInterface.html">EquatableInterface</a>，</tt>你使用<tt class="docutils literal">isEqualTo方法很容易的替代之前的属性检查</tt>，并且你能检查所有你想要的属性。你一定要明白这一点，要不你没有必要实现这个接口或者你也不用关心他。</code></tt></p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/702">(Security)安全系统如何从数据库中读取用户-Entity Provider(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/702/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Security)怎样使用API密钥验证用户(2.7)</title>
		<link>http://www.newlifeclan.com/symfony/archives/696</link>
		<comments>http://www.newlifeclan.com/symfony/archives/696#comments</comments>
		<pubDate>Tue, 24 Nov 2015 08:26:25 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cookbook]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=696</guid>
		<description><![CDATA[<p>如今，使用API去验证一个用户的身份是很平常的事情（例如开发一个web服务的时候）。该 API 密钥可以为每个 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/696">(Security)怎样使用API密钥验证用户(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>如今，使用API去验证一个用户的身份是很平常的事情（例如开发一个web服务的时候）。该 API 密钥可以为每个请求提供服务，并且以查询字符串参数的形式或通过 HTTP 头部信息进行传递。<span id="more-696"></span></p>
<h3>API密钥身份验证</h3>
<p>我们应该通过预身份验证机制请求信息来对用户身份进行验证。<a href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/Authentication/SimplePreAuthenticatorInterface.html" rel="nofollow">SimplePreAuthenticatorInterface</a> 接口能让您很容易的达到这个目的。</p>
<p>您的实际情况可能会有所不同，但在此示例中，一个(token)令牌就是apikey的参数，然后我们就可以正确加载用户名，并创建用户对象。</p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyAuthenticator.php
namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
    public function createToken(Request $request, $providerKey)
    {
        // look for an apikey query parameter 获得apikey参数
        $apiKey = $request-&gt;query-&gt;get('apikey');

        // or if you want to use an "apikey" header, then do something like this:
        // $apiKey = $request-&gt;headers-&gt;get('apikey');

        if (!$apiKey) {
            throw new BadCredentialsException('No API key found');

            // or to just skip api key authentication
            // return null;
        }

        return new PreAuthenticatedToken(
            'anon.',
            $apiKey,
            $providerKey
        );
    }

    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        if (!$userProvider instanceof ApiKeyUserProvider) {
            throw new \InvalidArgumentException(
                sprintf(
                    'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
                    get_class($userProvider)
                )
            );
        }

        $apiKey = $token-&gt;getCredentials();
        $username = $userProvider-&gt;getUsernameForApiKey($apiKey);

        if (!$username) {
            throw new AuthenticationException(
                sprintf('API Key "%s" does not exist.', $apiKey)
            );
        }

        $user = $userProvider-&gt;loadUserByUsername($username);

        return new PreAuthenticatedToken(
            $user,
            $apiKey,
            $providerKey,
            $user-&gt;getRoles()
        );
    }

    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof PreAuthenticatedToken &amp;&amp; $token-&gt;getProviderKey() === $providerKey;
    }
}</pre><p>一旦你已经配置好了一切，你就可以给apikey添加字符串参数来进行验证了，就像：</p>
<p><tt class="docutils literal"><code>http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2</code></tt></p>
<p>验证需要几个步骤，根据你的情况可能略有不同：</p>
<p><strong>1. createToken方法</strong></p>
<p>在请求周期的早期，symfony会调用createToken().在这里您需要做的就是去创建一个包含所有消息的令牌对象，这些消息是来自于您的某个请求，用户进行身份验证需要您这个请求 (例如 apikey 查询参数) 。如果缺少这些信息，则会抛出 <a href="http://api.symfony.com/2.7/Symfony/Component/Security/Core/Exception/BadCredentialsException.html" rel="nofollow">BadCredentialsException</a> 异常从而导致身份验证失败。相反，您可能想要跳过身份验证并且不返回信息，所以 Symfony 可以回退到另一种身份验证方法，如果存在这种方法。</p>
<p><strong>2.supportsToken方法</strong></p>
<p>当 Symfony 调用 <strong>createToken()</strong> 之后，它将调用您的类中的 <strong>supportsToken()</strong> 方法（和任何其它的身份验证监听器）来弄清到底应该谁来处理令牌。这只是一种方式,允许多个身份验证机制用于相同的防火墙（用这种方式，你可以先尝试使用证书或者API密钥来进行身份验证，或者滚回到表单登陆）。</p>
<p>大多数情况下，你只需要去让这个方法返回true，也就是令牌是由createToken方法创建的。你的逻辑应该跟这个例子的差不多。</p>
<p><strong>3. authenticateToken</strong></p>
<p>如果supportsToken()返回true，symfony就会立刻调用authenticateToken()。$userProvider是其中的一个关键部分，它来自外部的类，他可以帮助我们加载关于用户的信息。下面你将会了解更多。</p>
<p>在特定的例子中，下面的事情可能会在authenticateToken()中发生：</p>
<p>1.首先，您可以使用<strong>$userProvider</strong>以某种方式查找<strong>$apiKey</strong>对应的<strong>$username。</strong></p>
<p>2.其次，你使用 <tt class="docutils literal"><code>$userProvider根据</code></tt>$username加载和创建一个<tt class="docutils literal"><code>User</code></tt> 对象。</p>
<p>3.最后，你要创建一个<em>authenticated token</em>（例如，一个token至少有一个角色），你要把适当的角色和用户对象附加给他。</p>
<p>最终就是使用$apiKey去查找并创建用户对象。你如何去做（例如，查询数据库）要看你的用户对象类都有哪些不同。这些差异在user provider最为明显。</p>
<p>&nbsp;</p>
<h3>User Provider</h3>
<p>这个$userProvider能够是任何的用户提供器（请参阅 How to Create a custom User Provider ）。在此例子中，以某种方式用<strong>$apiKey</strong> 为用户查找用户名。这项工作是在 <strong>getUsernameForApiKey()</strong> 方法中完成的，创建完全自定义的样例(他不是一个symfony核心用户代理系统中的方法)。</p>
<p><tt class="docutils literal"><code>$userProvider是下面这个样子：</code></tt></p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyUserProvider.php
namespace AppBundle\Security;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class ApiKeyUserProvider implements UserProviderInterface
{
    public function getUsernameForApiKey($apiKey)
    {
        // Look up the username based on the token in the database, via
        // an API call, or do something entirely different
        $username = ...;

        return $username;
    }

    public function loadUserByUsername($username)
    {
        return new User(
            $username,
            null,
            // the roles for the user - you may choose to determine
            // these dynamically somehow based on the user
            array('ROLE_USER')
        );
    }

    public function refreshUser(UserInterface $user)
    {
        // this is used for storing authentication in the session
        // but in this example, the token is sent in each request,
        // so authentication can be stateless. Throwing this exception
        // is proper to make things stateless
        throw new UnsupportedUserException();
    }

    public function supportsClass($class)
    {
        return 'Symfony\Component\Security\Core\User\User' === $class;
    }
}</pre><p>这时，把您的用户提供程序注册成为一种服务：</p><pre class="crayon-plain-tag"># app/config/services.yml
services:
    api_key_user_provider:
        class: AppBundle\Security\ApiKeyUserProvider</pre><p></p>
<blockquote><p> 请阅读特定的文章来学习 <a class="reference internal" href="http://symfony.com/doc/current/cookbook/security/custom_provider.html"><em>how to create a custom user provider</em></a>.</p></blockquote>
<p><strong>getUsernameForApiKey()</strong> 方法中的代码逻辑是由你来决定的。你可能使用API KEY（如37b51d）通过某种方式从一个存有token的数据表中获取一些信息包括用户名（如jondoe）。</p>
<p>上面的过程同样适用于 <tt class="docutils literal"><code><strong>loadUserByUsername()</strong>。在这个例子中，symfony核心User类创建很简单。如果你不需要任何额外的信息存储在用户对象中，他就很容易。不过如果您需要去存储更多的信息，那么您可以创建一个您自己的用户类并且通过查询数据库来填充它，这样将允许您在<strong>用户（User）对象</strong>中添加自定义数据。</code></tt></p>
<p>最后，就像任何通过<strong>loadUserByUsername()</strong> 方法返回的用户类一样，我们只需要确保 <strong>supportsClass()</strong> 方法为用户对象并返回True。如果你的验证是没有状态的就像这个例子（你希望每次请求都发送密钥，你就不需要保存session了），那么你只用在refreshUser()中简单的抛出异常<tt class="docutils literal"><code>UnsupportedUserException。</code></tt></p>
<blockquote><p> 如果你想要在 session 中存储身份验证数据，那么并不需要在每个请求中发送秘钥，请参阅下面（在session中存储验证）。</p></blockquote>
<h3></h3>
<h3>处理失败的验证</h3>
<p>当凭据验证失败或者身份验证失败时，为了能让您的 <strong>ApiKeyAuthenticator</strong> 正确的显示 403 http 状态，您应该在您的身份验证器中实现 <a href="http://api.symfony.com/2.7/Symfony/Component/Security/Http/Authentication/AuthenticationFailureHandlerInterface.html" rel="nofollow">AuthenticationFailureHandlerInterface</a> 接口。您可以使用该接口中的<strong>onAuthenticationFailure</strong> 方法去创建一个错误Response。</p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyAuthenticator.php
namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    // ...

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new Response("Authentication Failed.", 403);
    }
}</pre><p><strong> 配置</strong></p>
<p>当你配置完<tt class="docutils literal"><code>ApiKeyAuthenticator所有配置后，你需要注册他为一个服务并且使用它在你的安全配置中（security.yml），首先，注册为一个服务。</code></tt></p><pre class="crayon-plain-tag"># app/config/config.yml
services:
    # ...

    apikey_authenticator:
        class:  AppBundle\Security\ApiKeyAuthenticator
        public: false</pre><p>现在，你在安全配置中（security.yml）的防火墙中分别使用<tt class="docutils literal"><code>simple_preauth和<tt class="docutils literal">provider键，来激活上面的服务和自定义的用户提供者（user provider）：</tt></code></tt></p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    firewalls:
        secured_area:
            pattern: ^/admin
            stateless: true
            simple_preauth:
                authenticator: apikey_authenticator
            provider: api_key_user_provider

    providers:
        api_key_user_provider:
            id: api_key_user_provider</pre><p>完成了上述步骤!现在，在每个请求开始的时候，您的 <strong>ApiKeyAuthenticator</strong> 方法都会被调用，然后将进行身份验证过程。</p>
<p>这个<tt class="docutils literal"><code>stateless参数配置防止symfony将用户验证信息存储在session中，因为每次请求都发送api给你，你就没有存储的必要。如果你需要存储验证到session的话，就继续阅读吧！</code></tt></p>
<h3>在Session中存储验证</h3>
<p>到目前为止，每个请求都会传送验证令牌。但某些情况下（例如OAuth流程）这个token请求可能只发送一遍。在这种情况下，你一定会想到对用户进行验证并将验证信息存储在session中。以便用户在后续请求中自动登陆。</p>
<p>要想实现这些功能，首先你的防火墙的<tt class="docutils literal"><code>stateless键就要删除或者设置为false：</code></tt></p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    # ...

    firewalls:
        secured_area:
            pattern: ^/admin
            stateless: false
            simple_preauth:
                authenticator: apikey_authenticator
            provider: api_key_user_provider

    providers:
        api_key_user_provider:
            id: api_key_user_provider</pre><p>即使令牌被存储在 session 中，在这种情况下由于某些安全性原因，凭据和 API 密钥 (即 <strong>$token-&gt;getCredentials()</strong>) 不会被存储在 session 中。如果想要利用 session，请更新 <strong>ApiKeyAuthenticator</strong> 来查看被存储的令牌是否有一个可以使用的有效用户对象：</p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyAuthenticator.php
// ...

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
    // ...
    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        if (!$userProvider instanceof ApiKeyUserProvider) {
            throw new \InvalidArgumentException(
                sprintf(
                    'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
                    get_class($userProvider)
                )
            );
        }

        $apiKey = $token-&gt;getCredentials();
        $username = $userProvider-&gt;getUsernameForApiKey($apiKey);

        // User is the Entity which represents your user
        $user = $token-&gt;getUser();
        if ($user instanceof User) {
            return new PreAuthenticatedToken(
                $user,
                $apiKey,
                $providerKey,
                $user-&gt;getRoles()
            );
        }

        if (!$username) {
            throw new AuthenticationException(
                sprintf('API Key "%s" does not exist.', $apiKey)
            );
        }

        $user = $userProvider-&gt;loadUserByUsername($username);

        return new PreAuthenticatedToken(
            $user,
            $apiKey,
            $providerKey,
            $user-&gt;getRoles()
        );
    }
    // ...
}</pre><p>在 session 中存储身份验证信息工作原理是这样的:</p>
<p>1. 在每一个请求结束后，symfony会序列化令牌（token ）对象（从authenticateToken()返回），他也序列化User对象（在token中设置了他的属性）；</p>
<p>2.在下一个请求中令牌将被反序列化并且被反序列化的用户对象将被传送给用户提供程序中的 <strong>refreshUser()</strong> 函数。第二步是最重要的: Symfony 将会调用 <strong>refreshUser()</strong> 方法并把在 session 周期中序列化的用户对象传递给您。如果您的用户信息存储在数据库中，然后你可能想要重新查询一个新版本的用户信息来确保还没过期。但是，如果不理会您的要求，<strong>refreshUser()</strong> 现在应该返回用户对象:</p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyUserProvider.php

// ...
class ApiKeyUserProvider implements UserProviderInterface
{
    // ...

    public function refreshUser(UserInterface $user)
    {
        // $user is the User that you set in the token inside authenticateToken()
        // after it has been deserialized from the session

        // you might use $user to query the database for a fresh user
        // $id = $user-&gt;getId();
        // use $id to make a query

        // if you are *not* reading from a database and are just creating
        // a User object (like in this example), you can just return it
        return $user;
    }
}</pre><p>你还要确保你的User对象被正确序列化。如果你的User对象是私有属性，php则不能序列化。在这种情况下，你可能会得到一个空的对象，他的每个属性都是null.有关示例，请阅读 <a href="http://symfony.com/doc/current/cookbook/security/entity_provider.html">How to Load Security Users from the Database (the Entity Provider)</a>。</p>
<p>&nbsp;</p>
<h3>只验证某些URL</h3>
<p>该项目假设你想在每一个请求中查找apikey验证。但在某些情况下（如一个OAuth流程）一旦用户已经到达了一个特定的url，他只需要寻找用户的验证信息。（例如 OAuth的重定向）。</p>
<p>幸运的是，处理这种情况很容易: 只要检查一下使用 <strong>createToken()</strong> 方法创建令牌之前的 URL 是什么即可:</p><pre class="crayon-plain-tag">// src/AppBundle/Security/ApiKeyAuthenticator.php

// ...
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\Request;

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
    protected $httpUtils;

    public function __construct(HttpUtils $httpUtils)
    {
        $this-&gt;httpUtils = $httpUtils;
    }

    public function createToken(Request $request, $providerKey)
    {
        // set the only URL where we should look for auth information
        // and only return the token if we're at that URL
        $targetUrl = '/login/check';
        if (!$this-&gt;httpUtils-&gt;checkRequestPath($request, $targetUrl)) {
            return;
        }

        // ...
    }
}</pre><p>在这里使用较为便利的 <a href="http://api.symfony.com/2.7/Symfony/Component/Security/Http/HttpUtils.html" rel="nofollow">HttpUtils</a> 类来检查当前的 URL 是否与您想要获取的 URL 相匹配。在这种情况下，URL (<span style="color: #339966">/login/check</span>) 已经在类中被硬编码，但是您仍然可以把它作为构造函数的第二个参数。</p>
<p>接下来，只用更新您的服务配置来注入<span style="color: #008000"> security.http_utils </span>服务:</p><pre class="crayon-plain-tag"># app/config/config.yml
services:
    # ...

    apikey_authenticator:
        class:     AppBundle\Security\ApiKeyAuthenticator
        arguments: ["@security.http_utils"]
        public:    false</pre><p>完了！玩的开心！</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/696">(Security)怎样使用API密钥验证用户(2.7)</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/696/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
