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

Next.js App Router中NextAuth登录动态路由重定向异常求助

问题描述

使用Next.js App Router开发网站,集成NextAuth处理登录功能,用户创建和登录流程正常,但登录后重定向到动态路由/profile/[userName]时遇到以下问题:

  • 点击登录按钮后要么默认跳转到/profile页面
  • 使用router.push(/profile/[${userSlug}]);会跳转到/profile/%5B%5D(方括号被URL编码)
  • 尝试router.push(/profile/${String(userSlug)});无效,但直接写死路由router.push("/profile/john")可以正常跳转

相关代码

登录页代码

"use client";

import { useState } from "react";
import { useFormik } from "formik";
import Image from "next/image";
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { redirect } from "next/navigation";

export default function Login({ params }) {
  const session = useSession();
  const { status } = useSession();
  let slug;
  const isAuthenticated = status === "Authenticated";

  const userName = session.data?.user?.name;
  const userSlug =
    userName && userName.includes(" ")
      ? userName.split(" ")[0].toLowerCase()
      : "";

  if (isAuthenticated) {
    return redirect("/");
  }
  const router = useRouter();
  const [error, setError] = useState([]);
  console.log("SESSION: ", session);

  const handleSubmit = async () => {
    const result = await signIn("credentials", {
      email: formik.values.email,
      password: formik.values.password,
      redirect: false,
    });
    if (result.error == null) {
      console.log("fetch", slug);
      router.push(`/profile/${String(userSlug)}`);
    }
    setError(result.error);
  };

  const formik = useFormik({
    initialValues: {
      email: "",
      password: "",
    },
    onSubmit: handleSubmit,
  });

  return (
    <section className="mt-8">
      <h1 className="text-center text-4xl mb-4">Login</h1>
      <form
        className="flex flex-col max-w-xs mx-auto"
        onSubmit={formik.handleSubmit}
      >
        {error && <div className="text-red-500">{error}</div>}
        <input
          type="email"
          name="email"
          placeholder="email"
          value={formik.values.email}
          onChange={formik.handleChange}
        />
        <input
          type="password"
          name="password"
          placeholder="password"
          value={formik.values.password}
          onChange={formik.handleChange}
        />
        <button type="submit">Login</button>
        <div className="my-4 text-center text-gray-500">
          or login with provider
        </div>
        <button
          type="button"
          alt="google logo"
          onClick={() => signIn("google", { callbackUrl: "/" })}
          className="flex gap-4 justify-center"
        >
          <Image src="/google.png" alt="" width={24} height={24} />
          Login with google
        </button>
      </form>
    </section>
  );
}

Auth路由代码

import GoogleProvider from "next-auth/providers/google";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcrypt";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    CredentialsProvider({
      name: "credentials",
      credentials: {
        name: { label: "Name", type: "text" },
        email: {
          label: "Email",
          type: "email",
          placeholder: "test@example.com",
        },
      },
      async authorize(credentials, req) {
        if (
          !credentials.email ||
          !credentials.password ||
          credentials.email === "" ||
          credentials.password === ""
        ) {
          throw new Error("Email and password are required");
        }

        const user = await prisma.user.findUnique({
          where: {
            email: credentials.email,
          },
        });

        if (!user) {
          throw new Error("User not found");
        }

        const passwordMatch = await bcrypt.compare(
          credentials.password,
          user.password
        );

        if (!passwordMatch) {
          throw new Error("Incorrect password");
        }

        return user;
      },
    }),
  ],

  session: {
    strategy: "jwt",
  },
  secret: process.env.NEXTAUTH_SECRET,
  adapter: PrismaAdapter(prisma),
});

export { handler as GET, handler as POST };

原因分析

  1. Session未及时更新:调用signIn成功后,useSession的缓存不会立即刷新,此时session.data?.user?.name仍为空,导致userSlug是空字符串,最终router.push变成/profile/,被默认重定向到/profile。
  2. 动态路由结构错误:你在pages目录下创建[userName]文件夹,但App Router的路由文件必须放在app目录下,pages目录属于Pages Router的结构,会导致路由无法匹配。
  3. userSlug逻辑有漏洞:当用户名不含空格时,userSlug会被设置为空字符串,导致路由不完整。

修复方案

1. 调整动态路由目录结构

将pages/profile/[userName]/page.jsx移动到app/profile/[userName]/page.jsx,确保符合App Router的路由规则。

2. 登录成功后获取最新用户信息跳转

方案A:直接从signIn返回结果取用户信息

修改handleSubmit函数,使用signIn返回的result.user获取用户名,避免依赖未更新的session:

const handleSubmit = async () => {
  const result = await signIn("credentials", {
    email: formik.values.email,
    password: formik.values.password,
    redirect: false,
  });
  if (result.error == null) {
    const userName = result.user?.name;
    // 优化userSlug生成逻辑,处理无空格的情况
    const userSlug = userName 
      ? userName.includes(" ") 
        ? userName.split(" ")[0].toLowerCase() 
        : userName.toLowerCase() 
      : "default"; // 兜底默认值
    router.push(`/profile/${userSlug}`);
  }
  setError(result.error);
};

方案B:强制刷新session后跳转

如果需要依赖session数据,调用refetch方法刷新session后再跳转:

// 修改useSession的调用方式,获取refetch方法
const { data: session, refetch } = useSession();

const handleSubmit = async () => {
  const result = await signIn("credentials", {
    email: formik.values.email,
    password: formik.values.password,
    redirect: false,
  });
  if (result.error == null) {
    await refetch(); // 强制刷新session
    const userName = session?.user?.name;
    const userSlug = userName 
      ? userName.includes(" ") 
        ? userName.split(" ")[0].toLowerCase() 
        : userName.toLowerCase() 
      : "default";
    router.push(`/profile/${userSlug}`);
  }
  setError(result.error);
};

3. 优化CredentialsProvider的返回值

确保authorize函数返回的用户对象包含name字段,这样signIn的结果中才能正确获取用户名:

async authorize(credentials, req) {
  // ...现有验证逻辑
  // 明确返回需要的字段
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    // 其他必要字段
  };
}

4. 处理Google登录的重定向

对于Google登录,修改为动态跳转逻辑:

<button
  type="button"
  alt="google logo"
  onClick={async () => {
    const session = await signIn("google", { redirect: false });
    const userName = session.user?.name;
    const userSlug = userName 
      ? userName.includes(" ") 
        ? userName.split(" ")[0].toLowerCase() 
        : userName.toLowerCase() 
      : "default";
    router.push(`/profile/${userSlug}`);
  }}
  className="flex gap-4 justify-center"
>
  <Image src="/google.png" alt="" width={24} height={24} />
  Login with google
</button>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 19:45:56