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

Next14+Auth.js v5客户端组件useSession无法获取会话问题

Next14 App Router 中 Auth.js@beta 客户端组件无法获取会话

我在基于App Router的Next14项目中配置Auth.js@beta对接自定义后端,服务端组件调用auth()方法可以正常获取会话数据,但客户端组件使用useSession钩子时,返回结果始终是{data: null, status: 'unauthenticated', update: ƒ}。

由于后端返回的用户数据不是Auth.js预设的格式(如email、name),我通过session回调从token中提取数据并赋值给session.user。已经配置了SessionProvider,但问题依然存在。

以下是相关代码:

/auth/auth.config.ts

import type { NextAuthConfig } from "next-auth";

const isTokenExpired = (token: string): Boolean => {
  const base64Url = token.split(".")[1];
  const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
  const decodedPayload = JSON.parse(Buffer.from(base64, "base64").toString("binary"));
  const tokenExpirationDate = new Date(decodedPayload.exp * 1000).getTime();
  const now = Date.now();
  return now >= tokenExpirationDate;
};

export const authConfig = {
  pages: {
    signIn: "/login",
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;

      if (nextUrl.pathname.startsWith("/login") && isLoggedIn) {
        // 未生效:已登录用户访问登录页时重定向到首页
        return Response.redirect(new URL("/", nextUrl));
      } else if (isLoggedIn) {
        // 检查用户是否在根路径或子路径下("/", "/users", "/dashboard"等)
        const isOnRootOrAbove = nextUrl.pathname === "/" || nextUrl.pathname.startsWith("/");
        if (isOnRootOrAbove) {
          return true; // 已登录用户允许访问根路径及子路径
        }
      }

      return false; // 默认拒绝访问
    },
    async jwt({ token, user, account, profile, isNewUser }) {
      if (user) {
        return { ...token, ...user };
      }

      if (!!token.accessToken && isTokenExpired(token.accessToken)) {
        console.log("token expired!");
        return null;
      }
      return token;
    },
    async session({ session, token, user }) {
      if (token.user && token.accessToken) {
        //@ts-ignore
        session.user = token.user;
        session.accessToken = token.accessToken;
      }
      return session;
    },
  },
  providers: [],
} satisfies NextAuthConfig;

/auth/auth.ts

import Credentials from "next-auth/providers/credentials";
import NextAuth, { AuthError } from "next-auth";
import { authConfig } from "./auth.config";

const credentialsConfig = Credentials({
  async authorize(credentials) {
    const { username, password } = credentials;

    const headers = new Headers();
    headers.append("Content-Type", "application/json");
    const body = JSON.stringify({ username, password });
    try {
      const res = await fetch("http://localhost/api/auth/login", {
        method: "POST",
        headers,
        body,
      });
      const user = await res.json();
      if (user?.status === 0) {
        return null;
      }

      return user;
    } catch (err) {
      throw new AuthError();
    }
  },
});

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  session: { strategy: "jwt" },
  jwt: { maxAge: 60 * 15 },
  providers: [credentialsConfig],
});

/src/app/providers.tsx

"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
const Providers = ({ children }: { children: ReactNode }) => {
  return <SessionProvider>{children}</SessionProvider>;
};

export default Providers;

/src/app/layout.tsx

import "@ui/globals.css";
import "@radix-ui/themes/styles.css";
import type { Metadata } from "next";
import { Theme, ThemePanel } from "@radix-ui/themes";
import { LayoutContextProvider, Layout, Navbar } from "@components";
import { DefaultSidebar } from "@layout";
import StoreProvider from "@redux/StoreProvider";
import Providers from "./providers";

export const metadata: Metadata = {
  title: "title",
  description: "description",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <Providers>
      <html lang="es" suppressHydrationWarning>
        <body>
          <Theme appearance="dark" accentColor="red" panelBackground="translucent">
            <StoreProvider>
              <LayoutContextProvider>{children}</LayoutContextProvider>
            </StoreProvider>
            {/* <ThemePanel /> */}
          </Theme>
        </body>
      </html>
    </Providers>
  );
}

/src/page/(home)/prueba/page.tsx

"use client";
import { Header } from "@components";
import { useSession } from "next-auth/react";

export default function Prueba() {
  const session = useSession();
  console.log(session);
  return <Header>Prueba</Header>;
}

/src/app/lib/actions.ts

"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { auth, signIn, signOut } from "@auth/auth";
import { AuthError } from "next-auth";

const SignInFormSchema = z.object({
  username: z.string().min(4, {
    message: "Username must be at least 4 characters long.",
  }),
  password: z.string().min(1, {
    message: "Password must not be empty",
  }),
});

export type State = {
  errors?: {
    username?: string[];
    password?: string[];
    auth?: string[];
  };
  message?: string | null;
};

export async function authenticate(
  prevState: State,
  formData: FormData
): Promise<State | undefined> {
  const validatedFields = SignInFormSchema.safeParse({
    username: formData.get("username"),
    password: formData.get("password"),
  });
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: "Campos incompletos. no puede inicial sesion",
    };
  }
  try {
    await signIn("credentials", formData);
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return { message: "Invalid credentials." };
        default:
          return {
            message: "Something went wrong.",
          };
      }
    }
    revalidatePath("/");
    redirect("/");
  }
}

export async function logout(): Promise<void> {
  await signOut();
}
解决方案

1. 修正SessionProvider的包裹范围

Next.js App Router中,html和body是根级元素,不能被客户端组件直接包裹。调整layout.tsx,将SessionProvider移到body内部:

// /src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="es" suppressHydrationWarning>
      <body>
        <Providers>
          <Theme appearance="dark" accentColor="red" panelBackground="translucent">
            <StoreProvider>
              <LayoutContextProvider>{children}</LayoutContextProvider>
            </StoreProvider>
            {/* <ThemePanel /> */}
          </Theme>
        </Providers>
      </body>
    </html>
  );
}

2. 优化jwt回调的赋值逻辑

直接展开user可能会覆盖token原有属性,需明确赋值用户数据和token:

// /auth/auth.config.ts
async jwt({ token, user, account, profile, isNewUser }) {
  if (user) {
    // 兼容后端返回结构,明确赋值用户信息和accessToken
    return { 
      ...token, 
      user: user.user || user,
      accessToken: user.accessToken 
    };
  }

  if (!!token.accessToken && isTokenExpired(token.accessToken)) {
    console.log("token expired!");
    return null;
  }
  return token;
},

3. 修复session回调的类型问题

移除@ts-ignore,添加自定义类型声明确保会话数据正确传递:

// /auth/auth.config.ts
// 自定义会话和JWT类型
declare module "next-auth" {
  interface Session {
    user: Record<string, any>; // 根据后端返回结构定义具体类型
    accessToken: string;
  }
  
  interface JWT {
    user: Record<string, any>;
    accessToken: string;
  }
}

async session({ session, token, user }) {
  session.user = token.user;
  session.accessToken = token.accessToken;
  return session;
},

4. 检查Auth.js路由配置

App Router必须存在/app/api/auth/[...nextauth]/route.ts文件,用于处理Auth.js的API请求:

// /app/api/auth/[...nextauth]/route.ts
import { auth } from "@auth/auth";
export const { GET, POST } = auth;

5. 配置useSession强制加载会话

在客户端组件中添加required: true,强制等待会话加载完成:

// /src/page/(home)/prueba/page.tsx
const session = useSession({ required: true });

6. 验证Cookies跨域配置

如果前后端不在同一域名,需配置trustHost和Cookies属性:

// /auth/auth.ts
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  session: { strategy: "jwt" },
  jwt: { maxAge: 60 * 15 },
  providers: [credentialsConfig],
  trustHost: true,
  cookies: {
    sessionToken: {
      name: `__Secure-next-auth.session-token`,
      options: {
        httpOnly: true,
        sameSite: "lax",
        path: "/",
        secure: process.env.NODE_ENV === "production",
      },
    },
  },
});

内容的提问来源于stack exchange,提问作者Eduardo Serrano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 02:27:06