Laravel 9:使用 Fetch API 登录 Sanctum 不起作用

NKn*_*lle 4 laravel fetch-api laravel-sanctum nuxtjs3

环境

  • 圣所版本:v2.15.1
  • 强化版本:v1.13.0
  • Laravel 版本:v9.17.0
  • PHP版本:PHP 8.1.6
  • 数据库驱动程序和版本:基于 Laravel Sail (docker): image: 'mysql:8.0'

描述:

我正在尝试让我的网络应用程序的登录流程正常工作。该 Web 应用程序是使用 Nuxt 3 作为在 http://localhost:3000 上运行的前端 (SPA) 和 Laravel 作为在 http://localhost 上运行的后端编写的。

因为 Nuxt 3 使用 fetch API,而 axios 目前不适用于 Nuxt 3,所以我试图让 Laravel Sanctum 的登录流程和 fetch API 正常工作。

重现步骤:

我在前端 Nuxt 应用程序上使用可组合函数调用 Laravel 后端,如下所示:

export const useLogin = async (email, password) => {
  const config = useRuntimeConfig()
  const tokenResponse = await $fetch(config.baseURL + '/sanctum/csrf-cookie', {
    method: 'GET',
    credentials: 'include'
  })
  const token = getCookie('XSRF-TOKEN')
  console.log(token)
  const loginResponse = await $fetch(config.baseURL + '/login', {
    method: 'POST',
    headers: {
      'X-XSRF-TOKEN': token,
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: {
      "username": email,
      "password": password
    }
  })
}
Run Code Online (Sandbox Code Playgroud)

然后我在 LoginForm-Component 中调用它:

<template>
    <form @submit.prevent="login()" class="bg-blue-500 flex flex-col rounded-xl p-4 text-white">
        <h1 class="text-center mb-4">Login</h1>
        <label for="fname">Email</label>
        <input v-model="form.email" class="rounded-md h-8 mb-4 text-black" type="text" id="email" name="email"><br><br>
        <label for="password">Password</label>
        <input v-model="form.password" class="rounded-md h-8 mb-4 text-black" type="password" id="password" name="password"><br><br>
        <button class="rounded-md bg-orange-700 cursor-pointer" type="submit">Login</button>
    </form>
</template>

<script setup>

const form = {
    email: '',
    password: ''
}

function login() {
    useLogin(form.email, form.password)
}

</script>
Run Code Online (Sandbox Code Playgroud)

问题是,尽管在请求标头中设置了 X-XSRF-Token(如 Laravel Sanctum 文档中所述),但我从登录路由返回 419 错误(csrf 不匹配):

在此输入图像描述

Laravel Sanctum 是否可能处理标头名称区分大小写,因此无法找到令牌?据我所知,fetch API 总是以小写形式发送标头名称,并且无法更改此行为。

附加信息

Laravel 后端正确设置了 cookie: 在此输入图像描述

cookie 数据似乎也没有问题:

绘图

这些是我的 Sanctum 值.env

SANCTUM_STATEFUL_DOMAINS='localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1'
SESSION_DOMAIN=.localhost
Run Code Online (Sandbox Code Playgroud)

已经尝试过了

我还尝试设置正确的时区,config/app.php并且还更改了 docker 容器(laravel sail)内的时区,但 cookie 仍然具有相同的数据值和过期时间,因此似乎没问题。

如果我尝试使用 Insomnia 作为 REST 客户端在全新安装的 Laravel 9 项目(配置如上)中注册新用户,也会发生同样的行为:

请求/响应时间表:

* Preparing request to http://localhost/register
* Current time is 2022-06-09T10:42:49.472Z
* Using default HTTP version
* Disable timeout
* Enable automatic URL encoding
* Enable SSL validation
* Enable cookie sending with jar of 2 cookies
* Hostname in DNS cache was stale, zapped
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#21)

> POST /register HTTP/1.1
> Host: localhost
> User-Agent: insomnia/2022.3.0
> Cookie: laravel_session=eyJpdiI6IkpGdXZWRmh1WHdlc1F2VEpLVzllSFE9PSIsInZhbHVlIjoibk5JV0hmZHIzd1RpazRUOTJOOVBJZm1KNFZGUGlyaVJXTEk3NFl4YWVwSzRDNU5QQXh2Q1BrelNIWW55VWpEUTlXZmZaUjZPQUkrdjRDcnlCZUtkOENMMEhxOHVVWXhxcmFIdjdzRWh4dVgrLy9zRVhmOURFbFduR1hCcFIwcy8iLCJtYWMiOiJmZmZiZmI0MTk5OGNhZmRmMjFkZjgxMDk3MjgwMDBmYWFiNDA4YTZiYjQ1MjFkNjg0Mjk5MDRlMGEyNTU0NTUxIiwidGFnIjoiIn0%3D; XSRF-TOKEN=eyJpdiI6IkhtVjZrTWV2WC9KZFV0NWN4QUo2UVE9PSIsInZhbHVlIjoiQWJqQ2FuVHM1eENWZ212ZUZDNFZJVEErZk5ueGRUTnY3RFl3N2pJbEM4WEFTTE9UVFVWSXR1VkwvbkhhK1lqUkRySUJsWEtUT3RUWHI3UlcvTHpXSkNlNkNVZ3R2RXhkTVpWdWx2VmpZbDhPeWdTenVqVE5BWW1Rc1Qyb0t3L00iLCJtYWMiOiIwNTU2ZjgyMzdhNWU0Mjk1MzkyM2ZkN2Q4NWY2N2Y1MzAzYThlY2YxNmU2MzQyNTYxM2I3YjI2YjkyZjhiODZiIiwidGFnIjoiIn0%3D
> Content-Type: application/json
> Accept: application/json
> X-XSRF-TOKEN: eyJpdiI6IkhtVjZrTWV2WC9KZFV0NWN4QUo2UVE9PSIsInZhbHVlIjoiQWJqQ2FuVHM1eENWZ212ZUZDNFZJVEErZk5ueGRUTnY3RFl3N2pJbEM4WEFTTE9UVFVWSXR1VkwvbkhhK1lqUkRySUJsWEtUT3RUWHI3UlcvTHpXSkNlNkNVZ3R2RXhkTVpWdWx2VmpZbDhPeWdTenVqVE5BWW1Rc1Qyb0t3L00iLCJtYWMiOiIwNTU2ZjgyMzdhNWU0Mjk1MzkyM2ZkN2Q4NWY2N2Y1MzAzYThlY2YxNmU2MzQyNTYxM2I3YjI2YjkyZjhiODZiIiwidGFnIjoiIn0%3D
> Content-Length: 110

| {
|   "name": "test",
|   "email": "test7@local.de",
|   "password": "test1234",
|   "password_confirmation": "test1234"
| }

* Mark bundle as not supporting multiuse

< HTTP/1.1 419 unknown status
< Host: localhost
< Date: Thu, 09 Jun 2022 10:42:49 GMT
< Connection: close
< X-Powered-By: PHP/8.1.6
< Cache-Control: no-cache, private
< Date: Thu, 09 Jun 2022 10:42:49 GMT
< Content-Type: application/json
< Content-Length: 11025

* Replaced cookie laravel_session="eyJpdiI6IlhpSzV3SnJLOExyeXU0NGtaM0piZEE9PSIsInZhbHVlIjoiMkhsekNJYy80OTJHb3o2OWppZ2pSMDg2aHZuNTkzc1pYMXh2VDdHSW9XaUVPSlRkSkphZ013cnNEMW1CY1I1Zy9zNGpHTGtra0tEY1BQWGdhUlFUbWNzM3FJOVNqbTlNSEhxSXFSck1oWUlvbURvRFlYbW1oLytBbGJIUW1wYksiLCJtYWMiOiI1MGZjOGNhNTNmOTcxNTg1MjhkY2FmZjcwMWYwODBlZGE1NzYwZjU2MGJiNzRlZDk0NTU3YzBmZmUxZTVjYmNiIiwidGFnIjoiIn0%3D" for domain localhost, path /, expire 1654778569

< Set-Cookie: laravel_session=eyJpdiI6IlhpSzV3SnJLOExyeXU0NGtaM0piZEE9PSIsInZhbHVlIjoiMkhsekNJYy80OTJHb3o2OWppZ2pSMDg2aHZuNTkzc1pYMXh2VDdHSW9XaUVPSlRkSkphZ013cnNEMW1CY1I1Zy9zNGpHTGtra0tEY1BQWGdhUlFUbWNzM3FJOVNqbTlNSEhxSXFSck1oWUlvbURvRFlYbW1oLytBbGJIUW1wYksiLCJtYWMiOiI1MGZjOGNhNTNmOTcxNTg1MjhkY2FmZjcwMWYwODBlZGE1NzYwZjU2MGJiNzRlZDk0NTU3YzBmZmUxZTVjYmNiIiwidGFnIjoiIn0%3D; expires=Thu, 09 Jun 2022 12:42:49 GMT; Max-Age=7200; path=/; domain=.localhost; httponly; samesite=lax


* Received 10.8 KB chunk
* Closing connection 21
* Saved 1 cookie
Run Code Online (Sandbox Code Playgroud)

错误信息:

{
    "message": "CSRF token mismatch.",
    "exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
    "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
    "line": 379,
    "trace": [
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php",
            "line": 353,
            "function": "prepareException",
            "class": "Illuminate\\Foundation\\Exceptions\\Handler",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php",
            "line": 51,
            "function": "render",
            "class": "Illuminate\\Foundation\\Exceptions\\Handler",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 185,
            "function": "handleException",
            "class": "Illuminate\\Routing\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php",
            "line": 49,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\View\\Middleware\\ShareErrorsFromSession",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
            "line": 121,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
            "line": 64,
            "function": "handleStatefulRequest",
            "class": "Illuminate\\Session\\Middleware\\StartSession",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Session\\Middleware\\StartSession",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php",
            "line": 37,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php",
            "line": 67,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Cookie\\Middleware\\EncryptCookies",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 116,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 726,
            "function": "then",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 703,
            "function": "runRouteWithinStack",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 667,
            "function": "runRoute",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
            "line": 656,
            "function": "dispatchToRoute",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 167,
            "function": "dispatch",
            "class": "Illuminate\\Routing\\Router",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 141,
            "function": "Illuminate\\Foundation\\Http\\{closure}",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
            "line": 21,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php",
            "line": 31,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php",
            "line": 21,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php",
            "line": 40,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\TrimStrings",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php",
            "line": 27,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php",
            "line": 86,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php",
            "line": 49,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Http\\Middleware\\HandleCors",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php",
            "line": 39,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 180,
            "function": "handle",
            "class": "Illuminate\\Http\\Middleware\\TrustProxies",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
            "line": 116,
            "function": "Illuminate\\Pipeline\\{closure}",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 142,
            "function": "then",
            "class": "Illuminate\\Pipeline\\Pipeline",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php",
            "line": 111,
            "function": "sendRequestThroughRouter",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        },
        {
            "file": "/var/www/html/public/index.php",
            "line": 52,
            "function": "handle",
            "class": "Illuminate\\Foundation\\Http\\Kernel",
            "type": "->"
        },
        {
            "file": "/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/resources/server.php",
            "line": 16,
            "function": "require_once"
        }
    ]
}
Run Code Online (Sandbox Code Playgroud)

我不知道如何进一步调查。

NKn*_*lle 6

我误解了我的错误日志,发现使用 Axios 和 Fetch API 还存在另一个小差异:Axios 似乎可以解码 X-XSRF-TOKEN,而 Fetch API 则不能。

所以这个示例令牌是由 Fetch API 发送的: eyJpdiI6IkRScW9GNGtIbndWWFdKbzRiN0VWVkE9PSIsInZhbHVlIjoiNUNuQStiZGt0Y3l0cXhReThHSEJxbCsxRmZvbFFHKzhQV3ArSlg0cjVQVm5qSDZoQ3ZsSERnUTREUXRvczdMOHhYSHFqbm5FUjd2dFpGMlN4bU81NWJ5SWpmem5BQi9vRmJBRFZZWmFSZFlqcHNxbTZ5N1Z0cGJzSmMwcUFRaXUiLCJtYWMiOiI4MjM1MTQ2ODExNzhlY2ExNDk1NDhhOWEwNzE0OWJlMzViOGQxNDJhMTY0YTI2NzYwMThjMzQ5ODVmMDYwMjk1IiwidGFnIjoiIn0%3D

这个示例令牌是由 Axios 发送的: eyJpdiI6IisydDZOOFJoREp5ZnZudUtjRk1teXc9PSIsInZhbHVlIjoiV1dQdjJDZlNrcW43Zlg4TW1yRFdjOVJWVnJkaC9CZndxejJLQ3JEUkJIRXJ3Z2pNb2pxSUJYN0Y2RDBxZ1hKd01mNHF6empsRkFIeEFOSkJKbi8vT0hWOFBYMDdlMkZybzBZdllJQlBFa1lHTytZd1E0aU9vL2pOM1ZRZWwxV0ciLCJtYWMiOiI3NTczZGJiMjg5MWZmOTUzMjhiMTZhNjAxM2ZiZmVjODVjYjc2MGRiMGJkMTFkOGYzOWQzYWQ5MjI5YWIwOTA5IiwidGFnIjoiIn0=

标记字符串末尾有一个非常小的差异(= 与 %3D)。

Laravel 本身发送 Token 值 URIEncoded(带有 %3D),但似乎期望末尾带有 = 的令牌。

文档中没有任何相关内容,但这个问题让我搜索了好几天,而且并不那么明显。

我将代码更改为:

export const useLogin = async (email, password) => {
  const config = useRuntimeConfig()
  const tokenResponse = await $fetch(config.baseURL + '/sanctum/csrf-cookie', {
    method: 'GET',
    credentials: 'include'
  })
  const token = decodeURIComponent(getCookie('XSRF-TOKEN')) // <---- CHANGED
  console.log(token)
  const loginResponse = await $fetch(config.baseURL + '/login', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'X-XSRF-TOKEN': token,
      'Accept': 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
      'X-Requested-With': 'XMLHttpRequest'
    },
    body: {
      "email": email,
      "password": password
    }
  })
}
Run Code Online (Sandbox Code Playgroud)

现在可以使用标准 Fetch API 进行登录。

我希望这可以帮助其他人调试他们的登录流程。

问候。