You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于JWT在服务端结合RequireAuth组件实现灵活路由权限保护

权限验证实现问题与解决方案

问题背景

我知道权限验证必须在服务端完成,不能只依赖客户端,但不清楚具体怎么实现。以下是我现有的代码和疑问:

前端React代码

1. App.js(路由配置)

这里配置了路由规则,页面渲染前通过RequireAuth组件做路由守卫,不同路由设置了对应的允许角色,比如教师路由只允许teacher角色访问,学生路由允许student和teacher角色访问。

import { useContext} from 'react'
import './style.scss'
import Register from './pages/Register/Register'
import TeacherLogin from './pages/Login/TeacherLogin/TeacherLogin'
import StudentLogin from './pages/Login/StudentLogin/StudentLogin'
import ForgotPassword from './pages/ForgotPassword/ForgotPassword'
import PageNotFound from './pages/PageNotFound/PageNotFound'
import Home from './pages/Home/Home'
import AccountType from './pages/AccountType/AccountType'
import Users from './pages/Students/Students'
import SOTW from './pages/SOTW/SOTW'
import Landing from './pages/Landing/Landing'
import Add from './pages/Add/Add'
import Test from './pages/Test/Test'
import Edit from './pages/Edit/Edit'
import ClassStats from './pages/ClassStats/ClassStats'
import {
  BrowserRouter,
  Routes,
  Route,
} from "react-router-dom";
import { DarkModeContext } from './Hooks/context/darkModeContext'
import Profile from './StudentPages/Profile/Profile'
import RequireAuth from './components/RequireAuth/RequireAuth'

function App() {

  const {darkMode} = useContext(DarkModeContext)

  return (
    <div className={`theme-${darkMode ? "dark" : "light"}`}>
      <BrowserRouter>
        <Routes>
            <Route path='/' element={<Landing />} />
            <Route path="/register" element={<Register/>}/>
            <Route path="forgot-password" element={<ForgotPassword/>} />
            <Route path="test" element={<Test/>} />
            <Route path="/auth">
              <Route index element={<AccountType/>} />
              <Route path ="teacher" element={<TeacherLogin/>} />
              <Route path="student" element={<StudentLogin/>} />
            </Route>
              <Route path="/auth/teacher" element={<RequireAuth allowedRoles={['teacher']}/>}>
                    <Route path='dashboard' element={<Home/>} />
                    <Route path="SOTW" element={<SOTW/>}/>
                    <Route path="users">
                      <Route index element={<Users />} />
                      <Route path="add" element={<Add title='Add Student' button='Add' />} />
                      <Route path="edit" element={<Edit title='Edit Student' button='Update' />} />
                  </Route>
            </Route>
              <Route path="auth/student" element={<RequireAuth allowedRoles={['student', 'teacher']}/>}>
                  <Route path='profile/:id' element={<Profile/>} />
              </Route>
              <Route path="*" element={<PageNotFound/>}/>
          </Routes> 
      </BrowserRouter>
    </div>
  );
}

export default App;

2. RequireAuth组件(路由守卫)

目前只检查本地存储的用户信息,有安全隐患。我已经在组件里调用了后端的/api/auth/protected接口,但不知道接下来该怎么处理。

import { Navigate, useLocation, Outlet } from 'react-router-dom';
import { AuthContext } from '../../Hooks/context/AuthContext';
import { useContext, useEffect, useState } from 'react';
import axios from 'axios';

const RequireAuth = ({ allowedRoles }) => {
  const { testUser, setTestUser, user } = useContext(AuthContext);
  const [statusAuth, setStatusAuth] = useState(false);
  const location = useLocation();

  console.log(user)

  useEffect(() => {
    const fetch = async () => {
      const res = await axios.get('/api/auth/protected')
      console.log(res) // THIS IS WHERE I AM STUCK. WHAT DO I DO HERE?
    }
    fetch()
  },[])
  
  return allowedRoles.includes(user?.role) ? (
    <Outlet />
  ) : user ? (
    <Navigate to="/unauthorised" state={{ from: location }} replace />
  ) : (
    <Navigate to="/" state={{ from: location }} replace />
  );
};

export default RequireAuth;

后端Node.js代码

1. 验证中间件

实现了verifyToken验证JWT,还有verifyAdmin中间件,但这个中间件只校验用户是否是teacher角色,没法灵活适配student等其他角色的权限需求。

import jwt from 'jsonwebtoken'
import { createError } from './error.js'

export const verifyToken = (req, res, next) => {
    const token = req.headers.cookie.split('=')[1]

    if (!token) {
      return next(createError(401, "You are not authenticated!"));
    }
    jwt.verify(token, process.env.JWT, (err, user) => {
        if (err) return next(createError(403, "Token is not valid!"));
        req.user = user;
        next();
      });
  };

export const verifyAdmin = (req, res, next) => {
    verifyToken(req, res, () => {
        if(req.user.role === 'teacher') {
            next()
        } else {
            return next(createError(403, 'You are not authorised'))
        }
    })
}

2. 路由与控制器

配置了受保护的/protected路由,使用verifyAdmin中间件,控制器只返回成功信息。

// Routes
import express from 'express'
import {register, login, refreshToken, logout, protectedRoute} from '../Controllers/auth.js'
import { verifyToken, verifyAdmin } from '../verifyToken.js';

const router = express.Router();

//PROTECTED ROUTE
router.get("/protected", verifyAdmin, protectedRoute)

export default router

// Controller
import mongoose from 'mongoose'
import Student from '../Models/student.js'
import bcrypt from 'bcrypt'
import { createError } from '../error.js'
import jwt from 'jsonwebtoken'


export const protectedRoute = async (req, res, next) => {
    res.status(200).send('Success')
      }

核心疑问

  1. 是否需要在每次页面请求时都调用服务端接口来验证用户是否有权限访问该页面?
  2. 如何让RequireAuth组件根据student、teacher、admin等不同角色灵活进行页面重定向?

解决方案

针对疑问1:页面请求时的服务端验证策略

  • 不需要每次页面跳转都调用通用的/protected接口,更高效合理的方式是:
    1. 登录成功后,把JWT存在HttpOnly Cookie中(你当前已实现),同时将用户基础信息(如角色)存在前端全局状态(比如AuthContext),用于前端路由的快速判断——但这只是体验优化,不能作为唯一验证依据。
    2. 核心是所有后端业务接口(包括页面依赖的数据接口)都要做权限验证:比如用户访问教师仪表盘时,前端请求仪表盘数据的接口会自动携带Cookie,后端用对应权限中间件校验,若权限不足直接返回403,前端再处理跳转。
    3. 如果要做页面级的服务端验证(防止用户直接输入URL访问),可以在RequireAuth中调用一个返回用户真实角色的接口,一次请求完成身份+权限的双重校验,而非通用的成功/失败接口。

针对疑问2:灵活的角色权限控制与重定向

第一步:改造后端中间件,支持多角色校验

把单一的verifyAdmin改成通用的verifyRoles中间件,接收允许的角色列表动态校验:

import jwt from 'jsonwebtoken'
import { createError } from './error.js'

export const verifyToken = (req, res, next) => {
    const token = req.headers.cookie.split('=')[1]

    if (!token) {
      return next(createError(401, "你未授权!"));
    }
    jwt.verify(token, process.env.JWT, (err, user) => {
        if (err) return next(createError(403, "Token无效!"));
        req.user = user;
        next();
      });
  };

// 通用角色验证中间件,支持传入多个允许角色
export const verifyRoles = (...allowedRoles) => {
  return (req, res, next) => {
    verifyToken(req, res, () => {
      if (allowedRoles.includes(req.user.role)) {
        next();
      } else {
        return next(createError(403, '你没有权限访问!'));
      }
    });
  };
};

然后更新后端路由,新增一个返回用户真实角色的接口,同时给业务路由配置对应权限:

// 路由文件
import express from 'express'
import {register, login, refreshToken, logout} from '../Controllers/auth.js'
import { verifyToken, verifyRoles } from '../verifyToken.js';

const router = express.Router();

// 返回当前用户的角色信息(用于前端校验)
router.get("/check-auth", verifyToken, (req, res) => {
  // 只返回必要信息,避免敏感数据泄露
  res.status(200).json({ role: req.user.role, id: req.user.id });
});

// 教师专属路由示例
router.get("/teacher/dashboard", verifyRoles('teacher'), (req, res) => {
  // 返回教师仪表盘数据
});

// 学生/教师均可访问的路由示例
router.get("/student/profile/:id", verifyRoles('student', 'teacher'), (req, res) => {
  // 返回学生信息
});

export default router

第二步:改造前端RequireAuth组件

让组件先调用后端的/api/auth/check-auth接口获取用户真实角色,再和allowedRoles对比,处理重定向逻辑:

import { Navigate, useLocation, Outlet } from 'react-router-dom';
import { AuthContext } from '../../Hooks/context/AuthContext';
import { useContext, useEffect, useState } from 'react';
import axios from 'axios';

const RequireAuth = ({ allowedRoles }) => {
  const { user, setUser } = useContext(AuthContext);
  const [isLoading, setIsLoading] = useState(true);
  const [authError, setAuthError] = useState(null);
  const location = useLocation();

  useEffect(() => {
    const checkAuthStatus = async () => {
      try {
        const res = await axios.get('/api/auth/check-auth');
        // 更新全局状态的用户信息,确保是服务端返回的真实数据
        setUser(prev => ({ ...prev, role: res.data.role, id: res.data.id }));
        setIsLoading(false);
      } catch (err) {
        setAuthError(err.response?.data?.message || '验证失败');
        setIsLoading(false);
      }
    };

    checkAuthStatus();
  }, [setUser]);

  // 加载状态可显示加载动画
  if (isLoading) {
    return <div>加载中...</div>;
  }

  // 验证失败,跳转到登录页
  if (authError) {
    return <Navigate to="/" state={{ from: location }} replace />;
  }

  // 角色校验通过,渲染页面
  if (allowedRoles.includes(user?.role)) {
    return <Outlet />;
  } 

  // 角色不匹配,跳转到无权限页面
  return <Navigate to="/unauthorised" state={{ from: location }} replace />;
};

export default RequireAuth;

第三步:保持前端路由配置一致性

确保路由的allowedRoles和后端verifyRoles的配置完全对应即可,你当前的App.js路由配置无需大改。


内容的提问来源于stack exchange,提问作者lmcc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 06:15:21