You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony中AJAX自定义DELETE请求的CSRF令牌验证失败问题

Symfony AJAX批量删除时CSRF令牌验证失败问题

在Symfony应用中,使用AJAX删除选中数据时遇到CSRF令牌验证无效的问题,已确认客户端与服务器的令牌匹配,但仍收到错误提示。

代码详情

HTML 代码

<tbody>
    {% for annonce in allAnnonces %}
    
        <tr class="text-dark" id="annonce_ids{{ annonce.id }}">
            <td>
                <input type="checkbox" name="ids" class="checkbox_ids" id="" value="{{ annonce.id }}">
                <input type="hidden" name="_method" value="DELETE">
                <input type="hidden" name="_tokenannonce" value="{{ csrf_token('annonce' ~ annonce.id) }}">
            </td>
            <td>
                {{ annonce.marques }} {{ annonce.modeles }} {{ annonce.year }}
            </td>
            <td>
                {{ annonce.price | number_format(0, ',', ' ') }} {{ annonce.device }}
            </td>
            ........
            <td>
                <a title="Éditer l'annonce" class="jsvm_cm-actn-btn p-1" href="{{ path('app_compte_annonceur_edit',{'id': annonce.id })}}">
                    <img src="{{ asset('/images/annonces/edit-icon.png')}}" alt="Modifier l'annonce" title="Éditer l'annonce">
                </a>
                <form method="POST" action="{{ path('app_compte_annonceur_delete', {id: annonce.id}) }}" style="display: inline-block" onsubmit="return confirm('Êtes vous vraiment sûr ?')">
                    <input type="hidden" name="_method" value="DELETE">
                    <input type="hidden" name="_token" value="{{ csrf_token('delete' ~ annonce.id) }}">
                    <button class="btn" title="supprimer" type="submit">
                        <img src="{{ asset('/images/annonces/delete.png')}}" alt="Supprimer l'annonce" title="Supprimer l'annonce">
                    </button>
                </form>
            </td>
        </tr>
    {% endfor %}
</tbody>
<button class="jsvm_cm-pro-dashb-myveh-btn jsvm_cm-dlt-btn jsvm_multioperation-frontend border btn btn-danger p-2 button red"
        data-for="removemulti"
        data-formid="jsvehiclemanager-list-form"
        id="delete-selected-btn"
        type="submit"
        data-delete-url="{{ path('app_delete_selected') }}">
    <img src="{{ asset('/images/annonces/delete.png')}}"
    <font style="vertical-align: inherit;">
        <font style="vertical-align: inherit;">
            delete selected row</font>
    </font>
    </span>
</button>

Symfony 控制器代码

#[Route('/dashbord/Annonces/deleteselected', name: 'app_delete_selected', methods: 'DELETE')]
public function deleteSelected(Request $request): JsonResponse
{
    $message = [];
    
    $token = $request->get('_tokenannonce');
    $ids= $request->get('ids');
    // dump($data);
    dump($ids);
    dump($token);
    if (!empty($ids)) {
        
        $annonces = $this->annonceRepository->findBy(['id' => $ids]);
        dump($annonces);
        foreach ($annonces as $annonce) {
            
            dump($annonce->getId());
            // dump($this->->getToken('csrf_token')->getValue());
            dump($this->isCsrfTokenValid('annonce'.$annonce->getId(), $token));
            if ($this->isCsrfTokenValid('annonce'.$annonce->getId(), $token)) {
                $this->em->remove($annonce);
                $this->em->flush();
                $this->addFlash('success', 'success');
                $message = ['success' => true, 'message' => 'Success'];
            } else {
                $this->addFlash('error', 'Invalid token');
                $message = ['success' =>false, 'message' => 'Invalid token'];
            }
        }
    dump($message);
    return new JsonResponse(['message' => $message]);
}

JavaScript AJAX 代码

// Handle multiple selection and row removal
$('#delete-selected-btn').click(function () {
    //var selectedRows = oTable.rows('.selected').data();
    var selectedRows = oTable.rows(".selected").nodes();
    var csrfToken = $('input[name="_tokenannonce"]').val();
    console.log(csrfToken);
    if (selectedRows.length > 0) {
        // Extract IDs from selected rows
       // var selectedIds = selectedRows.toArray().map(row => row[0]); // Assuming the ID is in the first column
        var selectedIds = []; 
         $(selectedRows).each(function () {
           var id = $(this).find("td:first-child input").val(); 
           selectedIds.push(id);
         });
        console.log(selectedIds);
        
        // Send AJAX request to delete selected rows
        console.log($(this).data('delete-url'));
        $.ajax({
          url: $(this).data("delete-url"),
          type: "DELETE",
          dataType: "json",
          processData: true,
          data: {
            ids: selectedIds,
           _tokenannonce: csrfToken
          },
          headers: {
            "_tokenannonce": csrfToken, // Include the CSRF token in the request headers
          },
          traditional: true,
          success: function (response) {
            console.log(response);
            if (response.success) {
              alert("success");
              oTable.rows(".selected").fadeOut().remove().draw(false);
            } else {
              alert("Erreur");
            }
          },
          error: function (xhr, status, error) {
            console.log(error);
            console.log(status);
            console.log(xhr);
            // alert('Erreur lors de la requête Ajax');
          },
          beforeSend: function (data) {
            $(selectedRows).css({
              "background-color": "#ccc",
              color: "#fff", // Change the text color for better visibility
            });
            console.log("Data sent:", data); 
          },
        });
    } else {
        alert('Select a row!');
    }
});

已尝试操作

  • 多次确认表单中的CSRF字段名与AJAX请求中使用的字段名一致;
  • 使用Symfony的dump()函数验证控制器已正确获取CSRF令牌。

问题分析与解决方案

核心错误原因

当前代码中,每一行的_tokenannonce是针对单个annonce生成的(令牌ID为annonce{id}),但AJAX请求只取了页面中第一个_tokenannonce的值,然后在控制器里用这个令牌去验证所有选中的annonce。由于每个annonce的令牌都是唯一的,除了第一个匹配的annonce外,其他所有annonce的令牌验证都会失败,最终返回"Invalid token"错误。

修复步骤

  1. 添加全局批量删除CSRF令牌
    在页面的表格上方或表单外层添加一个针对批量删除操作的全局令牌:

    <input type="hidden" id="bulk_delete_token" name="_token" value="{{ csrf_token('bulk_delete_annonces') }}">
    
  2. 修改AJAX请求获取全局令牌
    将JS中获取令牌的代码改为读取全局令牌:

    var csrfToken = $('#bulk_delete_token').val();
    

    同时修改AJAX的data参数,将_tokenannonce改为_token(与控制器获取的字段名保持一致):

    data: {
      ids: selectedIds,
      _token: csrfToken
    },
    // 可以移除headers里的令牌,因为已经在data中传递
    
  3. 修改控制器验证逻辑
    控制器中不再逐个验证单个annonce的令牌,而是先验证批量操作的全局令牌,通过后再执行批量删除:

    #[Route('/dashbord/Annonces/deleteselected', name: 'app_delete_selected', methods: 'DELETE')]
    public function deleteSelected(Request $request): JsonResponse
    {
        $message = ['success' => false, 'message' => 'Invalid token'];
        
        $token = $request->get('_token');
        $ids = $request->get('ids');
        
        // 先验证批量操作的CSRF令牌
        if ($this->isCsrfTokenValid('bulk_delete_annonces', $token) && !empty($ids)) {
            $annonces = $this->annonceRepository->findBy(['id' => $ids]);
            foreach ($annonces as $annonce) {
                $this->em->remove($annonce);
            }
            $this->em->flush();
            $this->addFlash('success', '选中的广告已成功删除');
            $message = ['success' => true, 'message' => '操作成功'];
        } else {
            $this->addFlash('error', '无效的CSRF令牌');
        }
        
        return new JsonResponse($message);
    }
    

额外排查点

  • 请求方法兼容性:部分服务器可能不支持DELETE方法,可以将AJAX的type改为POST,并在data中添加_method: 'DELETE',符合Symfony的HTTP方法伪装规则;
  • Session Cookie携带:如果是同域请求,确保AJAX请求默认携带Session Cookie;如果是跨域,需要在AJAX中添加xhrFields: { withCredentials: true };
  • 令牌ID一致性:确保令牌生成时的ID(如bulk_delete_annonces)和验证时的ID完全一致,包括大小写、拼接字符串;
  • 重复字段问题:避免页面中出现多个同名的CSRF令牌字段,确保全局令牌唯一。

内容的提问来源于stack exchange,提问作者kasali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 21:39:52