受保护的路由
应用的某些部分只应该对已登录用户可见:一个仪表板、账户页面,或者任何获取个人数据的地方。React Router 没有专门的"受保护路由"功能,也不需要。来自路由和嵌套路由的概念组合成一个模式:一个布局路由,检查用户是否已登录,然后渲染它的子元素或重定向到登录页。
在写任何代码之前要明确一点。在客户端守护路由是一个用户体验功能。它防止未登录的访客进入那些会获取数据并显示损坏、空白屏幕的页面。这永远不是安全保护,因为客户端包中的所有东西都是可以检查和绕过的。
真正的保护发生在服务器上,服务器必须拒绝向未经身份验证的请求交付数据。身份验证涵盖该强制执行的工作方式;本章讨论的是客户端部分。
auth-required 布局路由
一个布局路由是一个父路由,它的元素渲染共享的 UI 加上一个用于子元素的 Outlet。一个无路径布局路由根本不添加 URL 段;它纯粹是为了包装而存在。这使它成为登录检查的完美之地:用一个无路径布局路由包装你想保护的每个路由,它的唯一工作是决定 Outlet 是否渲染。
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="login" element={<Login />} />
<Route element={<AuthRequired />}>
<Route path="host" element={<Dashboard />} />
<Route path="host/vans" element={<HostVans />} />
<Route path="host/vans/:id" element={<HostVanDetail />} />
</Route>
</Route>所有嵌套在 <Route element={<AuthRequired />}> 内的内容现在都在守护之后,包括带参数的路由,比如 host/vans/:id。稍后保护一个新页面只需一行:把它的路由移到包装器内。
守护本身有两个分支。如果用户已登录,渲染 Outlet 使匹配的子元素出现。如果没有,渲染一些东西把他们送到登录页。不渲染任何东西就是全部诀窍:如果一个受保护的组件永远不渲染,里面的任何数据获取永远不会启动。
import { Outlet, Navigate } from 'react-router-dom'
export default function AuthRequired() {
const authenticated = false // 实际会话检查的占位符
if (!authenticated) {
return <Navigate to="/login" />
}
return <Outlet />
}这个课程用一个布尔值伪造检查,后来用 localStorage 中的值,以保持重点在路由上。真正的检查读取会话状态,通常通过上下文传递;同样,身份验证涵盖该状态实际来自哪里。
版本说明
这里的例子从 react-router-dom 导入,在 v6 和 v7 中都有效;路由涵盖 v7 对包的改变。本节课程全程教授 v6。
Navigate 组件
Link 渲染一个锚点并等待被点击。Navigate 跳过等待:一旦它渲染,路由就把用户移动到它的 to 路径。这使它成为渲染逻辑的重定向工具。
你的组件在渲染中决定用户不应该在这里,并返回 <Navigate to="/login" /> 而不是任何 UI。还有一个 useNavigate hook 返回一个用于从事件处理程序重定向的函数,这就是登录表单本身在成功提交后发送用户继续的方式。
登陆一个空白登录页而没有解释会令人困惑,所以守护应该说明为什么用户最终出现在这里。Navigate 接受与 Link 相同的 state prop,所以重定向可以给目标一个原因。
if (!authenticated) {
return (
<Navigate
to="/login"
state={{ message: '你必须先登录' }}
/>
)
}import { useLocation } from 'react-router-dom'
function Login() {
const location = useLocation()
return (
<>
{location.state?.message && <h3>{location.state.message}</h3>}
<h1>登录你的账户</h1>
{/* 表单... */}
</>
)
}可选链接很重要,因为直接点击到登录页的人从不通过守护,发现 location.state 设置为 null。
历史栈和 replace
到目前为止的模式有一个你能感觉到的 bug。访问一个受保护的页面而未登录,被重定向,登录,然后按浏览器的后退按钮。与你在这一切开始前所在的页面不同,你登陆了登录页面,完整地显示它的"你必须先登录"消息。
浏览器维护一个历史栈:每次导航推送一个新项,后退弹出到前一个。重定向在受保护页面之后推送了 /login 到栈,登录表单自己的导航在其上方推送了受保护页面。后退直接走入剩余物。
修复是使重定向替换当前项而不是推送新的。在 Navigate 组件上是 replace prop;在 useNavigate 函数上是一个选项。
// 在 AuthRequired 中:重定向为被阻挡的页面交换自己
return <Navigate to="/login" state={{ message: '你必须先登录' }} replace />
// 在 Login 中,在成功提交后
navigate('/host', { replace: true })两者都到位后,登录迂回永远不会在历史中存活。从受保护页面后退现在去往用户之前所在的任何地方,登录页面无法通过意外重新访问。一个好的经验法则:任何用户没有要求的导航,这就是重定向,应该替换。
回到他们要去的地方
一个粗糙的边缘仍然存在。登录表单硬编码 navigate('/host', ...),所以试图到达 /host/vans/2 的用户在登录后被倾倒在 /host。守护知道被阻挡的 URL,因为 AuthRequired 内的 useLocation 描述了用户试图渲染的页面。在相同的导航状态中沿着传递它。
export default function AuthRequired() {
const location = useLocation()
const authenticated = false
if (!authenticated) {
return (
<Navigate
to="/login"
state={{
message: '你必须先登录',
from: location.pathname + location.search + location.hash,
}}
replace
/>
)
}
return <Outlet />
}pathname 单独不是完整的地址:在 /host/vans?type=luxury 被阻挡的用户试图去往过滤的列表,丢掉查询字符串会剥去过滤。添加 search 和 hash 再现路由跟踪的 URL 的每个部分。登录页面读取值回,对于直接来到登录页并因此没有状态的人有一个回退。
function Login() {
const location = useLocation()
const navigate = useNavigate()
const from = location.state?.from || '/host'
function handleLogin() {
// ...成功时:
navigate(from, { replace: true })
}
// ...
}现在一个任何受保护页面的共享链接存活登录迂回:被阻挡,重定向,登录,并传递到确切的 URL 他们想要。
Outlet 页面正常显示。如果他们没有,它渲染 Navigate,一旦它出现就把他们迅速带到登录页。 添加 replace 将重定向从后退按钮的记忆中保留出去,沿着传递他们去往的地方让登录页在之后将他们直接送回那里。
记住这对用户的友好;服务器仍然必须自己保护数据。
接下来:React 如何渲染,在每个后续性能决定之后的心智模型。

