Next.js 中的中间件仅适用于来自客户端导航的第一个请求

问题描述 投票:0回答:1

我正在使用 Next.js 并面临一个问题,即客户端路由的后续导航不会触发中间件。具体来说,我在 React 组件中使用 router.push 和 use client,但中间件似乎只对第一个请求执行检查,并且不会重新评估后续导航。 (NextJS 14)

enter image description here

问题: 我有以下中间件配置:

//app/user/signIn/page.js

'use client'

import { useState } from 'react'
import { signIn } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Link from 'next/link';

export default function SignIn() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState(null)
  const router = useRouter()

  const handleSubmit = async (e) => {
    e.preventDefault()
    try {
      const result = await signIn('credentials', {
        redirect: false,
        email,
        password,
      })

      if (result.error) {
        setError('email or password is incorrect.')
      } else {
        router.push('/user/profile')
      }
    } catch (error) {
      console.log('error', error)
      setError('An unexpected error occurred. Please try again later.')
    }
  }

  return (
    <>
      <div className='text-center'>
        <Link href="/user/profile" className="block mb-2">[Profile Menu]</Link>
        <Link href="/user/signIn" className="block mb-2">[Login Menu]</Link>
      </div>
      <div className="flex items-center justify-center">
        <form
          onSubmit={handleSubmit}
          className="bg-white p-6 rounded-md shadow-md"
        >
          <div className="mb-4">
            <label htmlFor="email">Email</label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="w-full border border-gray-300 px-3 py-2 rounded"
            />
          </div>
          <div className="mb-4">
            <label htmlFor="password">Password</label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="w-full border border-gray-300 px-3 py-2 rounded"
            />
          </div>
          {error && <p className="text-red-500 mb-4">{error}</p>}
          <button
            type="submit"
            className="w-full bg-blue-500 text-white py-2 rounded mb-4"
          >
            Sign In
          </button>
        </form>
      </div>
    </>
  )
}

//应用程序/用户/个人资料/page.js

'use client'

import { useSession, signOut } from 'next-auth/react'
import Swal from 'sweetalert2'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'

export default function Profile() {
    const { data: session, status } = useSession()
    const [userData, setUserData] = useState(null)
    const router = useRouter()

    useEffect(() => {
        if (status === 'unauthenticated') {
            router.push('/user/signIn')
        }
    }, [status, router])

    useEffect(() => {
        if (session?.user?.id) {
            fetch(`/api/user/${session.user.id}`)
                .then(res => res.json())
                .then(data => setUserData(data))
                .catch(error => {
                    console.error('Error fetching user data:', error)
                    Swal.fire('Error', 'Unable to fetch user data', 'error')
                })
        }
    }, [status, session])

    const handleSignOut = async () => {
        const result = await Swal.fire({
            title: 'Do you want to logout?',
            text: "Confirm logout.",
            icon: 'warning',
            showCancelButton: true,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'OK',
            cancelButtonText: 'Cancel'
        })

        if (result.isConfirmed) {
            signOut({ callbackUrl: '/user/signIn' })
        }
    }

    return (
        status === 'authenticated' &&
        session.user && (
            <>
                <div className="flex h-screen items-center justify-center">
                    <div className="bg-white p-6 rounded-md shadow-md">
                        <p>
                            Welcome, <b>{userData?.firstName} {userData?.lastName}!</b>
                        </p>
                        <p>Email: {userData?.email}</p>
                        <p>Role: {userData?.role.title}</p>
                        <button
                            onClick={handleSignOut}
                            className="w-full bg-blue-500 text-white py-2 rounded"
                        >
                            Logout
                        </button>
                    </div>
                </div>
            </>
        )
    )
}

//中间件.js

import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';

export async function middleware(request) {
  const user = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  if (request.nextUrl.pathname.startsWith('/user/profile') && !user) {
    const signInUrl = new URL('/user/signIn', request.url);
    return NextResponse.redirect(signInUrl);
  }

  return NextResponse.next();
}

//app/api/auth/[...nextauth]/route.js

import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaClient } from '@prisma/client'
import bcrypt from 'bcrypt'
import { PrismaAdapter } from '@auth/prisma-adapter'

const prisma = new PrismaClient()

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: 'Email', type: 'email', placeholder: '[email protected]' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials, req) {
        if (!credentials) return null
        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
        })

        if (
          user &&
          (await bcrypt.compare(credentials.password, user.password))
        ) {
          return {
            id: user.id,
            firstName: user.firstName,
            lastName: user.lastName,
            email: user.email,
            roleId: user.roleId
          }
        } else {
          throw new Error('Invalid email or password')
        }
      },
    })
  ],
  adapter: PrismaAdapter(prisma),
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    jwt: async ({ token, user }) => {
      if (user) {
        token.id = user.id
        token.roleId = user.roleId
      }
      return token
    },
    session: async ({ session, token }) => {
      if (session.user) {
        session.user.id = token.id
        session.user.roleId = token.roleId
      }
      return session
    }
  },
}

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

问题:

  • 当我登录后导航到 /user/profile 时,如果没有令牌,中间件会按预期执行重定向。
  • 但是,当我使用 router.push 从客户端再次导航到 /user/profile 时,中间件不会再次重新检查或执行重定向。

问题:

Next.js 中间件是否会出现这种行为?为什么中间件仅在第一个请求时触发,而不在客户端的后续导航时触发?如何确保中间件运行并检查每个请求,甚至是由客户端组件中的 router.push 触发的请求?

附加信息:

  • 我使用 Next.js 和 NextAuth 进行身份验证。
  • 我的目标是确保每次用户尝试访问 /user/profile 时,中间件都会检查令牌,如果令牌丢失,则重定向到登录页面。
authentication middleware next-auth response.redirect nextjs14
1个回答
0
投票

我认为问题出在下一个中间件缓存上。上次登录后有token就不会再检查了,所以需要加检查

response.headers.set("x-middleware-cache", "no-cache");

所以你的中间件需要这样改变。

//中间件.js

import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';

export async function middleware(request) {
  const user = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET,
  });

  if (request.nextUrl.pathname.startsWith('/user/profile') && !user) {
    const response = NextResponse.redirect(new URL("/user/signIn", request.url));
    response.headers.set("x-middleware-cache", "no-cache");
    return response;
  }

  const response = NextResponse.next();
  response.headers.set("x-middleware-cache", "no-cache");
  return response;
}
最新问题
© www.soinside.com 2019 - 2025. All rights reserved.