我正在使用 Next.js 并面临一个问题,即客户端路由的后续导航不会触发中间件。具体来说,我在 React 组件中使用 router.push 和 use client,但中间件似乎只对第一个请求执行检查,并且不会重新评估后续导航。 (NextJS 14)
问题: 我有以下中间件配置:
//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 }
问题:
问题:
Next.js 中间件是否会出现这种行为?为什么中间件仅在第一个请求时触发,而不在客户端的后续导航时触发?如何确保中间件运行并检查每个请求,甚至是由客户端组件中的 router.push 触发的请求?
附加信息:
我认为问题出在下一个中间件缓存上。上次登录后有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;
}