返回

Nuxt3+Sanctum Cookie认证指南:告别`nuxt-auth-utils`会话难题

vue.js

搞定 Nuxt 3 + Laravel Sanctum 身份验证:告别 nuxt-auth-utils 的 sessão 难题

上手一个 Nuxt 3 配 Laravel 的新项目,想着认证这块儿用 nuxt-auth-utils 应该挺方便。结果呢?一顿操作猛如虎,就是没法跟 Laravel Sanctum 好好配合,用户登是登了,nuxt-auth-utils 那边死活不认账。

具体来说,调用 Laravel 的登录接口,后端看着是成功返回了点东西(可能是设置了 Cookie),紧接着 nuxt-auth-utils 会自动请求它自己的 /api/_auth/session 端点。问题就出在这儿,这个请求要么没带对认证信息,要么处理逻辑不对,反正最后返回一个空荡荡的 {},前端自然就认为用户没登录,卡这儿了。

瞅一眼当时的登录代码片段:

            // 之前的登录尝试代码
            const login = async () => {
                await $fetch('https://ecommerce.loc/api/login', { // 假设这是你的 Laravel 登录 API
                    method: 'POST',
                    body: {
                        email: form.email,
                        password: form.password,
                    },
                }).then(async (data) => {
                    // 这里是关键:登录请求成功后,需要一种机制让 Nuxt 端知道用户状态
                    // 直接调用 fetch() (假设是 nuxt-auth-utils 的内部刷新) 或者 toast 提示,
                    // 并不能保证 /api/_auth/session 能正确获取到认证状态。
                    fetch(); // 假设这是触发 nuxt-auth-utils 状态更新的函数

                    toast.add({
                        color: 'green',
                        title: 'User logged in successfully', // 登录请求本身可能成功了
                    });

                    // router.push('/admin'); // 跳转可能因为认证状态未更新而失败或被守卫拦下
                }).catch((err) => {
                    console.log(err)
                    toast.add({
                        color: 'red',
                        title: err.data?.message || err.message,
                    })
                })
            }

翻了 nuxt-auth-utils 的文档,确实没找到专门针对 Laravel Sanctum SPA 模式(基于 Cookie)的现成方案。那这到底是咋回事?又该怎么解决呢?

问题根源分析

这事儿的核心在于 Laravel Sanctum 给单页应用(SPA)设计的认证方式和 nuxt-auth-utils 默认的工作模式可能对不上。

  1. Sanctum SPA 认证机制

    • Sanctum 对 SPA 主要推荐用 Cookie-based 认证。流程大概是这样:
      • 前端先请求 Laravel 的 /sanctum/csrf-cookie 接口,获取 XSRF-TOKEN Cookie。
      • 发起登录请求(比如 /api/login),带上从 Cookie 里读出来的 X-XSRF-TOKEN 头。
      • 如果登录成功,Laravel 会在响应里设置一个 HttpOnlySecure (如果配置了)、SameSite 的 session Cookie。
      • 后续所有需要认证的请求,浏览器会自动带上这个 session Cookie。Laravel 通过验证这个 Cookie 来识别用户。
    • 这个流程依赖于:
      • 前端和后端需要配置好 CORS ,特别是允许携带凭证 (supports_credentials = true) 并且 allowed_origins 包含你的 Nuxt 前端地址。
      • Laravel 的 .env 文件里,SANCTUM_STATEFUL_DOMAINS 要包含你的 Nuxt 应用域名 (比如 yourapp.loc:3000)。
  2. nuxt-auth-utils 的 Session 端点 (/api/_auth/session)

    • 这个库提供了一个内置的 API 路由 /api/_auth/session 来获取用户的会话信息。
    • 它的默认实现可能比较通用,或者更偏向于某些特定的认证提供商(比如 OAuth),不一定能自动理解并处理 Sanctum 设置的那个 session Cookie。
    • 当它去检查会话状态时,如果没配置对,它可能没法正确地向 Laravel 后端发起一个 已认证 的请求来获取用户信息,或者它不知道该检查哪个 Cookie。所以,结果就是返回 {},表示未认证。
  3. 可能的配置缺失

    • Nuxt 端发起请求时,没有正确处理 Cookie 和 CSRF Token。特别是,后续的请求(包括 /api/_auth/session 内部发起的请求,如果它需要访问后端的话)可能没有配置 credentials: 'include',导致浏览器不发送 Cookie。
    • CORS 或 SANCTUM_STATEFUL_DOMAINS 配置不当,导致 Cookie 无法跨域设置或发送。

解决方案

别急,路不止一条。我们可以调整策略,让 Nuxt 和 Sanctum 顺畅对接。

方案一:拥抱 Sanctum 的 Cookie 认证(推荐给 SPA)

既然 Sanctum 对 SPA 有一套成熟的 Cookie 方案,咱们可以直接在 Nuxt 里适配这套机制,甚至可能不需要 nuxt-auth-utils 的核心会话管理。

原理: 让 Nuxt 应用像一个标准的 SPA 一样,直接和 Laravel API 交互,依赖浏览器自动处理 Cookie。关键在于确保请求能正确发送和接收 Cookie,以及处理 CSRF 保护。

步骤:

  1. Laravel 配置检查(后端)

    • .env 文件:
      SESSION_DRIVER=cookie # 或 database/redis,确保 session 能工作
      SESSION_DOMAIN=.yourdomain.com # 重要:共享主域名,例如 .ecommerce.loc
      SANCTUM_STATEFUL_DOMAINS=yourapp.loc:3000,www.yourapp.loc:3000 # 你的 Nuxt 前端地址,注意端口
      
      SESSION_DOMAIN 设置为共享的主域名(比如 .ecommerce.loc,注意前面的点),这样 ecommerce.locapi.ecommerce.loc (如果后端在子域) 就能共享 Cookie。如果 Nuxt 和 Laravel 在同一个主域下但不同子域或端口,这个配置很关键。如果它们完全在不同顶级域,Sanctum 的 Cookie 方案会遇到挑战,可能需要考虑 Token 方案。
    • config/cors.php
      'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'], // 确保你的登录、登出、API 路径被包含
      'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], // 使用环境变量配置前端地址
      'allowed_origins_patterns' => [],
      'allowed_headers' => ['*'], // 或者更精确地指定 'X-XSRF-TOKEN', 'Content-Type', 'Accept', 'Authorization''allowed_methods' => ['*'], // 或者指定 'POST', 'GET', 'OPTIONS', 'PUT', 'DELETE'
      'exposed_headers' => [],
      'max_age' => 0,
      'supports_credentials' => true, // !! 必须为 true !!
      
      .env 文件里加上 FRONTEND_URL=https://ecommerce.loc:3000
  2. Nuxt Fetch 配置(前端)

    • 使用 Nuxt 提供的 $fetchuseFetch 发起请求。为了能发送和接收跨域 Cookie,必须配置 credentials: 'include'
    • CSRF 初始化: 在应用加载时(例如,在 Nuxt 插件或 app.vueonMounted 里),需要先请求 Laravel 的 /sanctum/csrf-cookie 端点。这一步是为了获取 XSRF-TOKEN
    // plugins/sanctum.client.ts (或者在 app.vue 的 onMounted)
    import { $fetch } from 'ofetch';
    
    export default defineNuxtPlugin(async (nuxtApp) => {
      const config = useRuntimeConfig();
      const baseURL = config.public.laravelBaseUrl; // 从 nuxt.config.ts 读取后端地址
    
      try {
        await $fetch('/sanctum/csrf-cookie', {
          baseURL: baseURL,
          credentials: 'include', // 必须包含,以便接收 Set-Cookie
        });
        console.log('CSRF cookie fetched.');
      } catch (error) {
        console.error('Failed to fetch CSRF cookie:', error);
        // 可能需要处理错误,比如提示用户刷新页面
      }
    });
    
    // nuxt.config.ts
    export default defineNuxtConfig({
      runtimeConfig: {
        public: {
          laravelBaseUrl: process.env.LARAVEL_BASE_URL || 'https://ecommerce.loc' // 从环境变量读取后端 API 地址
        }
      }
    })
    
    • 登录请求修改:
    // 在你的登录组件或方法里
    import { $fetch } from 'ofetch';
    import { useAuth } from '#imports'; // 如果你仍然想用 nuxt-auth-utils 的某些功能,比如状态管理
    
    const form = reactive({ email: '', password: '' });
    const toast = useToast(); // 假设你用了某个 UI 库的 Toast
    const { fetch: refreshAuthSession } = useAuth(); // 获取 nuxt-auth-utils 的刷新函数 (如果后面选择方案二)
    const runtimeConfig = useRuntimeConfig();
    
    const login = async () => {
        try {
            // 注意:$fetch 默认会处理 CSRF (如果 Cookie 存在)
            // 但最好确保 csrf-cookie 请求已完成
            await $fetch('/api/login', {
                baseURL: runtimeConfig.public.laravelBaseUrl,
                method: 'POST',
                body: {
                    email: form.value.email,
                    password: form.value.password,
                },
                credentials: 'include', // !! 必须包含 !! 这样浏览器才会发送登录后设置的 Session Cookie
            });
    
            // 登录成功后,Laravel 应该设置了 session cookie
            // 现在需要通知 Nuxt 应用(和 nuxt-auth-utils,如果用的话)用户状态变了
            // 方法 A: 直接获取用户信息验证
            await fetchUser(); // 调用一个获取用户信息的函数 (见下方)
    
            // 方法 B: (如果结合方案二) 显式调用 nuxt-auth-utils 的刷新
            // await refreshAuthSession();
    
            toast.add({ color: 'green', title: '登录成功!' });
            // router.push('/admin'); // 现在可以跳转了
        } catch (err: any) {
            console.error('登录失败:', err);
            toast.add({
                color: 'red',
                title: err.data?.message || err.message || '登录过程中发生错误',
            });
        }
    }
    
    // 获取用户信息的辅助函数
    async function fetchUser() {
        try {
            const user = await $fetch('/api/user', { // 请求 Laravel 的用户端点
               baseURL: runtimeConfig.public.laravelBaseUrl,
               credentials: 'include', // 带上 Cookie
               headers: {
                   'Accept': 'application/json', // Laravel 通常需要这个
               }
            });
            // 在这里处理用户信息,比如存到 Pinia store 或者 Nuxt 的 useState
            console.log('用户信息:', user);
            // setUserState(user); // 更新全局状态
            return user;
        } catch (error) {
            console.error('获取用户信息失败:', error);
            // 可能表示未登录或 session 过期
            // clearUserState(); // 清除用户状态
            return null;
        }
    }
    
    • 后续认证请求: 所有需要登录才能访问的 API 请求,都记得加上 credentials: 'include'baseURL
  3. 移除或调整 nuxt-auth-utils

    • 如果你完全采用这种纯 Cookie 的方式,nuxt-auth-utils 主要的 session 管理(/api/_auth/session)可能就多余了。你可以自己写一个简单的 useAuth composable 或者 Pinia store 来管理从 /api/user 获取的用户状态。
    • 如果你还想用 nuxt-auth-utils 的其他功能(比如全局状态 useAuth().loggedIn),你需要让它的 /api/_auth/session 端点能正确反映 Sanctum 的状态。这就引出了方案二。

安全建议:

  • 始终在生产环境使用 HTTPS。
  • 确保 Laravel Session Cookie 配置了 secure=truehttponly=true
  • SameSite 属性根据你的部署情况设置为 LaxStrict。如果前后端在不同子域,可能需要 Lax;如果可能从其他站点发起请求,需要仔细考虑 SameSite 策略。

方案二:改造 nuxt-auth-utils 的 Session 端点

如果你确实想继续使用 nuxt-auth-utils 并让它了解 Sanctum 的 Cookie 状态。

原理: 重写或扩展 nuxt-auth-utils/api/_auth/session 服务器路由。让这个路由的处理器去做正确的事情:带上浏览器传来的 Cookie,去请求 Laravel 的 /api/user (或其他能验证身份的端点),然后根据 Laravel 的响应来返回用户数据或空对象。

步骤:

  1. 完成方案一中的 Laravel 和 Nuxt Fetch 配置: 基础的 Cookie 设置和 CSRF 必须能工作。

  2. 创建/覆盖 Nuxt Server Route:

    • 在你的 Nuxt 项目里,创建文件 server/api/_auth/session.get.ts。这会覆盖 nuxt-auth-utils 的默认实现。
    // server/api/_auth/session.get.ts
    import { defineEventHandler, getRequestHeaders, setResponseStatus } from 'h3';
    import { $fetch } from 'ofetch'; // 需要安装 ofetch: npm install ofetch
    
    export default defineEventHandler(async (event) => {
      const config = useRuntimeConfig(); // 获取运行时配置
      const laravelBaseUrl = config.laravelBaseUrl; // 需要在 nuxt.config.ts 定义这个私有变量
    
      // 从客户端请求中提取 Cookie,转发给 Laravel
      const headers = getRequestHeaders(event);
      const requestHeaders = {
          'Cookie': headers.cookie || '', // 把浏览器带来的 Cookie 透传过去
          'Accept': 'application/json',
          'Referer': headers.referer || '', // Sanctum 有时会检查 Referer
      };
    
      try {
          // 使用服务器端的 fetch 向 Laravel /api/user 发起请求
          const user = await $fetch('/api/user', {
              baseURL: laravelBaseUrl,
              headers: requestHeaders,
              // 在服务器端请求不需要 'credentials: include', Cookie 是通过 headers.cookie 传的
              // 但是要注意 h3/nitro 如何处理代理 Cookie
    
              // 重要: 这里 fetch 是从 Nuxt 服务器发起的,要确保 Cookie 正确传递
              // 一种更可靠的方式是直接从 event 对象获取 Cookie 并设置
          });
    
          // 如果请求成功并且返回了用户数据,说明用户已通过 Sanctum 认证
          return user; // 返回获取到的用户信息
    
      } catch (error: any) {
          // 如果请求失败 (比如 401 Unauthorized 或 419 CSRF Token Mismatch),说明未认证
          console.error('Auth session check failed:', error.status, error.data);
    
          // 清除可能无效的认证上下文(可选,取决于 nuxt-auth-utils 的设计)
          // event.context.auth = undefined; // 示例,具体看库文档
    
          setResponseStatus(event, 401); // 设置响应状态码
          return {}; // 返回空对象,表示未认证
      }
    });
    
    // nuxt.config.ts 添加后端地址 (服务器端使用)
    export default defineNuxtConfig({
        runtimeConfig: {
            laravelBaseUrl: process.env.INTERNAL_LARAVEL_BASE_URL || 'http://laravel-service:80', // 服务器间通信可能用内部地址/Docker服务名
            public: {
                laravelBaseUrl: process.env.LARAVEL_BASE_URL || 'https://ecommerce.loc' // 客户端使用的公开地址
            }
        },
        // ... 其他配置
    })
    
    • 注意 runtimeConfig: 我们需要区分客户端 (public) 和服务器端 (private) 的 Laravel 地址。服务器到服务器的请求可能使用内部网络地址(如 Docker 服务名 http://laravel-service),而客户端 JS 需要使用公开可访问的地址。
  3. 调整登录逻辑(与方案一类似): 登录成功后,需要调用 nuxt-auth-utils 提供的 fetch() 或类似方法来触发它重新请求 /api/_auth/session(现在是我们自定义的版本了)。

    // ... 登录代码同方案一 ...
    .then(async (data) => {
        // ... 其他操作 ...
        await refreshAuthSession(); // 调用 nuxt-auth-utils 的刷新函数
        // ... toast 和跳转 ...
    })
    // ... catch ...
    

安全建议:

  • 自定义的 session.get.ts API 端点本身不需要额外的 CSRF 保护,因为它依赖的是浏览器发送给它的、由 Sanctum 验证的 Session Cookie。
  • 确保 Nuxt 服务器到 Laravel 服务器的网络通信是安全的(比如在内网,或使用 HTTPS)。

方案三:改用 Sanctum API Tokens (如果情况允许)

如果你的应用场景不仅仅是 SPA(比如,还有移动 App),或者 Cookie 方式实在配置不顺,可以考虑让 Laravel 发放 API Token。

原理: 登录时,Laravel 返回一个临时的 API Token。前端(Nuxt)保存这个 Token(比如存在 localStoragesessionStorage),然后在每次需要认证的请求头里带上 Authorization: Bearer <token>nuxt-auth-utils 对 Token 认证模式通常有更好的原生支持。

步骤:

  1. Laravel 配置:

    • 修改认证逻辑,使其在登录成功后创建并返回一个 Sanctum API Token。
    • API 路由 (routes/api.php) 使用 auth:sanctum 中间件保护。
    • 不再依赖 web 中间件组的 Session 和 CSRF。
  2. Nuxt 配置:

    • 配置 nuxt-auth-utils 使用 Bearer Token 策略。可能需要设置 Token 的存储位置、请求头名称等。
    • 登录函数需要保存返回的 Token。
    • $fetchuseFetch 需要配置自动附加 Authorization 头。

缺点:

  • Token 需要安全地存储在前端,比 HttpOnly Cookie 更容易受到 XSS 攻击。
  • 需要自己处理 Token 过期和刷新逻辑。
  • 这与 Sanctum 推荐给 SPA 的标准 Cookie 流程不同。

安全建议:

  • 绝对不要把 Token 存在 Local Storage,优先考虑 Session Storage 或内存。考虑使用具有适当安全标志的 Cookie(如果不是 HttpOnly)来存储,但需权衡。
  • 实现完善的 Token 刷新机制。
  • 防御 XSS 攻击是重中之重。

进阶使用技巧

  • API 请求代理: 可以在 Nuxt 服务器端设置代理路由,将所有对 /api/* 的请求转发给 Laravel。这样前端只与 Nuxt 服务器通信,可以隐藏后端地址,简化 CORS 配置(前端与 Nuxt 服务器同源),并可能在 Nuxt 服务器层做一些预处理或缓存。
  • 全局状态管理: 结合 Pinia 或 Nuxt 的 useState 来存储用户信息,并提供全局的 useAuth composable,使其可以在任何组件或页面中方便地访问用户状态和认证方法(登录、登出、检查状态)。
  • 中间件/路由守卫: 使用 Nuxt 的路由中间件来保护需要登录才能访问的页面。中间件里检查用户状态(无论是通过自定义的状态管理还是 nuxt-auth-utils 的状态),未登录则重定向到登录页。
// middleware/auth.global.ts (示例)
export default defineNuxtRouteMiddleware((to, from) => {
  // 假设你有一个 useMyAuth composable 或者用了 nuxt-auth-utils 的状态
  const { loggedIn, user } = useMyAuth(); // 或者 const { loggedIn } = useAuth();

  // 定义需要认证的路由路径前缀
  const protectedRoutes = ['/admin', '/profile'];

  if (protectedRoutes.some(path => to.path.startsWith(path))) {
    if (!loggedIn.value) {
      console.log('访问受限页面,但未登录,重定向到 /login');
      return navigateTo('/login'); // 重定向到登录页
    }
    // 可以在这里添加角色或权限检查
    // if (to.meta.requiresRole && user.value.role !== to.meta.requiresRole) {
    //   return abortNavigation('无权访问');
    // }
  }
});

通过以上分析和方案,你应该能找到适合你项目的方式,让 Nuxt 3 和 Laravel Sanctum 这对组合愉快地处理用户身份验证了。关键在于理解 Sanctum 的工作原理,并相应地配置 Nuxt 端如何与之交互。