You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel Sanctum+Axios开发SPA遇CSRF令牌不匹配致401未授权问题

Laravel Sanctum + React SPA 认证401 Unauthorized 问题排查与解决

环境配置

  • 后端: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/login POST请求时,请求头已携带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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 11:02:05