React+Laravel Sanctum首次登录报401 Unauthorized,二次登录正常
问题:React + Laravel Sanctum 首次登录401,第二次成功的问题
我开发了一个React前端、Laravel 10后端(使用Sanctum做身份验证)的应用。首次登录时会出现“401: unauthorized”错误,第二次尝试则能成功登录;同一用户登出后再次登录可正常使用,但切换其他用户时仍需两次尝试才能登录。
Laravel端配置
auth.php
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], ],
api路由
Route::post('login', LoginController::class)->name('login'); Route::post('logout', function (Request $request) { Auth::guard('web')->logout(); }); Route::middleware('auth:sanctum')->group(function () { // admin routes here });
Kernel.php
class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array<int, class-string|string> */ protected $middleware = [ // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\TrustProxies::class, \Illuminate\Http\Middleware\HandleCors::class, \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, // \App\Http\Middleware\HandleQueryStringParams::class, \App\Http\Middleware\SwitchLocale::class, ]; /** * The application's route middleware groups. * * @var array<string, array<int, class-string|string>> */ 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, \Illuminate\Routing\Middleware\ThrottleRequests::class . ':api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; /** * The application's middleware aliases. * * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. * * @var array<string, class-string|string> */ protected $middlewareAliases = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; }
LoginController.php
class LoginController extends Controller { /** * Handle the incoming request. */ public function __invoke(LoginRequest $request) { $credentials = $request->validated(); if (Auth::attempt($credentials)) { $user = Auth::user(); // check if user is active if ($user->active != true) { return alert_error(__('auth.not_activated'), 422); } // updated signed_in_at field $user = User::find($user->id); $user->logged_in_at = Carbon::now(); $user->save(); $auth = [ 'uuid' => $user->uuid, 'role' => $user->role_id, 'name' => $user->name, ]; return result_success(__('auth.auth_success'), $auth); } else { return alert_error(__('auth.auth_failed'), 422); } } }
React端配置
axios配置
// configure axios export const http = axios.create({ baseURL: import.meta.env.VITE_API_URL, withCredentials: true, withXSRFToken: true, headers: { "Content-Type": "application/json", Accept: "application/json", }, });
CsrfCookie组件
const HttpCsrfCookie = ({ children }) => { useEffect(() => { http .get(import.meta.env.VITE_CSRF_COOKIE_URL) .then((response) => { iLog(response); iLog("csrf: done"); }) .catch((error) => { iLog(error.response); }); }, []); return children; }; export default HttpCsrfCookie;
main.js
ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <HttpCsrfCookie> <HttpInterceptor> <QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> </HttpInterceptor> </HttpCsrfCookie> </React.StrictMode> );
用户登录API函数
const userLogin = async (post) => { try { const response = await http.post("login", post); const data = response.data; return data; } catch (error) { return error.response; } };
登录表单提交mutation(使用React Query)
const { mutate, isPending } = useMutation({ mutationFn: userLogin, onSuccess: ({ data, success }) => { if (success) { setAuth(true, data); navigate("/"); } else { removeAuth(); } }, }); const onSubmit = (formData) => { mutate(formData); };
注:setAuth()函数用于将loggedIn设为true,并在本地存储保存部分用户基础信息。
问题解决方法
核心原因
问题本质是CSRF令牌与后端会话不同步:首次登录时,前端获取的CSRF令牌未绑定到新创建的登录会话,导致验证失败;第二次请求时,令牌已与会话关联,因此成功。切换用户时,旧会话的令牌残留,同样需要一次请求刷新令牌。
Laravel端修复
给登录/登出路由添加web中间件
Sanctum有状态认证依赖web中间件的会话管理,修改api路由:Route::middleware('web')->group(function () { Route::post('login', LoginController::class)->name('login'); Route::post('logout', function (Request $request) { Auth::guard('web')->logout(); // 清除会话和令牌,避免残留 $request->session()->invalidate(); $request->session()->regenerateToken(); return response()->json(['message' => 'Logged out']); }); });优化LoginController的用户更新逻辑
无需重新查询用户,直接使用当前认证用户:public function __invoke(LoginRequest $request) { $credentials = $request->validated(); if (Auth::attempt($credentials)) { $user = Auth::user(); if (!$user->active) { return alert_error(__('auth.not_activated'), 422); } // 直接更新当前认证用户 $user->logged_in_at = Carbon::now(); $user->save(); $auth = [ 'uuid' => $user->uuid, 'role' => $user->role_id, 'name' => $user->name, ]; return result_success(__('auth.auth_success'), $auth); } else { return alert_error(__('auth.auth_failed'), 422); } }配置Sanctum状态域
在config/sanctum.php中确保包含前端域名:'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( '%s%s', 'localhost,localhost:3000,localhost:5173,127.0.0.1,::1', env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' ))),环境变量
SANCTUM_STATEFUL_DOMAINS要添加你的前端端口(比如localhost:5173)。
React端修复
确保CSRF令牌加载完成后再渲染应用
修改HttpCsrfCookie组件,等待令牌获取完成:const HttpCsrfCookie = ({ children }) => { const [csrfLoaded, setCsrfLoaded] = useState(false); useEffect(() => { http .get(import.meta.env.VITE_CSRF_COOKIE_URL) .then(() => setCsrfLoaded(true)) .catch(err => console.error('CSRF获取失败:', err)); }, []); if (!csrfLoaded) { return <div>加载中...</div>; } return children; };登出时清除本地存储并刷新CSRF令牌
登出后重新获取令牌,避免切换用户时残留旧令牌:const userLogout = async () => { try { await http.post("logout"); removeAuth(); // 清除本地存储的用户信息 await http.get(import.meta.env.VITE_CSRF_COOKIE_URL); // 刷新令牌 } catch (err) { console.error('登出失败:', err); } };手动添加Axios拦截器确保XSRF令牌携带
确保请求头正确携带令牌:http.interceptors.request.use(config => { const xsrfToken = document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1]; if (xsrfToken) { config.headers['X-XSRF-TOKEN'] = decodeURIComponent(xsrfToken); } return config; });
内容的提问来源于stack exchange,提问作者Noor
相关产品推荐
相关产品推荐

