Skip to content

第五章第四节:处理 CSRF 令牌

学习时间: 30 分钟


1. 什么是 CSRF 攻击?“劫持”您的飞船

想象一下,您已登录到您的太空舰队控制面板 (space-api.test)。在相邻的标签页中,您打开了一个无害的猫咪网站 (evil-cats.com)。该网站有一个隐藏的表单,会自动向您的网站 POST /api/planets/1/delete 地址发送请求。

由于您已在 space-api.test 上授权,您的浏览器会友好地将您的所有 Cookie 附加到此请求中。Laravel 服务器将看到有效的会话,并认为这是您自己决定报废行星。行星将在您不知情的情况下被删除。

这就是 CSRF (Cross-Site Request Forgery) —— 一种攻击,攻击者诱骗经过身份验证的用户浏览器在受信任的网站上执行不希望的操作。

💡 宇宙类比:

您是飞船的船长,拥有一张钥匙卡(会话/Cookie)。攻击者无法窃取您的卡。但他们可以在您分心时,欺骗您将卡刷向资源报废终端。CSRF 令牌就像一个 PIN 码,需要与卡一起输入。攻击者不知道 PIN 码,他们的攻击就会失败。


2. Laravel 如何防范 CSRF?

Laravel 默认使用 CSRF 令牌保护所有“不安全”的 Web 请求 (POST, PUT, PATCH, DELETE)。

  1. 生成页面时,Laravel 会为用户会话创建一个独特的随机令牌。
  2. 此令牌被嵌入到 HTML 表单中。
  3. 提交表单时,令牌随请求一起发送。
  4. 在服务器端,VerifyCsrfToken 中间件会将请求中的令牌与会话中存储的令牌进行比较。
  5. 如果令牌不匹配,Laravel 会以 419 错误(会话过期/页面过期)中断请求。

重要提示: routes/api.php 中的 API 路由受 CSRF 保护,因为它们假定使用不同的身份验证机制(例如 Sanctum 令牌),而不是基于 Cookie 的会话。我们当前的问题专门涉及我们在 routes/web.php 中创建的 Web 路由和页面。


3. 在 HTML 表单中使用 CSRF 令牌

这是最简单的场景。Laravel 为此提供了一个特殊的 Blade 指令。

示例:创建星球的表单 我们将在 resources/views/planets/create.blade.php 文件中创建一个简单表单:

<h2>启动新星球表单</h2>
<form action="/planets" method="POST">
    @csrf {{-- 这就是魔法! --}}

    <label for="name">名称:</label>
    <input type="text" id="name" name="name" required>

    <label for="solar_system">太阳系:</label>
    <input type="text" id="solar_system" name="solar_system" required>

    {{-- ... 其他字段 ... --}}

    <button type="submit">启动</button>
</form>

@csrf 指令将自动在表单中生成一个隐藏字段:

<input type="hidden" name="_token" value="j2aK3dLf4gH5...唯一令牌...">

这足以保护标准 HTML 表单。


4. 在 AJAX/Fetch 请求中使用 CSRF 令牌

在上一章中,我们使用 JavaScript 发送了 DELETE 请求。现在 Laravel 将以 419 错误阻止它。我们需要将 CSRF 令牌添加到我们的 Fetch 请求头中。

步骤 1:使令牌可供 JavaScript 访问

将带有令牌的 meta 标签添加到您的主布局 resources/views/app.blade.php<head> 中。这是 Laravel 中的标准做法。

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    {{-- 将 CSRF 令牌添加到 meta 标签中 --}}
    <meta name="csrf-token" content="{{ csrf_token() }}">

    {{-- ... --}}
</head>

csrf_token() 函数返回当前的令牌。

步骤 2:修改 JavaScript 以发送令牌

现在在我们的 public/js/planets.js 中,我们可以读取此令牌并将其添加到所有“不安全”请求的请求头中。

// ... 在 public/js/planets.js 文件中 ...

document.addEventListener('DOMContentLoaded', () => {
    // 从 meta 标签中获取令牌
    const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

    const deleteButtons = document.querySelectorAll('.delete-btn');

    deleteButtons.forEach(button => {
        button.addEventListener('click', async (event) => {
            // ... 确认逻辑 ...

            try {
                const response = await fetch(apiUrl, {
                    method: 'DELETE',
                    headers: {
                        'Accept': 'application/json',
                        'X-CSRF-TOKEN': csrfToken // <-- 将令牌添加到请求头中!
                    }
                });

                // ... 剩余的响应处理逻辑 ...
            } catch (error) {
                // ...
            }
        });
    });
});
  • 请求头名称 X-CSRF-TOKEN 是 Laravel 默认检查的标准。

现在我们的 AJAX 请求也受到保护了。尝试再次删除星球——这次请求将成功通过。


巩固练习小测

1. CSRF 令牌可以防止哪种攻击?

2. 哪个 Blade 指令会向表单中添加一个带有 CSRF 令牌的隐藏字段?

3. 如果向 Web 路由发送不带 CSRF 令牌的 POST 请求会发生什么?

4. 在 AJAX 请求中,哪个标准的 HTTP 头用于发送 CSRF 令牌?

5. 为什么 API 路由 (`routes/api.php`) 默认不使用 CSRF 保护?


🚀 本章总结:

您已在您的宇宙飞船上安装了“敌我识别系统”,保护它免受 CSRF 攻击。您学会了:

  • 理解 CSRF 攻击的本质和危险性。
  • 使用 @csrf 指令保护标准 HTML 表单。
  • 通过 meta 标签将 CSRF 令牌传递给 JavaScript。
  • 将令牌包含在 AJAX/Fetch 请求头中以确保其成功执行。

您的 Web 界面现在不仅具有交互性,而且是安全的。 在下一章中,我们将通过探讨如何正确组织 Web 页面的路由来完成我们的 Web 界面创建。