在测试中获取CSRF令牌,“CSRF令牌无效”-功能性ajax测试。

8

我正在尝试在Symfony2中测试一个ajax请求。 我编写了一个单元测试,在我的app/logs/test.log中抛出以下错误:

request.CRITICAL: Uncaught PHP Exception Twig_Error_Runtime: 
"Impossible to access an attribute ("0") on a string variable 
("The CSRF token is invalid. Please try to resubmit the form.")
in .../vendor/twig/twig/lib/Twig/Template.php:388

我的代码非常直接明了。
public function testAjaxJsonResponse()
{
    $form['post']['title'] = 'test title';
    $form['post']['content'] = 'test content';
    $form['post']['_token'] = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken();

    $client->request('POST', '/path/to/ajax/', $form, array(), array(
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    ));

    $response = $client->getResponse();
    $this->assertSame(200, $client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $response->headers->get('Content-Type'));
}

问题似乎是 CSRF 令牌,我可以在测试中禁用它,但我并不想这样做。我通过进行两个请求来解决问题(第一个请求加载带有表单的页面,我们获取 _token 并使用 XMLHttpRequest 进行第二个请求)。这显然看起来相当愚蠢和低效!
2个回答

9

解决方案

我们可以使用以下方法为我们的ajax请求生成自己的CSRF令牌:

$client->getContainer()->get('form.csrf_provider')->generateCsrfToken($intention);

这里的变量$intention是指在你的表单类型选项中设置的数组键。

添加intention

在你的表单类型中,你需要添加intention键。例如:

# AcmeBundle\Form\Type\PostType.php

/**
 *  Additional fields (if you want to edit them), the values shown are the default
 * 
 * 'csrf_protection' => true,
 * 'csrf_field_name' => '_token', // This must match in your test
 *
 * @param OptionsResolverInterface $resolver
 */
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Acme\AcmeBundle\Entity\Post',
        // a unique key to help generate the secret token
        'intention' => 'post_type',
    ));
}

阅读文档

在功能测试中生成CSRF Token

现在我们有了这个“意图”,我们可以在我们的单元测试中使用它来生成有效的CSRF令牌。

/**
 * Test Ajax JSON Response with CSRF Token
 * Example uses a `post` entity
 *
 * The PHP code returns `return new JsonResponse(true, 200);`
 */
public function testAjaxJsonResponse()
{
    // Form fields (make sure they pass validation!)
    $form['post']['title'] = 'test title';
    $form['post']['content'] = 'test content';

    // Create our CSRF token - with $intention = `post_type`
    $csrfToken = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken('post_type');
    $form['post']['_token'] = $csrfToken; // Add it to your `csrf_field_name`

    // Simulate the ajax request
    $client->request('POST', '/path/to/ajax/', $form, array(), array(
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    ));

    // Test we get a valid JSON response
    $response = $client->getResponse();
    $this->assertSame(200, $client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $response->headers->get('Content-Type'));

    // Assert the content
    $this->assertEquals('true', $response->getContent());
    $this->assertNotEmpty($client->getResponse()->getContent());
}

2
请注意,从Symfony 2.3开始,服务名称为security.csrf.token_manager而不是form.csrf_provider - COil

2

虽然这个问题非常古老,但它仍然是谷歌搜索的第一个结果,所以我想用Symfony 5.4 / 6.x更新一下我的发现。


简短回答:使用你的表单类型的getBlockPrefix()方法的结果作为tokenId:

$csrfToken = self::getContainer()->get('security.csrf.token_manager')->getToken('your_blockprefix');

长的回答:

这是 Symfony 在其表单系统中创建 CSRF Token 的位置: https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php#L81

tokenId 将按以下顺序确定:

  • 如果存在,使用表单类型选项 csrf_token_id
  • 使用表单类型的块前缀,由 getBlockPrefix() 方法返回(参见 文档
  • 使用实体的类名,转换为小写并带下划线(参见 源代码

由于我不想将 csrf_token_id 选项添加到每个单独的表单类型中,因此我编写了以下方法根据表单类型的完全限定名称获取 CSRF Token:

protected function generateCSRFToken(string $formTypeFQN): string
{
    $reflectionClass = new \ReflectionClass($formTypeFQN);
    $constructor = $reflectionClass->getConstructor();
    $args = [];
    foreach ($constructor->getParameters() as $parameter) {
        $args[] = $this->createMock($parameter->getType()->getName());
    }
    /** @var FormTypeInterface $instance */
    $instance = $reflectionClass->newInstance(... $args);
    return self::getContainer()->get('security.csrf.token_manager')->getToken($instance->getBlockPrefix());
}

它会实例化一个表单类型的对象,模拟每个必需的构造函数参数,然后根据实例的块前缀创建CSRF令牌。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接