Laravel Sanctum+Axios开发SPA遇CSRF令牌不匹配致401未授权问题
环境配置
- 后端:Laravel 10 + Sanctum API认证
- 前端:React(localhost:3000)+ Axios HTTP客户端
- Laravel API运行地址:localhost:8080
CORS与Sanctum配置
已配置CORS允许前端域名请求,Axios设置withCredentials: true。
config/cors.php:
return [ 'paths' => ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => ['*'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => true, ];
config/sanctum.php:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost:3000'))
.env:
APP_URL=http://localhost:8080 SERVER_PORT=8080 FRONTEND_URL=http://localhost:3000 SESSION_DOMAIN=localhost SANCTUM_STATEFUL_DOMAINS=localhost:8080,localhost:3000 SESSION_SECURE_COOKIE=false SESSION_HTTP_ONLY=true SESSION_EXPIRE_ON_CLOSE=true SESSION_DRIVER=cookie SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/
Axios配置
const baseURL = process.env.NEXT_PUBLIC_BACKEND_API_URL || 'http://localhost:8080'; const servicesApi = axiosLib.create({ baseURL: baseURL, headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json', 'Content-Type': 'application/json', }, withCredentials: true, withXSRFToken: true, });
路由配置
routes/web.php:
Route::prefix('api')->group(function () { Route::post('/login', [AuthController::class, 'login']); Route::middleware(['auth:sanctum'])->group(function () { Route::post('/favorites/toggle', [FavoriteController::class, 'favoritesToggle']); }); });
next-auth路由片段(route.js):
providers: [ CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { try { const loginResponse = await servicesApi.post('/api/login', { email: credentials.email, password: credentials.password, })
问题详情
- 调用
await servicesApi.get('/sanctum/csrf-cookie')成功返回200,XSRF-TOKEN Cookie已正确设置 - 后续发送
/api/loginPOST请求时,请求头已携带X-XSRF-TOKEN,但仍返回401 Unauthorized - Laravel抛出
TokenMismatchException
调试发现
VerifyCsrfToken中间件的tokensMatch()方法中,会话令牌($request->session()->token())与解密后的XSRF-TOKEN Cookie值不匹配,Sanctum生成的CSRF令牌与会话令牌无法对应。
问题解答与修复步骤
1. 令牌不匹配的原因
Sanctum的CSRF令牌机制依赖Laravel会话系统,但登录路由放在web.php中时,会触发web中间件组的VerifyCsrfToken验证,该验证用会话令牌做对比,而Sanctum针对SPA的CSRF逻辑与这套机制不兼容。另外,SANCTUM_STATEFUL_DOMAINS包含后端域名,会导致会话上下文混乱,进而令牌无法关联。
2. 确保令牌匹配的方法
步骤1:调整路由位置
将登录路由从web.php移到api.php,api中间件组默认不包含VerifyCsrfToken,Sanctum会单独处理SPA的CSRF验证:
// routes/api.php Route::post('/login', [AuthController::class, 'login']); Route::middleware(['auth:sanctum'])->group(function () { Route::post('/favorites/toggle', [FavoriteController::class, 'favoritesToggle']); });
步骤2:修正Sanctum状态域名配置
SANCTUM_STATEFUL_DOMAINS只需要配置前端域名,后端域名无需加入:
SANCTUM_STATEFUL_DOMAINS=localhost:3000
步骤3:确保请求实例一致性
获取CSRF令牌和发送登录请求必须使用同一个Axios实例,避免Cookie丢失。也可以手动从Cookie中提取令牌并设置到请求头(需引入js-cookie库):
// 在authorize方法中 await servicesApi.get('/sanctum/csrf-cookie'); const xsrfToken = Cookies.get('XSRF-TOKEN'); servicesApi.defaults.headers.common['X-XSRF-TOKEN'] = xsrfToken; const loginResponse = await servicesApi.post('/api/login', { email: credentials.email, password: credentials.password, });
步骤4:确认会话驱动配置
保证SESSION_DRIVER=cookie且SESSION_PATH=/,确保Cookie能在整个域名下共享。
3. 遗漏的关键步骤
- 路由归属错误:SPA认证路由应放在
api.php,而非web.php,避免触发不兼容的VerifyCsrfToken验证逻辑。 - 状态域名冗余:
SANCTUM_STATEFUL_DOMAINS仅需配置前端域名,多余的后端域名会干扰会话上下文。 - 请求实例统一:获取CSRF令牌和发送登录请求必须用同一个Axios实例,否则Cookie无法跨实例共享。
内容的提问来源于stack exchange,提问作者Renars Kilis

