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中间件,直接定义路由即可。例如添加商品列表接口:
- 在
routes/api.php中添加:
// 公开接口:获取商品列表 Route::get('/products', [ProductController::class, 'index']);
- 创建
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 ]); }
- 前端请求该接口时,无需登录和
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
相关产品推荐
相关产品推荐

