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

Next.js Server Action对接Laravel Breeze API认证失败求助

问题排查:Next.js服务端认证调用Laravel /api/user返回"Unauthenticated"

我需要将Next.js应用作为SPA与Laravel API集成,现有breeze-next库可实现该集成,但它的认证操作是客户端的。我要实现服务端认证,编写了如下代码,但调用/api/user接口时返回错误信息User Data { message: 'Unauthenticated.' },恳请协助排查。

服务端认证代码("use server")

"use server";

import { cookies } from "next/headers";
import { z } from "zod";
import { redirect } from "next/navigation";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

const getCookieValue = (name: string, cookies: string[]): string | null => {
  const cookieString = cookies.find((cookie) => cookie.startsWith(name + "="));
  if (cookieString) {
    const value = cookieString.split(";")[0].split("=")[1];
    return decodeURIComponent(value);
  }
  return null;
};

export async function getCsrfToken() {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_BACKEND_URL}/sanctum/csrf-cookie`,
    {
      method: "GET",
      credentials: "include",
    }
  );

  const cookieHeader = response.headers.get("set-cookie");

  const setCookies = cookieHeader?.split(", ");

  if (setCookies) {
    const xsrfToken = getCookieValue("XSRF-TOKEN", setCookies);
    const sessionKey = getCookieValue("laravel_session", setCookies);

    if (xsrfToken && sessionKey) {
      cookies().set("XSRF-TOKEN", xsrfToken, {
        httpOnly: true,
      });
      cookies().set("laravel_session", sessionKey, {
        httpOnly: true,
      });

      return { xsrfToken, sessionKey };
    }

    return { xsrfToken: null, sessionKey: null };
  }
}

export async function authenticate(_currentState: unknown, data: FormData) {
  const formData = Object.fromEntries(data);
  const parsed = schema.safeParse(formData);

  const { xsrfToken, sessionKey } = (await getCsrfToken()) as {
    xsrfToken: string | null;
    sessionKey: string | null;
  };

  const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/login`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json",
      "X-XSRF-TOKEN": xsrfToken,
      Cookie: `XSRF-TOKEN=${xsrfToken};laravel_session=${sessionKey}`,
    } as HeadersInit,
    credentials: "include",
    body: JSON.stringify(parsed.data),
  });

  const cookieHeader = response.headers.get("set-cookie");

  const setCookies = cookieHeader?.split(", ");

  if (setCookies) {
    const xsrfToken = getCookieValue("XSRF-TOKEN", setCookies);
    const sessionKey = getCookieValue("laravel_session", setCookies);

    if (xsrfToken && sessionKey) {
      cookies().set("XSRF-TOKEN", xsrfToken, {
        httpOnly: true,
      });
      cookies().set("laravel_session", sessionKey, {
        httpOnly: true,
      });

      const response = await fetch(
        `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/user`,
        {
          method: "GET",
          headers: {
            "Content-Type": "application/json",
            Accept: "application/json",
            "X-XSRF-TOKEN": xsrfToken,
            Cookie: `XSRF-TOKEN=${xsrfToken};laravel_session=${sessionKey}`,
          } as HeadersInit,
          credentials: "include",
        }
      );

      const data = await response.json();

      console.log("User Data", data);
    }
  }

  if (!response.ok) {
    return { message: "Login failed!" };
  }

  redirect("/dashboard");
}

客户端登录页面代码("use client")

"use client";

import { authenticate } from "@/app/actions";
import { useFormState, useFormStatus } from "react-dom";

export default function Page() {
  const [state, dispatch] = useFormState(authenticate, {
    message: "",
  });

  return (
    <form action={dispatch}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <div>{state && <p>{state.message}</p>}</div>
      <LoginButton />
    </form>
  );
}

function LoginButton() {
  const { pending } = useFormStatus();

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (pending) {
      e.preventDefault();
    }
  };

  return (
    <button disabled={pending} type="submit" onClick={handleClick}>
      {pending ? "Loading..." : "Login"}
    </button>
  );
}

核心问题排查与修复方案

1. Cookie的HttpOnly配置错误

Laravel的XSRF-TOKEN设计为非HttpOnly(前端需要读取该值设置请求头),而你代码中把XSRF-TOKEN设置为httpOnly: true,这会导致后续请求的XSRF验证失败。

修复代码:在getCsrfToken和authenticate函数中修改Cookie配置:

// 设置XSRF-TOKEN时改为非HttpOnly
cookies().set("XSRF-TOKEN", xsrfToken, {
  httpOnly: false,
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
});
// laravel_session保持HttpOnly不变
cookies().set("laravel_session", sessionKey, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
});

2. Laravel端配置检查

  • 确保.env中SANCTUM_STATEFUL_DOMAINS包含Next.js的域名(例如localhost:3000)
  • 检查config/cors.php中supports_credentials设置为true
  • 确认routes/api.php中/api/user路由使用auth:sanctum中间件

3. 登录请求的参数验证

在authenticate函数中,需提前校验表单参数的合法性,避免无效请求:

const parsed = schema.safeParse(formData);
if (!parsed.success) {
  return { message: "邮箱或密码格式错误!" };
}

4. 简化Cookie传递逻辑

服务端fetch请求中,无需手动拼接Cookie头,credentials: "include"会自动携带当前存储的Cookie,确保使用登录后返回的最新会话Cookie。


内容的提问来源于stack exchange,提问作者Ubeydullah Yılmaz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 09:58:16