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

Laravel Sanctum对接Next.js SPA未认证问题及解决方案咨询

Laravel 9 Sanctum + Next.js 认证问题解决方案

问题背景

后端Laravel 9(Sanctum)地址http://127.0.0.1:8000,前端Next.js地址http://localhost:3000,请求带withCredentials: true的/api/user接口返回“Unauthenticated”。需求:

  • 无需登录即可获取公开数据(如商品列表)
  • 登录后可通过会话Cookie或Bearer Token进行认证操作

现有配置梳理

config/cors.php

'paths' => ['api/*', 'sanctum/csrf-cookie','login','logout',],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://127.0.0.1:3000'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,

app/Http/Kernel.php

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
    'api' => [
         \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

config/sanctum.php

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
    '%s%s',
    'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
    Sanctum::currentApplicationUrlWithPort()
))),
'guard' => ['web'],

.env

SESSION_DRIVER=cookie
SANCTUM_STATEFUL_DOMAINS=localhost:3000
SESSION_DOMAIN=localhost

public/.htaccess

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>
    RewriteEngine On
    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]
    # Send Requests To Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

routes/api.php

Route::get('/user', [AuthController::class, 'me'])->middleware('auth:sanctum');

AuthController.php

public function me(Request $request)
{
  return response()->json([
    'status' => 'success',
    'message' => 'Data Fetch Success',
    'data' => [
        "name" => "Zahid Hasan Raju",
    ],
  ]);
}

前端Axios代码

import Head from "next/head";
import axios from "axios";
export default function Home() {
  const getConfig = {
    method: "get",
    url: "http://localhost:8000/api/user",
    withCredentials: true,
  };
  const handleClick = async () => {
    await axios(getConfig).then((res) => console.log(res.data));
    alert("success");
  };
  return (
    <>
      <Head>
        <title>Home Page</title>
      </Head>
      <h1>Home Page</h1>
      <button className='btn btn-success' onClick={handleClick}>
        Fetch user
      </button>
    </>
  );
}

解决方案

一、修复当前认证失败问题

1. 统一前后端域名,修正CORS配置

浏览器会将localhost和127.0.0.1视为不同源,导致Cookie无法跨域携带:

  • 启动后端时使用php artisan serve --host=localhost --port=8000,将后端地址改为http://localhost:8000
  • 修改config/cors.php的allowed_origins,包含前端实际地址:
'allowed_origins' => ['http://localhost:3000'],

2. 前端请求前先获取CSRF Cookie

Sanctum要求SPA在发送认证请求前,先获取CSRF令牌。修改前端点击事件代码:

const handleClick = async () => {
  // 先请求CSRF Cookie接口
  await axios.get('http://localhost:8000/sanctum/csrf-cookie', { withCredentials: true });
  // 再请求用户数据
  await axios(getConfig).then((res) => console.log(res.data));
  alert("success");
};

3. 确认会话配置

保持.env中的以下配置不变:

SESSION_DOMAIN=localhost
SESSION_DRIVER=cookie
SANCTUM_STATEFUL_DOMAINS=localhost:3000

二、实现无需登录的公开数据接口

公开接口不需要添加auth:sanctum中间件,直接定义路由即可。例如添加商品列表接口:

  1. 在routes/api.php中添加:
// 公开接口:获取商品列表
Route::get('/products', [ProductController::class, 'index']);
  1. 创建ProductController并实现index方法:
public function index()
{
    // 模拟商品数据
    $products = [
        ['id' => 1, 'name' => '商品A', 'price' => 99],
        ['id' => 2, 'name' => '商品B', 'price' => 199],
    ];
    return response()->json([
        'status' => 'success',
        'data' => $products
    ]);
}
  1. 前端请求该接口时,无需登录和withCredentials:
const fetchProducts = async () => {
  const res = await axios.get('http://localhost:8000/api/products');
  console.log(res.data);
};

三、登录后认证操作流程

方式1:会话Cookie模式(SPA推荐)

后端配置
  • 在routes/api.php添加登录路由:
Route::post('/login', [AuthController::class, 'login']);
  • 在AuthController添加login方法:
use Illuminate\Support\Facades\Auth;

public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        return response()->json(['message' => '账号或密码错误'], 401);
    }

    return response()->json([
        'status' => 'success',
        'user' => Auth::user()
    ]);
}
前端流程
const login = async () => {
  // 先获取CSRF Cookie
  await axios.get('http://localhost:8000/sanctum/csrf-cookie', { withCredentials: true });
  // 发送登录请求
  try {
    const res = await axios.post('http://localhost:8000/api/login', {
      email: '你的测试邮箱',
      password: '你的测试密码'
    }, { withCredentials: true });
    console.log('登录成功:', res.data.user);
    // 登录成功后,后续请求带withCredentials: true即可自动携带会话Cookie访问认证接口
  } catch (err) {
    console.error('登录失败:', err.response.data.message);
  }
};

方式2:Bearer Token模式(API场景)

后端配置
  • 修改AuthController的login方法,生成并返回Token:
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (!Auth::attempt($credentials)) {
        return response()->json(['message' => '账号或密码错误'], 401);
    }

    $user = Auth::user();
    // 生成API Token
    $token = $user->createToken('api-token')->plainTextToken;

    return response()->json([
        'status' => 'success',
        'user' => $user,
        'token' => $token
    ]);
}
前端流程
// 登录并保存Token
const login = async () => {
  try {
    const res = await axios.post('http://localhost:8000/api/login', {
      email: '你的测试邮箱',
      password: '你的测试密码'
    });
    localStorage.setItem('api_token', res.data.token);
    console.log('登录成功');
  } catch (err) {
    console.error('登录失败:', err.response.data.message);
  }
};

// 使用Token访问认证接口
const fetchUser = async () => {
  const token = localStorage.getItem('api_token');
  try {
    const res = await axios.get('http://localhost:8000/api/user', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    console.log(res.data);
  } catch (err) {
    console.error('请求失败:', err.response.data.message);
  }
};

内容的提问来源于stack exchange,提问作者zahid hasan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 02:25:40