
# CSRF 保护（CSRF Protection）

-   [简介](#csrf-introduction)
-   [防止 CSRF 请求](#preventing-csrf-requests)
    -   [来源验证（Origin Verification）](#origin-verification)
    -   [排除 URI](#csrf-excluding-uris)
-   [X-CSRF-Token](#csrf-x-csrf-token)
-   [X-XSRF-Token](#csrf-x-xsrf-token)


## 简介

跨站请求伪造（Cross-Site Request Forgery，CSRF）是一种恶意攻击方式，攻击者会在用户已认证（已登录）的情况下，冒充用户执行未授权的操作。

幸运的是，Laravel 可以非常轻松地保护你的应用免受 Cross-Site Request Forgery （CSRF）攻击。

#### 漏洞原理说明

如果你对跨站请求伪造（CSRF）还不熟悉，我们来看一个漏洞是如何被利用的示例。

假设你的应用程序有一个 `/user/email` 路由，它接收 `POST` 请求，用于修改当前登录用户的邮箱地址。

这个路由大概率会接收一个 `email` 输入字段，用来保存用户希望更改的新邮箱地址。

如果**没有 CSRF 保护**，一个恶意网站就可以创建一个 HTML 表单，偷偷请求你应用中的 `/user/email` 路由，并把邮箱改成攻击者自己的邮箱：

```blade
<form action="https://your-application.com/user/email" method="POST">
    <input type="email" value="malicious-email@example.com">
</form>

<script>
    document.forms[0].submit();
</script>
```

如果恶意网站在页面加载时自动提交该表单，那么攻击者只需要诱导你应用中的一个毫无防备的用户访问该网站，该用户在你应用中的邮箱地址就会被修改。

为了防止这种漏洞，我们需要检查每一个传入的 `POST`、`PUT`、`PATCH` 或 `DELETE` 请求，以验证其中是否包含恶意应用程序无法访问的秘密 Session 值。



## 防止 CSRF 请求

默认包含在 `web` 中间件组中的 `Illuminate\Foundation\Http\Middleware\PreventRequestForgery` [中间件](/docs/laravel/12.x/middleware)，通过双层方式保护你的应用程序免受跨站请求伪造（CSRF）攻击。

首先，该中间件会检查浏览器的 `Sec-Fetch-Site` 请求头。现代浏览器会在每个请求中自动设置此请求头，用于指示请求是否来自相同源（same origin）、相同站点（same site）或跨站来源（cross-site source）。如果请求头表明请求来自相同源，则会立即允许该请求，而无需进行任何 token 验证。

如果来源验证未通过 —— 例如，请求来自不发送 `Sec-Fetch-Site` 请求头的旧版浏览器，或者连接不安全 —— 中间件将回退到传统的 CSRF token 验证方式。

Laravel 会为应用程序管理的每个活动 [用户会话](/docs/laravel/12.x/session) 自动生成一个 CSRF “token”。此 token 用于验证已认证用户是否确实是向应用程序发起请求的人。由于该 token 存储在用户 session 中，并且每次 session 重新生成时都会发生变化，因此恶意应用程序无法访问它。

当前 session 的 CSRF token 可以通过请求的 session 或 `csrf_token` 辅助函数获取：

```php
use Illuminate\Http\Request;

Route::get('/token', function (Request $request) {
    $token = $request->session()->token();

    $token = csrf_token();

    // ...
});
```

每当你在应用程序中定义一个 `"POST"`、`"PUT"`、`"PATCH"` 或 `"DELETE"` HTML 表单时，你都应该在表单中包含一个隐藏的 CSRF `_token` 字段，以便 CSRF 防护中间件能够验证该请求。为了方便，你可以使用 `@csrf` Blade 指令来生成隐藏的 token 输入字段：

```blade
<form method="POST" action="/profile">
    @csrf

    <!-- 等价于... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>
```



#### CSRF Token 与 SPA

如果你正在构建一个使用 Laravel 作为 API 后端的 SPA（单页应用程序），你应该查阅 [Laravel Sanctum 文档](/docs/laravel/12.x/sanctum)，以获取有关 API 身份验证以及防止 CSRF 漏洞的信息。

### 来源验证（Origin Verification）

如上所述，Laravel 的请求伪造中间件首先会检查 `Sec-Fetch-Site` 请求头，以确定请求是否来自相同源。

默认情况下，如果该检查未通过，中间件将回退到 CSRF token 验证。

不过，如果你希望完全依赖来源验证，并完全禁用 CSRF token 的回退验证机制，你可以在应用程序的 `bootstrap/app.php` 文件中使用 `preventRequestForgery` 方法：

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(originOnly: true);
})
```

当使用仅来源验证模式（origin-only mode）时，来源验证失败的请求将收到 `403` HTTP 响应，而不是通常与 CSRF token 不匹配相关联的 `419` 响应。

> [!警告]
> `Sec-Fetch-Site` 请求头仅会由浏览器在安全（HTTPS）连接中发送。如果你的应用程序未通过 HTTPS 提供服务，则来源验证将不可用，中间件将回退到 CSRF token 验证。

如果你的应用程序需要接受来自子域名的请求（例如，`dashboard.example.com` 接受来自 `example.com` 的请求），你可以允许同站点（same-site）请求，而不仅仅是相同源（same-origin）请求：

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(allowSameSite: true);
})
```

### 从 CSRF 保护中排除 URI

有时候，你可能希望将一组 URI 排除在 CSRF 保护之外。例如，如果你正在使用 [Stripe](https://stripe.com) 处理支付，并使用它们的 webhook 系统，那么你需要将 Stripe webhook 处理路由从 CSRF 保护中排除，因为 Stripe 不会知道应该向你的路由发送什么 CSRF token。



通常，你应该将这类路由放在 Laravel 自动应用于 `routes/web.php` 文件中所有路由的 `web` 中间件组之外。不过，你也可以通过在应用程序的 `bootstrap/app.php` 文件中向 `preventRequestForgery` 方法提供 URI 来排除特定路由：

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
        'http://example.com/foo/bar',
        'http://example.com/foo/*',
    ]);
})
```

> [!注意]
> 为了方便，在 [运行测试](/docs/laravel/12.x/testing) 时，CSRF 中间件会自动对所有路由禁用。

## X-CSRF-TOKEN

除了将 CSRF token 作为 POST 参数进行检查之外，`PreventRequestForgery` 中间件还会检查 `X-CSRF-TOKEN` 请求头。例如，你可以将 token 存储在 HTML 的 `meta` 标签中：

```blade
<meta name="csrf-token" content="{{ csrf_token() }}">
```

然后，你可以指示像 jQuery 这样的库自动将 token 添加到所有请求头中。这为基于 AJAX 的应用程序使用传统 JavaScript 技术提供了简单且方便的 CSRF 防护：

```js
$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
});
```

## X-XSRF-TOKEN

Laravel 会将当前的 CSRF token 存储在一个加密的 `XSRF-TOKEN` Cookie 中，并随框架生成的每个响应一起返回。你可以使用该 Cookie 的值来设置 `X-XSRF-TOKEN` 请求头。

该 Cookie 主要是为了方便开发者，因为某些 JavaScript 框架和库（例如 Angular 和 Axios）会在同源请求中自动将其值放入 `X-XSRF-TOKEN` 请求头中。

