Laravel : 使用清漆和 CSRF 令牌

Laravel : using Varnish and CSRF Token

我正在使用 Varnish 来缓存 Laravel 个页面。

为了给大家展示不同的CSRF Tokens,我使用ESI从缓存中排除CSRF :

app.blade.php

<html lang="fr">
    <head>
        <title>@yield('title')</title>
        <meta name="description" content="@yield('description')">

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1, initial-scale=1">
        <esi:include src="/esi" />
...

在 /esi 中,我只是使用控制器来显示带有 CSRF 令牌的元标记

csrf-token.blade.php

<meta name="csrf-token" content="{{ csrf_token() }}">

在 ajax 表单中,我获取令牌并将其传递给 POST 请求:

form.js

this.formData._token = document.head.querySelector("[name~=csrf-token][content]").content;
axios.post('url', this.formData);

接收到通过POST发送的_token,但是当laravel尝试验证它时,通过ESI生成的与$session->token()中的不同。

有人知道如何在 Laravel 中使用 Varnish 正确处理 csrf 吗?

如果有帮助,这是我的 Varnish 配置文件 default.vcl

sub vcl_recv {
    call devicedetect;

    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    if (req.esi_level == 0 && req.url ~ "^/esi(.*)?") {
        return (synth(403, "Error"));
    }

    return (hash);
}

sub vcl_hash {
    hash_data(req.url);

    if (req.http.host) {
        hash_data(req.http.host);
    } else {
        hash_data(server.ip);
    }

    if (req.http.X-UA-Device) {
        hash_data(req.http.X-UA-Device);
    }

    return (lookup);
}

sub vcl_backend_response {
    if (bereq.method != "GET"&&bereq.method != "HEAD") {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
        return (deliver);
    }

    if (beresp.http.X-Reverse-Proxy-TTL) {
        set beresp.ttl = std.duration(beresp.http.X-Reverse-Proxy-TTL + "s", 0s);
        unset beresp.http.X-Reverse-Proxy-TTL;
        return (deliver);
    }

    set beresp.do_esi = true;
    set beresp.grace = 5m;
    set beresp.ttl = 5h;
    set beresp.http.Cache-Control = "public, s-maxage=18000, maxage=3600";
    unset beresp.http.set-cookie;
}

恐怕您的 VCL 文件有一些问题。不仅与 CSRF 令牌有关,而且与传统缓存行为有关。 [built-in VCL][1] 的一些基本规则在您的配置中被忽略了。

Laravel 文档指出 CSRF 令牌应与 session 数据中的值相匹配。但是,您的 vcl_backend_response 逻辑几乎无条件地删除了 Set-Cookie header。

我相信我们将需要 Set-Cookie 响应 header 和 Cookie 请求 header 以确保 Laravel session 保持活跃。

了解业务逻辑

在编写正确的 VCL 代码之前,我们需要确保了解 CSRF 令牌背后的业务逻辑。

根据 https://github.com/laravel/framework/blob/0d601f598a2434b8b126c06af75a0f089b10a102/src/Illuminate/Session/Store.php#L614-L617,令牌是 40 个字符的随机字符串。

乍一看,令牌似乎是每个 session 而不是每个请求都是唯一的。这意味着只要我们有每个 session.

的缓存变体,我们就可以缓存它们

在 POST 调用期间传递 CSRF 令牌

https://laravel.com/docs/8.x/csrf 指出 CSRF 令牌可以通过两种方式传递:

  • 通过_tokenpost字段
  • 通过X-CSRF-TOKEN请求header

如果您打算使用 post 字段,这就是您要添加的内容:

<input type="hidden" name="_token" value="{{ csrf_token() }}" />

It seems as though you're storing the token in a <meta> HTML tag, and passing it via Javascript logic as a _token input field. Not sure if that is the best way, but if you're sure that Laravel receives it as an _token field, all is good.

为什么遇到令牌不匹配?

我看到每个 POST 呼叫收到的响应都是 unset beresp.http.set-cookie;。这意味着 Set-Cookie: laravel_session=xyz 不会被浏览器处理 GET 调用。

但是,如果 session 从未建立,在通过 POST 调用传递令牌之前,如何在您的页面上设置令牌?

会发生的情况是POST请求可能包含token,但是由于laravel_sessioncookie没有发送到后端,所以会建立一个新的session,并且将生成一个新令牌。

我们有哪些选择?

最简单的方法是在访问包含联系表的页面时接受正在建立的 session。

允许Laravel生成令牌

如果您网站的 99% 不依赖 laravel_session cookie,我们可以使用 VCL 删除这些 cookie。例如,我们可以只允许 /esi 端点上的 cookie 处理。我赞成将其称为 /token 端点。

我也会缓存来自 /token 端点的输出,但我会确保根据 session.

创建一个缓存变体

当然,我们需要清理我们的 VCL 代码才能做到这一点。

我们自己生成 CSRF 令牌

也可以在 Varnish 中处理 CSRF 令牌生成。这意味着令牌由 Varnish 发布并由 Varnish 验证。

也可以与Laravel集成并将结果存储在Laravel session存储中。

这个选项要复杂得多,但也更强大。

我建议我们尝试第一个选项,如果 ESI 调用没有产生预期的性能,我们仍然可以在 Varnish 中处理 CSRF 令牌

VCL代码

这是我建议的 VCL 代码。请记住,我做了一些假设:

  • session cookie 名为 laravel_session
  • devicedetect 子例程包含在 /etc/varnish/devicedetect.vcl
  • CSRF 令牌端点是 /token
  • X-Reverse-Proxy-TTL响应header用于设置TTL
  • Session 数据仅在 /token 端点
  • 上需要
vcl 4.0;

import std;
include "devicedetect.vcl";

sub vcl_recv {
    call devicedetect;

    set req.http.Cookie = ";" + req.http.Cookie;
    set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
    set req.http.Cookie = regsuball(req.http.Cookie, ";(laravel_session)=", "; =");
    set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");
    
    if (req.http.cookie ~ "^\s*$" || req.url != "/token") {
        unset req.http.cookie;
    }
}

sub vcl_hash {
    if(req.url == "token" && req.http.cookie ~ "^.*laravel_session=([^;]*);*.*$") {
        hash_data(regsub(req.http.cookie,"^.*laravel_session=([^;]*);*.*$",""));
    }
}

sub vcl_backend_response {
    set beresp.grace = 5m;

    if (beresp.http.X-Reverse-Proxy-TTL) {
        set beresp.ttl = std.duration(beresp.http.X-Reverse-Proxy-TTL + "s", 0s);
        unset beresp.http.X-Reverse-Proxy-TTL;
    } else {
        set beresp.ttl = 5h;
    }

    if(bereq.url == "/token") {
        set beresp.do_esi = true;
    }
}

此 VCL 代码将允许处理 Set-Cookie header。这意味着浏览器将收到 laravel_session cookie,但 Varnish 只会在 ESI 调用期间收到 /token 端点时使用它。

/token 端点将被缓存,但 vcl_hash 使用该端点的 session ID 扩展哈希键。

我不太喜欢 X-Reverse-Proxy-TTL header 逻辑和 set beresp.ttl = 5h 回退:HTTP 缓存约定有 Cache-Control header这样,就没必要re-invent轮子了。

如果您在 Laravel 应用程序中使用正确的 Cache-Control header,将执行以下 backend_response 逻辑:

sub vcl_backend_response {
    set beresp.grace = 5m;

    if(bereq.url == "/token") {
        set beresp.do_esi = true;
    }
}