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

背景

上一篇(前端路由(二):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 提供的声明式重定向组件------渲染它,就会触发路由跳转。这里的 replacestate 两个属性各有用途:

  • 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.stateundefined,可选链运算符 ?. 防止访问 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 变为 /payProtectRoute 再次判定------这次 isLogin === 'true',渲染 <Pay />
  8. 用户按「后退」,浏览器回到 /login 之前的页面------登录页已从历史栈中消失

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

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

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

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

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

小结

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

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

相关推荐
半个落月1 小时前
从零梳理 React Router:路由、懒加载、嵌套页面与登录鉴权
前端·react.js
tedcloud1232 小时前
Impeccable 部署指南:开源前端设计工具 Linux 环境搭建实践
linux·运维·服务器·前端·人工智能·开源
默_笙2 小时前
🚩 React + TypeScript 的 Props 通信,我从"把事件对象传给父组件"进化到了"只传值"
前端·javascript
渣波2 小时前
赋予 React “多线程”:Hooks + Web Worker 并发计算架构深度解析
前端·javascript
光影少年2 小时前
Codex 斜杠指令体系与业务场景调度实战
前端·react.js·reactivex
90后的晨仔2 小时前
uni-app中如何跳转?
前端
Y3815326623 小时前
SERP API 请求体 Gzip 压缩优化实战:大数据量降带宽 80%
开发语言·前端·php
踩着两条虫4 小时前
VTJ.PRO AI Agent 技术白皮书
前端·vue.js·低代码
子兮曰4 小时前
解剖 Claude Code:从入口架构逆向工程看 AI 编程工具的信任边界设计
前端·后端·claude