Next路由权限控制

分类: 编码创建于: 6/1/2025

在 Next.js 中实现路由的动态渲染,并根据后端设置的当前用户权限来控制哪些路由可以渲染,类似于 Vue Router 的权限路由管理,是完全可行的。Next.js 虽然不像 Vue Router 那样有内置的路由守卫(Route Guard),但可以通过以下几种方式结合其特性(如中间件、动态路由、API 路由等)实现类似的功能。

下面我将详细介绍如何在 Next.js 中实现基于用户权限的路由动态渲染,并提供代码示例。


实现思路

  1. 用户权限获取:从后端获取当前用户的角色或权限列表(例如通过 API 调用或 cookie/token 解析)。
  2. 路由权限控制
    • 使用 Next.js 的 中间件 (Middleware) 在请求到达页面前进行权限校验,决定是否允许访问某些路由。
    • 或者在页面级别(如 getServerSidePropsgetStaticProps)根据权限动态决定是否渲染页面。
  3. 动态路由渲染:根据用户权限动态过滤或生成可访问的路由列表,并在前端导航(如菜单或链接)中只显示用户有权限访问的路由。
  4. 重定向或错误处理:对于无权限访问的路由,重定向到登录页或显示无权限提示页面。

方法一:使用中间件 (Middleware) 进行全局路由权限控制

Next.js 的中间件(middleware.jsmiddleware.ts)运行在请求到达页面之前,非常适合用来实现全局路由守卫,检查用户权限并决定是否允许访问某些路由。

实现步骤

  1. 创建中间件文件
    在项目根目录下创建 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};
  2. 创建无权限页面
    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;
  3. 动态导航菜单
    在前端导航组件中,根据用户角色动态渲染可访问的路由链接。

    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)可能需要额外处理。

方法二:使用 getServerSidePropsgetStaticProps 进行页面级权限控制

如果您不希望使用中间件,或者只对部分页面进行权限控制,可以在页面级别使用 getServerSidePropsgetStaticProps 结合 getSession 等方式检查权限,并决定是否渲染页面。

实现步骤

  1. 在页面中添加权限校验
    在需要保护的页面中,使用 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;
  2. 动态导航菜单
    与方法一相同,根据用户角色动态渲染导航菜单。

优点与缺点

  • 优点:适合对特定页面进行权限控制,可以直接在页面级别获取用户数据并传递给组件;易于与 SSR 结合。
  • 缺点:每个页面都需要单独编写权限逻辑,代码重复性较高;不适合全局路由控制(需要重复代码)。

方法三:结合 React Context 或状态管理库实现前端动态路由

如果权限数据已经在前端状态(如 Context、Redux、Zustand)中,可以在前端通过条件渲染动态显示路由或页面内容。

实现步骤

  1. 使用 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};
  2. 条件渲染路由或组件
    在页面或布局中根据用户角色条件渲染内容。

    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}
  3. 动态导航
    参考方法一中 Navbar.tsx 的实现,根据用户角色过滤显示路由。

优点与缺点

  • 优点:权限逻辑完全在前端,易于与状态管理结合;适合客户端渲染(CSR)场景。
  • 缺点:不适合 SSR 或 SSG,因为权限校验在客户端执行,可能导致闪烁或不安全的短暂内容显示;需要额外的重定向逻辑。

总结与推荐

  • 全局权限控制:使用 中间件 (Middleware),这是 Next.js 中最接近 Vue Router 路由守卫的方式,适用于大多数场景,性能和安全性较高。
  • 页面级权限控制:使用 getServerSidePropsgetStaticProps,适合少数特定页面需要权限控制的场景。
  • 前端动态渲染:结合 Context 或状态管理库,适合客户端渲染项目,但不推荐用于安全性要求高的场景(应结合后端校验)。

完整实现流程建议

  1. 使用中间件校验用户权限并重定向无权限请求。
  2. 在前端状态管理中存储用户角色,动态渲染导航菜单或页面内容。
  3. 后端 API 提供权限校验接口,确保 token 和角色信息准确。

如果您有具体的路由结构或权限需求(例如特定的角色和路由映射),请提供更多细节,我可以进一步定制代码实现。