如何在NextJS中实现JWT验证及过期导航自动跳转?
问题原因分析
Next.js 的 Link 组件采用客户端导航(SPA 式路由切换),这种导航方式不会向服务器发送新请求,仅在浏览器端更新页面内容。而你的 Middleware 运行在服务器端,只有当请求到达服务器时才会执行(比如刷新页面、直接输入 URL、外部链接跳转),因此通过 Link 组件导航时,Middleware 不会被触发,自然无法执行过期跳转逻辑。
解决方案
需要在客户端层面添加路由守卫,覆盖客户端导航场景,同时保留原 Middleware 处理服务器端请求的逻辑,实现全场景的过期跳转。
方案1:客户端直接检查JWT(非HttpOnly Cookie场景)
如果你的 JWT 不是 HttpOnly 的 Cookie,可直接在客户端解析并检查过期时间:
1. 编写JWT过期检查函数
// utils/auth.ts export const isJwtExpired = (token: string | undefined): boolean => { if (!token) return true; try { // 解析JWT payload部分 const payload = JSON.parse(atob(token.split('.')[1])); // 比较过期时间与当前时间(exp是秒级,转成毫秒) return payload.exp * 1000 < Date.now(); } catch (err) { // 解析失败视为过期 return true; } };
2. 添加全局路由守卫组件
创建客户端组件,监听路由变化并检查权限:
// components/AuthGuard.tsx 'use client'; import { useEffect } from 'react'; import { useRouter, usePathname } from 'next/navigation'; import { isJwtExpired } from '@/utils/auth'; export default function AuthGuard() { const router = useRouter(); const pathname = usePathname(); useEffect(() => { // 仅在admin路由范围内检查 if (pathname.startsWith('/admin')) { // 从Cookie中获取JWT const jwtToken = document.cookie .split('; ') .find(row => row.startsWith('jwt=')) ?.split('=')[1]; if (isJwtExpired(jwtToken)) { // 清除过期Cookie并跳转登录页 document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; router.push('/login'); } } }, [pathname, router]); return null; }
3. 在Admin布局中引入守卫
将守卫组件添加到 Admin 全局布局,确保所有 admin 路由都会触发检查:
// app/admin/layout.tsx import AuthGuard from '@/components/AuthGuard'; export default function AdminLayout({ children }: { children: React.ReactNode }) { return ( <> <AuthGuard /> {/* 你的Admin布局内容 */} <div className="admin-layout"> {children} </div> </> ); }
方案2:通过API检查JWT(HttpOnly Cookie场景)
如果你的 JWT 是 HttpOnly 的 Cookie(更安全的做法),客户端无法直接读取,需通过后端 API 验证:
1. 创建权限检查API
// app/api/check-auth/route.ts import { NextRequest, NextResponse } from 'next/server'; import jwt from 'jsonwebtoken'; export async function GET(request: NextRequest) { // 从HttpOnly Cookie中获取JWT const jwtToken = request.cookies.get('jwt')?.value; if (!jwtToken) { return NextResponse.json({ authenticated: false }, { status: 401 }); } try { // 验证JWT有效性 jwt.verify(jwtToken, process.env.JWT_SECRET!); return NextResponse.json({ authenticated: true }, { status: 200 }); } catch (err) { // 验证失败(过期/无效) return NextResponse.json({ authenticated: false }, { status: 401 }); } }
2. 修改路由守卫的检查逻辑
// components/AuthGuard.tsx 'use client'; import { useEffect } from 'react'; import { useRouter, usePathname } from 'next/navigation'; export default function AuthGuard() { const router = useRouter(); const pathname = usePathname(); useEffect(() => { const checkAuthStatus = async () => { if (pathname.startsWith('/admin')) { try { const res = await fetch('/api/check-auth', { credentials: 'include' }); if (!res.ok) { // 权限验证失败,跳转登录页 router.push('/login'); } } catch (err) { // 请求失败也视为无权限 router.push('/login'); } } }; checkAuthStatus(); }, [pathname, router]); return null; }
不推荐方案:强制服务器端导航
如果你愿意牺牲客户端导航的流畅性,可以用普通的 <a> 标签代替 Link 组件,这样每次点击都会发送服务器请求触发 Middleware。但这种方式会失去 SPA 路由的优势,不建议在后台管理系统中使用。
内容的提问来源于stack exchange,提问作者javadeveloper2
相关产品推荐
相关产品推荐

