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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 07:28:10