Nuxt3+Sanctum Cookie认证指南:告别`nuxt-auth-utils`会话难题
2025-05-04 22:27:54
搞定 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
默认的工作模式可能对不上。
-
Sanctum SPA 认证机制 :
- Sanctum 对 SPA 主要推荐用 Cookie-based 认证。流程大概是这样:
- 前端先请求 Laravel 的
/sanctum/csrf-cookie
接口,获取XSRF-TOKEN
Cookie。 - 发起登录请求(比如
/api/login
),带上从 Cookie 里读出来的X-XSRF-TOKEN
头。 - 如果登录成功,Laravel 会在响应里设置一个
HttpOnly
、Secure
(如果配置了)、SameSite
的 session Cookie。 - 后续所有需要认证的请求,浏览器会自动带上这个 session Cookie。Laravel 通过验证这个 Cookie 来识别用户。
- 前端先请求 Laravel 的
- 这个流程依赖于:
- 前端和后端需要配置好 CORS ,特别是允许携带凭证 (
supports_credentials = true
) 并且allowed_origins
包含你的 Nuxt 前端地址。 - Laravel 的
.env
文件里,SANCTUM_STATEFUL_DOMAINS
要包含你的 Nuxt 应用域名 (比如yourapp.loc:3000
)。
- 前端和后端需要配置好 CORS ,特别是允许携带凭证 (
- Sanctum 对 SPA 主要推荐用 Cookie-based 认证。流程大概是这样:
-
nuxt-auth-utils
的 Session 端点 (/api/_auth/session
) :- 这个库提供了一个内置的 API 路由
/api/_auth/session
来获取用户的会话信息。 - 它的默认实现可能比较通用,或者更偏向于某些特定的认证提供商(比如 OAuth),不一定能自动理解并处理 Sanctum 设置的那个 session Cookie。
- 当它去检查会话状态时,如果没配置对,它可能没法正确地向 Laravel 后端发起一个 已认证 的请求来获取用户信息,或者它不知道该检查哪个 Cookie。所以,结果就是返回
{}
,表示未认证。
- 这个库提供了一个内置的 API 路由
-
可能的配置缺失 :
- Nuxt 端发起请求时,没有正确处理 Cookie 和 CSRF Token。特别是,后续的请求(包括
/api/_auth/session
内部发起的请求,如果它需要访问后端的话)可能没有配置credentials: 'include'
,导致浏览器不发送 Cookie。 - CORS 或
SANCTUM_STATEFUL_DOMAINS
配置不当,导致 Cookie 无法跨域设置或发送。
- Nuxt 端发起请求时,没有正确处理 Cookie 和 CSRF Token。特别是,后续的请求(包括
解决方案
别急,路不止一条。我们可以调整策略,让 Nuxt 和 Sanctum 顺畅对接。
方案一:拥抱 Sanctum 的 Cookie 认证(推荐给 SPA)
既然 Sanctum 对 SPA 有一套成熟的 Cookie 方案,咱们可以直接在 Nuxt 里适配这套机制,甚至可能不需要 nuxt-auth-utils
的核心会话管理。
原理: 让 Nuxt 应用像一个标准的 SPA 一样,直接和 Laravel API 交互,依赖浏览器自动处理 Cookie。关键在于确保请求能正确发送和接收 Cookie,以及处理 CSRF 保护。
步骤:
-
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.loc
和api.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
。
-
Nuxt Fetch 配置(前端)
- 使用 Nuxt 提供的
$fetch
或useFetch
发起请求。为了能发送和接收跨域 Cookie,必须配置credentials: 'include'
。 - CSRF 初始化: 在应用加载时(例如,在 Nuxt 插件或
app.vue
的onMounted
里),需要先请求 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
。
- 使用 Nuxt 提供的
-
移除或调整
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 的状态。这就引出了方案二。
- 如果你完全采用这种纯 Cookie 的方式,
安全建议:
- 始终在生产环境使用 HTTPS。
- 确保 Laravel Session Cookie 配置了
secure=true
和httponly=true
。 SameSite
属性根据你的部署情况设置为Lax
或Strict
。如果前后端在不同子域,可能需要Lax
;如果可能从其他站点发起请求,需要仔细考虑SameSite
策略。
方案二:改造 nuxt-auth-utils
的 Session 端点
如果你确实想继续使用 nuxt-auth-utils
并让它了解 Sanctum 的 Cookie 状态。
原理: 重写或扩展 nuxt-auth-utils
的 /api/_auth/session
服务器路由。让这个路由的处理器去做正确的事情:带上浏览器传来的 Cookie,去请求 Laravel 的 /api/user
(或其他能验证身份的端点),然后根据 Laravel 的响应来返回用户数据或空对象。
步骤:
-
完成方案一中的 Laravel 和 Nuxt Fetch 配置: 基础的 Cookie 设置和 CSRF 必须能工作。
-
创建/覆盖 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 需要使用公开可访问的地址。
- 在你的 Nuxt 项目里,创建文件
-
调整登录逻辑(与方案一类似): 登录成功后,需要调用
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(比如存在 localStorage
或 sessionStorage
),然后在每次需要认证的请求头里带上 Authorization: Bearer <token>
。nuxt-auth-utils
对 Token 认证模式通常有更好的原生支持。
步骤:
-
Laravel 配置:
- 修改认证逻辑,使其在登录成功后创建并返回一个 Sanctum API Token。
- API 路由 (
routes/api.php
) 使用auth:sanctum
中间件保护。 - 不再依赖
web
中间件组的 Session 和 CSRF。
-
Nuxt 配置:
- 配置
nuxt-auth-utils
使用Bearer
Token 策略。可能需要设置 Token 的存储位置、请求头名称等。 - 登录函数需要保存返回的 Token。
$fetch
或useFetch
需要配置自动附加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 端如何与之交互。