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

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端修复

  1. 给登录/登出路由添加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']);
        });
    });
    
  2. 优化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);
        }
    }
    
  3. 配置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端修复

  1. 确保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;
    };
    
  2. 登出时清除本地存储并刷新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);
      }
    };
    
  3. 手动添加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 14:35:57