前端路由(三):鉴权、拦截与重定向

背景

上一篇(前端路由(二):React Router组件化路由实战.md)用 React Router 搭起了路由的组件化骨架:<Routes> 声明路径到组件的映射,<Link> 处理导航,useParams 提取动态参数,<Outlet> 承载嵌套渲染,lazy() 做代码分割。到这一步,页面之间的切换已经可以正常工作。

但「能切换页面」和「能管理访问」之间还有一段距离。一个面向用户的 Web 应用通常会需要:

  • 某些页面只有登录后才能访问(如支付页、个人中心)
  • 未登录用户访问受限页面时,自动跳转到登录页
  • 登录成功后,回到用户原本想去的那一页
  • 登录页在浏览历史中不留痕迹------用户按「后退」不应该再看到登录表单

这些需求不再属于「路由怎么配」,而是「路由在运行时怎么决策」。本文基于 react-router-demo 中新增的鉴权相关代码,拆解路由守卫、登录重定向、状态传递这一组模式的实现方式。

路由守卫:用 children 模式做条件渲染

路由守卫的职责是在进入目标页面之前做一个判定:当前用户是否有权访问?如果有,正常渲染;如果没有,拦截并跳转。

ProtectRoute.jsx 的实现:

javascript 复制代码
import { Navigate } from 'react-router-dom'

const ProtectRoute = ({ children }) => {
  const isLogin = localStorage.getItem('isLogin') === 'true'

  if (!isLogin) {
    return (
      <Navigate to="/login" replace state={{ from: location.pathname }} />
    )
  }

  return <>{children}</>
}

export default ProtectRoute

这个组件的结构值得拆开来看。

children 是路由守卫的通用模式。 ProtectRoute 不关心自己包裹的是什么页面------它只做判定。通过 props 接收 children,调用方决定被保护的内容:

xml 复制代码
<ProtectRoute>
  <Pay />
</ProtectRoute>

这种写法让守卫逻辑和页面逻辑彻底解耦。ProtectRoute 可以包任何组件------支付页、个人中心、后台管理------不需要为每个受保护页面单独写一个守卫。

判定逻辑依赖 localStorage。 HTTP 是无状态协议,登录状态需要客户端自己维护。常见方案有三种:请求头携带 Token、Cookie、localStorage。demo 选择了 localStorage------登录成功时写入 isLogin = 'true',ProtectRoute 读取这个标记做判定。选择 localStorage 的好处是读写都在前端完成,不依赖服务端 Session,适合 demo 级别的快速验证。

未登录时返回 <Navigate> 组件。 <Navigate> 是 React Router 提供的声明式重定向组件------渲染它,就会触发路由跳转。这里的 replace 和 state 两个属性各有用途:

  • replace:用新 URL 替换当前历史记录,用户无法通过「后退」回到被拦截前的页面
  • state:携带 location.pathname------用户原本想去哪里。这个值将在登录成功后用于跳回

登录与回跳:useNavigate、location.state 和 replace

Login/index.jsx 是鉴权流程的另一半:

javascript 复制代码
import { useNavigate, useLocation } from 'react-router-dom'

const Login = () => {
  const navigate = useNavigate()
  const location = useLocation()
  const from = location.state?.from || '/'

  function handleSubmit(e) {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    const username = formData.get('username')
    const password = formData.get('password')

    if (username === 'admin' && password === '123456') {
      localStorage.setItem('isLogin', 'true')
      navigate(from, { replace: true })
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <h1>登录</h1>
      <input name="username" placeholder="请输入用户名" required />
      <input name="password" placeholder="请输入密码" required />
      <button type="submit">登录</button>
    </form>
  )
}

这里有几个设计选择值得展开。

const from = location.state?.from || '/'------可选链 + 默认值。

location.state 是路由跳转时携带的附属数据,不会出现在 URL 里。用户直接访问 /login(而非被拦截后跳转过来)时,location.state 为 undefined,可选链运算符 ?. 防止访问 undefined.from 抛错,|| '/' 提供默认跳转目标。

从支付页被拦截的用户,from 的值是 /pay,登录后回到支付页。直接访问登录页的用户,from 为 /,登录后回到首页。一条表达式覆盖两种场景。

navigate(from, { replace: true })------登录成功后替换历史记录。

这里用了 replace: true,和 ProtectRoute 中 <Navigate replace> 形成呼应。如果不加 replace,登录成功后的跳转会在历史栈中新增一条记录。用户按「后退」按钮时,浏览器会回到登录页------此时用户已经登录,登录页再读 localStorage 判定已登录,又自动跳走。这个循环不会报错,但会让导航行为变得不可预测。

replace: true 用新页面的历史记录覆盖掉登录页的记录,用户在登录成功后的页面按「后退」,会直接跳过登录页,回到进入登录页之前的页面。对用户来说,登录是一个「透明的中间步骤」------来过,但历史里找不到痕迹。

useNavigate 与 <Navigate> 的对应关系。 <Navigate> 是声明式写法,适合放在 JSX 中做条件渲染;useNavigate() 是编程式写法,适合放在事件处理、异步回调中触发跳转。ProtectRoute 用前者(判定未登录时直接返回 <Navigate>),Login 用后者(表单提交成功后才调用 navigate())。同一套路由跳转能力,两种使用入口。

串联:一次完整的鉴权流程

把上面的组件放回路由配置中,看一次完整请求的走向。

App.jsx 中的相关路由:

xml 复制代码
<Route path="/login" element={<Login />} />
<Route path="/pay" element={
  <ProtectRoute>
    <Pay />
  </ProtectRoute>
} />

用户点击导航栏中的「支付」链接时:

  1. 路由匹配到 /pay,渲染 <ProtectRoute><Pay /></ProtectRoute>
  2. ProtectRoute 读取 localStorage,发现 isLogin !== 'true'
  3. 返回 <Navigate to="/login" replace state={{ from: '/pay' }} />,触发重定向
  4. URL 变为 /login,渲染 <Login />,location.state.from 为 /pay
  5. 用户输入用户名密码,点击登录
  6. handleSubmit 写入 localStorage,调用 navigate('/pay', { replace: true })
  7. URL 变为 /pay,ProtectRoute 再次判定------这次 isLogin === 'true',渲染 <Pay />
  8. 用户按「后退」,浏览器回到 /login 之前的页面------登录页已从历史栈中消失

整个流程中,路由守卫、登录表单、状态传递、历史记录管理各司其职,组合在一起形成了一条完整的访问控制链路。

三篇回顾:一条从原理到实践的路线

系列三篇文章覆盖了前端路由的三个层次:

  1. 原理层 (其一):浏览器如何管理 URL------Hash 的 hashchange 事件、History API 的 pushState 和 popstate。手写一个 HashRouter 理解路由的本质是「键值对映射 + 事件监听」。
  2. 配置层 (其二):React Router 如何把路由表组织成组件树------<Routes> 声明式配置、<Link> 导航、useParams 参数提取、<Outlet> 嵌套渲染、lazy() 按需加载。组件化路由解决的核心问题是:当路由规模从三个页面扩展到三十个页面时,怎样用一致的 API 管理复杂性。
  3. 控制层 (其三,本文):路由在运行时如何做访问控制------children 模式做守卫组件、<Navigate> 声明式重定向、useNavigate 编程式跳转、location.state 跨页面传递状态、replace 管理历史记录。这些模式让路由从「能切换」进到「能决策」。

三层之间的关系是递进而不是替代:原理层解释「为什么能工作」,配置层提供「怎么组织」,控制层补充「怎么决策」。实际项目中,三层同时存在------底层是浏览器的 URL API,中间是框架的路由配置,上层是业务方的守卫和跳转逻辑。

小结

鉴权路由这一组模式,本质上是在路由的匹配-渲染流程中插入了一个判定节点。ProtectRoute 用 children 接收被保护的内容,用 localStorage 做状态判定,用 <Navigate> 触发拦截跳转,用 location.state 记录来源路径。Login 用 useNavigate 做编程式回跳,用 replace: true 清理历史记录。

这些模式不限于鉴权场景。任何需要在进入页面前做前置检查的逻辑------角色权限、功能开关、A/B 实验分流------都可以复用同一套「守卫组件 + children + 条件重定向」的结构。理解了这一层,前端路由就不再只是「切换页面的工具」,而是一个可以承载业务决策的运行时框架。

相关推荐
Setsuna_F_Seiei2 小时前
前端转型 Agent 开发 06 之 Agent Memory 记忆系统(让 Agent 更智能,更懂你)
前端·agent·ai编程
zhangzeyuaaa3 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
默_笙3 小时前
🌊 向量库和 ES 都查不出"关系",我只好给奶茶建了一张人脉网
前端·javascript
优选资讯3 小时前
标签打印软件怎么对接 Excel 数据
前端·excel
一个风轻云淡4 小时前
GCC 和 GDB命令简单解读
java·linux·前端
u0111026755 小时前
图片处理接口如何统一错误信息 前端提示与后端错误码设计
前端·状态模式
福兮说5 小时前
浏览器里把 iPhone 的 HEIC 转成 JPG:Chrome 解不开、转完大了七成、拍摄时间全丢,六个坑实测
前端·javascript·图像处理·chrome·ios·iphone·heic
libokaifa6 小时前
把语音模型塞进手机:sherpa-onnx 在 Android 上的 ASR / TTS
前端
deli0076 小时前
DLA 枝晶生长实测:5000 个粒子集体停摆之后,分形维数收敛到 1.70
前端
IT_陈寒6 小时前
Redis卡顿的锅,这次真不是大key的错
前端·人工智能·后端