Next路由权限控制
在 Next.js 中实现路由的动态渲染,并根据后端设置的当前用户权限来控制哪些路由可以渲染,类似于 Vue Router 的权限路由管理,是完全可行的。Next.js 虽然不像 Vue Router 那样有内置的路由守卫(Route Guard),但可以通过以下几种方式结合其特性(如中间件、动态路由、API 路由等)实现类似的功能。
下面我将详细介绍如何在 Next.js 中实现基于用户权限的路由动态渲染,并提供代码示例。
实现思路
- 用户权限获取:从后端获取当前用户的角色或权限列表(例如通过 API 调用或 cookie/token 解析)。
- 路由权限控制:
- 使用 Next.js 的 中间件 (Middleware) 在请求到达页面前进行权限校验,决定是否允许访问某些路由。
- 或者在页面级别(如
getServerSideProps
或getStaticProps
)根据权限动态决定是否渲染页面。
- 动态路由渲染:根据用户权限动态过滤或生成可访问的路由列表,并在前端导航(如菜单或链接)中只显示用户有权限访问的路由。
- 重定向或错误处理:对于无权限访问的路由,重定向到登录页或显示无权限提示页面。
方法一:使用中间件 (Middleware) 进行全局路由权限控制
Next.js 的中间件(middleware.js
或 middleware.ts
)运行在请求到达页面之前,非常适合用来实现全局路由守卫,检查用户权限并决定是否允许访问某些路由。
实现步骤
-
创建中间件文件:
在项目根目录下创建middleware.ts
(或middleware.js
)。1import { NextResponse, NextRequest } from 'next/server'; 2 3// 定义需要权限控制的路由及其对应角色要求 4const protectedRoutes = { 5 '/dashboard': ['admin', 'editor'], // 只有 admin 和 editor 角色可以访问 6 '/profile': ['user', 'admin', 'editor'], // user、admin、editor 都可以访问 7}; 8 9export async function middleware(request: NextRequest) { 10 const pathname = request.nextUrl.pathname; 11 const authToken = request.cookies.get('auth_token')?.value; 12 13 // 如果没有 token,说明未登录,重定向到登录页面 14 if (!authToken) { 15 if (pathname !== '/login') { 16 return NextResponse.redirect(new URL('/login', request.url)); 17 } 18 return NextResponse.next(); 19 } 20 21 // 模拟从 token 或后端 API 获取用户角色(这里假设从 cookie 或 API 获取) 22 let userRole = 'user'; // 假设默认角色为 user 23 try { 24 // 这里可以调用 API 验证 token 并获取用户角色 25 // 例如:const response = await fetch('/api/auth/validate', { headers: { Authorization: `Bearer ${authToken}` } }); 26 // const data = await response.json(); 27 // userRole = data.role; 28 } catch (error) { 29 console.error('Failed to validate token:', error); 30 return NextResponse.redirect(new URL('/login', request.url)); 31 } 32 33 // 检查当前路由是否需要权限控制 34 const routeRoles = protectedRoutes[pathname]; 35 if (routeRoles && !routeRoles.includes(userRole)) { 36 // 无权限访问,重定向到无权限页面或首页 37 return NextResponse.redirect(new URL('/unauthorized', request.url)); 38 } 39 40 // 有权限,继续请求 41 return NextResponse.next(); 42} 43 44// 定义中间件匹配的路径 45export const config = { 46 matcher: ['/dashboard', '/profile', '/login'], 47};
-
创建无权限页面:
在app/unauthorized/page.tsx
中创建一个页面,用于显示无权限提示。1"use client"; 2 3import React from 'react'; 4import Link from 'next/link'; 5 6const Unauthorized = () => { 7 return ( 8 <div className="flex flex-col items-center justify-center h-screen"> 9 <h1 className="text-4xl font-bold text-red-600">Access Denied</h1> 10 <p className="text-lg mt-4">You do not have permission to access this page.</p> 11 <Link href="/" className="mt-6 text-blue-500 hover:underline"> 12 Back to Home 13 </Link> 14 </div> 15 ); 16}; 17 18export default Unauthorized;
-
动态导航菜单:
在前端导航组件中,根据用户角色动态渲染可访问的路由链接。1"use client"; 2 3import React, { useEffect, useState } from 'react'; 4import Link from 'next/link'; 5 6const Navbar = () => { 7 const [userRole, setUserRole] = useState<string | null>(null); 8 9 useEffect(() => { 10 // 假设从 cookie 或 API 获取用户角色 11 const fetchRole = async () => { 12 // 这里可以调用 API 获取角色 13 setUserRole('admin'); // 模拟角色为 admin 14 }; 15 fetchRole(); 16 }, []); 17 18 // 定义路由及其对应的角色权限 19 const routes = [ 20 { path: '/', label: 'Home', roles: ['user', 'admin', 'editor'] }, 21 { path: '/dashboard', label: 'Dashboard', roles: ['admin', 'editor'] }, 22 { path: '/profile', label: 'Profile', roles: ['user', 'admin', 'editor'] }, 23 ]; 24 25 return ( 26 <nav className="bg-gray-800 text-white p-4"> 27 <ul className="flex space-x-4"> 28 {routes 29 .filter(route => userRole && route.roles.includes(userRole)) 30 .map(route => ( 31 <li key={route.path}> 32 <Link href={route.path} className="hover:underline"> 33 {route.label} 34 </Link> 35 </li> 36 ))} 37 </ul> 38 </nav> 39 ); 40}; 41 42export default Navbar;
优点与缺点
- 优点:中间件在请求到达页面之前运行,性能高效,适用于全局路由权限控制;可以轻松扩展到复杂的权限逻辑。
- 缺点:中间件无法直接访问前端状态(如 React Context),需要通过 API 或 cookie 获取用户权限信息;对静态页面(SSG)可能需要额外处理。
方法二:使用 getServerSideProps
或 getStaticProps
进行页面级权限控制
如果您不希望使用中间件,或者只对部分页面进行权限控制,可以在页面级别使用 getServerSideProps
或 getStaticProps
结合 getSession
等方式检查权限,并决定是否渲染页面。
实现步骤
-
在页面中添加权限校验:
在需要保护的页面中,使用getServerSideProps
检查用户权限。1import { GetServerSideProps } from 'next'; 2import React from 'react'; 3 4const Dashboard = () => { 5 return ( 6 <div> 7 <h1>Dashboard</h1> 8 <p>Welcome to the dashboard. Only authorized users can see this.</p> 9 </div> 10 ); 11}; 12 13export const getServerSideProps: GetServerSideProps = async (context) => { 14 const { req } = context; 15 const cookieString = req.headers.cookie || ''; 16 17 // 解析 cookie 获取 auth_token 18 const getCookie = (name: string) => { 19 if (!cookieString) return null; 20 const cookies = cookieString.split(';'); 21 for (let cookie of cookies) { 22 cookie = cookie.trim(); 23 if (cookie.startsWith(name + '=')) { 24 return cookie.split('=')[1]; 25 } 26 } 27 return null; 28 }; 29 const authToken = getCookie('auth_token'); 30 31 if (!authToken) { 32 return { 33 redirect: { 34 destination: '/login', 35 permanent: false, 36 }, 37 }; 38 } 39 40 // 模拟验证 token 和角色(可以调用 API) 41 const userRole = 'admin'; // 假设从 API 获取 42 const allowedRoles = ['admin', 'editor']; 43 44 if (!allowedRoles.includes(userRole)) { 45 return { 46 redirect: { 47 destination: '/unauthorized', 48 permanent: false, 49 }, 50 }; 51 } 52 53 return { 54 props: {}, // 可以将用户数据传递给页面 55 }; 56}; 57 58export default Dashboard;
-
动态导航菜单:
与方法一相同,根据用户角色动态渲染导航菜单。
优点与缺点
- 优点:适合对特定页面进行权限控制,可以直接在页面级别获取用户数据并传递给组件;易于与 SSR 结合。
- 缺点:每个页面都需要单独编写权限逻辑,代码重复性较高;不适合全局路由控制(需要重复代码)。
方法三:结合 React Context 或状态管理库实现前端动态路由
如果权限数据已经在前端状态(如 Context、Redux、Zustand)中,可以在前端通过条件渲染动态显示路由或页面内容。
实现步骤
-
使用 Context 存储用户权限:
参考之前提供的 Context 示例,将用户角色或权限存储在全局状态中。1"use client"; 2 3import React, { createContext, useContext, useState, useEffect } from 'react'; 4 5interface AppContextType { 6 isLoggedIn: boolean; 7 userRole: string | null; 8 setLoggedIn: (isLoggedIn: boolean) => void; 9 setUserRole: (role: string | null) => void; 10} 11 12const AppContext = createContext<AppContextType | undefined>(undefined); 13 14export const useAppContext = () => { 15 const context = useContext(AppContext); 16 if (context === undefined) { 17 throw new Error('useAppContext must be used within an AppProvider'); 18 } 19 return context; 20}; 21 22export const AppProvider = ({ children }: { children: React.ReactNode }) => { 23 const [isLoggedIn, setLoggedIn] = useState(false); 24 const [userRole, setUserRole] = useState<string | null>(null); 25 26 useEffect(() => { 27 // 从 cookie 或 API 初始化用户状态 28 const token = document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]; 29 if (token) { 30 setLoggedIn(true); 31 setUserRole('admin'); // 假设从 API 获取角色 32 } 33 }, []); 34 35 return ( 36 <AppContext.Provider value={{ isLoggedIn, userRole, setLoggedIn, setUserRole }}> 37 {children} 38 </AppContext.Provider> 39 ); 40};
-
条件渲染路由或组件:
在页面或布局中根据用户角色条件渲染内容。1"use client"; 2 3import { useAppContext } from '@/context/AppContext'; 4import Navbar from '@/components/Navbar'; 5import Footer from '@/components/Footer'; 6import { Geist, Geist_Mono } from 'next/font/google'; 7import './globals.css'; 8import React from 'react'; 9import { useRouter } from 'next/navigation'; 10 11const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); 12const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); 13 14export default function RootLayout({ children }: { children: React.ReactNode }) { 15 const { isLoggedIn, userRole } = useAppContext(); 16 const router = useRouter(); 17 18 // 示例:如果未登录,重定向到登录页面 19 React.useEffect(() => { 20 if (!isLoggedIn && window.location.pathname !== '/login') { 21 router.push('/login'); 22 } 23 }, [isLoggedIn, router]); 24 25 return ( 26 <html lang="en"> 27 <body className={`${geistSans.variable} ${geistMono.variable} antialiased !bg-white`}> 28 <Navbar /> 29 <div className="mt-[63px]"> 30 {children} 31 </div> 32 <Footer /> 33 </body> 34 </html> 35 ); 36}
-
动态导航:
参考方法一中Navbar.tsx
的实现,根据用户角色过滤显示路由。
优点与缺点
- 优点:权限逻辑完全在前端,易于与状态管理结合;适合客户端渲染(CSR)场景。
- 缺点:不适合 SSR 或 SSG,因为权限校验在客户端执行,可能导致闪烁或不安全的短暂内容显示;需要额外的重定向逻辑。
总结与推荐
- 全局权限控制:使用 中间件 (Middleware),这是 Next.js 中最接近 Vue Router 路由守卫的方式,适用于大多数场景,性能和安全性较高。
- 页面级权限控制:使用
getServerSideProps
或getStaticProps
,适合少数特定页面需要权限控制的场景。 - 前端动态渲染:结合 Context 或状态管理库,适合客户端渲染项目,但不推荐用于安全性要求高的场景(应结合后端校验)。
完整实现流程建议
- 使用中间件校验用户权限并重定向无权限请求。
- 在前端状态管理中存储用户角色,动态渲染导航菜单或页面内容。
- 后端 API 提供权限校验接口,确保 token 和角色信息准确。
如果您有具体的路由结构或权限需求(例如特定的角色和路由映射),请提供更多细节,我可以进一步定制代码实现。