NextAuth v4中Token过期自动登出实现方案求助
NextAuth Credentials登录实现Token过期自动登出方案
我用Node.js后端实现NextAuth的Credentials登录,流程正常。设置Token过期时间15秒后,希望系统在Token过期时自动登出,哪怕页面没有任何API请求(比如无交互的仪表盘页面),同时要支持页面刷新场景。尝试设置
maxAge:15但未生效,相关代码如下:
1. NextAuth配置文件
import { NextAuthOptions } from "next-auth"; import GithubProvider from "next-auth/providers/github"; import CredentialProvider from "next-auth/providers/credentials"; import { signOut } from "next-auth/react"; export const authOptions: NextAuthOptions = { providers: [ CredentialProvider({ name: "credentials", credentials: {}, async authorize(credentials, req) { console.log("credentials", credentials); const response = await fetch("http://localhost:5000/api/v1/user/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(credentials), }).then((res) => res.json()); console.log("user", response); if (response?.status === 200) { return { ...response.data }; } else if (response?.status === 422 || response?.status === 401 || response?.status === 500) { throw new Error(response.message); } else { return null; } }, }), ], callbacks: { async jwt({ token, user }: { token: any, user: any }) { return { ...token, ...user }; }, async session({ session, token }: { session: any, token: any }) { session.user = token; return session; }, }, pages: { signIn: "/", //sigIn page, }, };
2. 登录表单组件(客户端)
"use client"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { zodResolver } from "@hookform/resolvers/zod"; import { signIn } from "next-auth/react"; import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Input } from "../ui/input"; import { Checkbox } from "../ui/checkbox"; // import GoogleSignInButton from "../github-auth-button"; const formSchema = z.object({ email: z.string().email({ message: "Enter a valid email address" }), password: z.string().min(6, { message: "Password must be at least 6 characters" }), }); type UserFormValue = z.infer<typeof formSchema>; export default function UserAuthForm() { const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl"); const [loading, setLoading] = useState(false); const defaultValues = { email: "", password: "", }; const form = useForm<UserFormValue>({ resolver: zodResolver(formSchema), defaultValues, }); const router = useRouter(); const onSubmit = async (data: UserFormValue) => { setLoading(true); try { const response = await signIn("credentials", { redirect: false, email: data.email, password: data.password, // callbackUrl: callbackUrl ?? "/dashboard", }); console.log("response", response); if (response?.ok) { router.push("/dashboard"); } else if (!response?.ok && response?.error === "fetch failed") { form.setError("email", { message: "Network error" }); form.setError("password", { message: "Network error" }); } else { form.setError("email", { message: response?.error ?? "Invalid credentials" }); } } catch (error) { console.log("Error", error); } finally { setLoading(false); } }; return ( <> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 w-full" > <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input type="email" placeholder="Enter your email..." disabled={loading} {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <Input type="password" placeholder="Enter your password..." disabled={loading} {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button disabled={loading} className="ml-auto w-full" type="submit"> Sign In {loading && "..."} </Button> </form> </Form> <div className="relative"> <div className="absolute inset-0 flex items-center"> <span className="w-full border-t" /> </div> <div className="relative flex justify-center text-xs uppercase"> <span className="bg-background px-2 text-muted-foreground"> Already have an account </span> <span onClick={() => router.push("/register")} className="bg-background px-2 text-primary cursor-pointer">Sign Up</span> </div> </div> {/* <GoogleSignInButton /> */} </> ); }
3. 尝试添加的session配置片段
session: { // maxAge: 15 seconds maxAge: 15, },
解决方案
步骤1:正确配置NextAuth的Session参数
首先确保session配置正确嵌套在authOptions对象中,同时指定JWT策略并同步过期时间,这是maxAge不生效的核心原因:
export const authOptions: NextAuthOptions = { providers: [/* 保持原有配置不变 */], callbacks: [/* 保持原有配置不变 */], pages: [/* 保持原有配置不变 */], // 正确添加session配置 session: { strategy: "jwt", // Credentials登录默认使用JWT,必须显式指定 maxAge: 15, // session过期时间(秒) updateAge: 0, // 禁止自动延长session有效期 }, jwt: { maxAge: 15, // JWT过期时间和session保持一致 }, };
步骤2:前端实现自动检测过期逻辑
在需要自动登出的页面(如Dashboard)添加客户端定时器,基于session的过期时间触发登出:
"use client"; import { useSession, signOut } from "next-auth/react"; import { useEffect, useState } from "react"; export default function Dashboard() { const { data: session } = useSession(); const [logoutTimer, setLogoutTimer] = useState<NodeJS.Timeout | null>(null); useEffect(() => { if (!session) return; // 计算剩余过期时间 const expiryTime = new Date(session.expires).getTime(); const now = Date.now(); const timeLeft = expiryTime - now; if (timeLeft <= 0) { // 已过期,直接登出 signOut({ redirect: true, callbackUrl: "/" }); return; } // 设置定时器,到期自动登出 const timer = setTimeout(() => { signOut({ redirect: true, callbackUrl: "/" }); }, timeLeft); setLogoutTimer(timer); // 组件卸载时清理定时器 return () => { if (timer) clearTimeout(timer); }; }, [session]); return ( <div className="p-6"> <h1>Dashboard</h1> <p>Welcome {session?.user?.email}</p> </div> ); }
步骤3:处理页面刷新场景
页面刷新时,通过useSession获取最新session状态,直接判断是否过期并触发登出,上述Dashboard组件的useEffect已包含该逻辑。也可以在根布局添加全局检测,覆盖所有认证页面:
"use client"; import { useSession, signOut } from "next-auth/react"; import { useEffect } from "react"; export default function RootLayout({ children }: { children: React.ReactNode }) { const { data: session } = useSession(); useEffect(() => { if (!session) return; const expiryTime = new Date(session.expires).getTime(); if (Date.now() >= expiryTime) { signOut({ redirect: true, callbackUrl: "/" }); } }, [session]); return ( <html lang="en"> <body>{children}</body> </html> ); }
关键说明
- Session策略:Credentials登录不支持数据库session,必须指定
strategy: "jwt"才能让maxAge生效。 - 时间同步:JWT和session的
maxAge必须保持一致,避免出现逻辑冲突。 - 定时器清理:组件卸载时必须清除定时器,防止内存泄漏。
内容的提问来源于stack exchange,提问作者Anuj Todankar
相关产品推荐
相关产品推荐

