Next.js中间件仅在客户端首次导航请求生效问题排查
Next.js 14 中间件客户端导航不触发校验的问题解决
核心结论
这种行为是Next.js的预期表现。中间件仅在服务器接收到完整请求时执行(比如初始页面加载、硬刷新、API请求),而客户端路由导航(router.push、Link组件跳转)是SPA式的客户端页面切换,不会向服务器发起新的页面请求,因此不会触发中间件。
原因解析
Next.js的App Router采用客户端路由优化,当你使用router.push或Link跳转时,页面组件会在客户端直接切换,无需重新向服务器请求整个页面资源,中间件作为服务器端逻辑自然不会被触发。
解决方案
要确保访问/user/profile时的身份校验,推荐结合客户端Session校验(利用NextAuth的useSession)和服务器端中间件的双重校验,覆盖所有场景:
1. 优化客户端校验(推荐)
在/user/profile/page.js中,使用useSession的required选项简化校验逻辑,确保未认证用户被立即重定向:
'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() { // 直接配置required和未认证回调 const { data: session, status } = useSession({ required: true, onUnauthenticated() { router.push('/user/signIn') } }) const [userData, setUserData] = useState(null) const router = useRouter() 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') }) } }, [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> </> ) ) }
2. 强制触发服务器端校验(不推荐,会失去SPA流畅性)
如果必须让中间件在每次导航时都执行,可以放弃客户端路由,改用传统的页面跳转:
// 替换router.push('/user/profile')为: window.location.href = '/user/profile'
这种方式会发起完整的服务器请求,触发中间件,但会导致页面刷新,牺牲SPA的流畅体验。
3. 保留现有中间件配置
你的middleware.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/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> </> ) }
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: 'john@doe.com' }, 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 }
内容的提问来源于stack exchange,提问作者tomdythai
相关产品推荐
相关产品推荐

