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
相关产品推荐
相关产品推荐

