资讯动态

使用 TanStack Router 构建认证与路由守卫:beforeLoad、redirect 与 RBAC 完整指南

发布时间:2026/9/15 12:09:35 来源:尧图企业网站定制
使用 TanStack Router 构建认证与路由守卫beforeLoad、redirect 与 RBAC 完整指南【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本文以 TanStack Router 路由层的认证与守卫为主题围绕beforeLoad生命周期、redirect()/throw redirect()、isRedirect辅助函数、无路径布局路由_authenticated以及基于角色与权限的 RBAC 控制展开。读完本文你将掌握在路由加载前完成登录校验、登录后回跳、内联登录、分层权限控制并避开路由守卫不保护 Server Function等关键陷阱的完整实战方案。需要先明确一个边界路由守卫是 UX 与导航控制数据的真实安全边界仍在服务端。beforeLoad拦截的是页面体验而读写私有数据的 Server Function、Server Route 或 API 端点必须自行鉴权例如通过authMiddleware中间件。服务端侧的会话 CookieHttpOnly/Secure/SameSite、OAuthstate PKCE、CSRF、密码重置枚举防御与限流等原语请参见 start-core/auth-server-primitives。为什么把认证放进路由层TanStack Router 的路由生命周期中beforeLoad是一个特殊的存在。源码中对它的定位描述得很清楚见 route.tsThis async function is called before a route is loaded. If an error is thrown here, the routes loader will not be called.也就是说beforeLoad在任何渲染发生之前执行并且它抛出的错误会阻断后续的 loader。认证检查放在这里意味着未登录用户永远不会触发受保护路由的 loader、永远不会渲染受保护内容从而避免先闪现受保护内容再跳转的体验缺陷。从 load-client.ts 的客户端执行流程看beforeLoad返回的上下文对象会被合并进match.context随后的loader就能通过 route context 读取认证信息在服务端load-server.tsbeforeLoad的返回值同样会被合并进match.context并可通过__beforeLoadContext参与服务端渲染的水合传输。这为客户端 服务端双端一致的守卫行为提供了实现基础。快速上手beforeLoad redirect() _authenticated 无路径布局最经典的认证边界是无路径pathless布局路由_authenticated。任何放在src/routes/_authenticated/目录下的路由文件都会自动被它包裹并保护// src/routes/_authenticated.tsx import { createFileRoute, redirect } from tanstack/react-router export const Route createFileRoute(/_authenticated)({ beforeLoad: ({ context, location }) { if (!context.auth.isAuthenticated) { throw redirect({ to: /login, search: { redirect: location.href, }, }) } }, // component 默认是 Outlet —— 无需显式声明 })// src/routes/_authenticated/dashboard.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/_authenticated/dashboard)({ component: DashboardComponent, }) function DashboardComponent() { const { auth } Route.useRouteContext() return divWelcome, {auth.user?.username}/div }仓库中的 examples/react/authenticated-routes 提供了一个可直接运行的完整样例_auth.tsx使用完全相同的beforeLoadthrow redirect({ to: /login, search: { redirect: location.href } })模式并在布局组件中结合router.invalidate()实现登出后清空缓存再跳转。核心模式1. 用 Router Context 承载认证状态认证状态通过createRootRouteWithContext注入路由再由RouterProvider的context属性动态更新。这是类型安全的路由上下文会被推断到所有子路由的context上。// src/routes/__root.tsx import { createRootRouteWithContext, Outlet } from tanstack/react-router interface AuthState { isAuthenticated: boolean user: { id: string; username: string; email: string } | null login: (username: string, password: string) Promisevoid logout: () void } interface MyRouterContext { auth: AuthState } export const Route createRootRouteWithContextMyRouterContext()({ component: () Outlet /, })// src/router.tsx import { createRouter } from tanstack/react-router import { routeTree } from ./routeTree.gen export const router createRouter({ routeTree, context: { auth: undefined!, // 占位符 —— 由 RouterProvider 的 context 属性填充 }, }) declare module tanstack/react-router { interface Register { router: typeof router } }// src/App.tsx import { RouterProvider } from tanstack/react-router import { AuthProvider, useAuth } from ./auth import { router } from ./router function InnerApp() { const auth useAuth() // context 属性注入实时认证状态无需重建 router return RouterProvider router{router} context{{ auth }} / } function App() { return ( AuthProvider InnerApp / /AuthProvider ) }关键设计router 只创建一次占位 context之后每次渲染通过RouterProvider的context属性注入实时认证状态。如果每次认证变化都重建 router会重置缓存、重建路由树代价高昂。router的context选项在 router.ts 中被明确描述为提供给所有路由的根上下文且在使用createRootRouteWithContext创建根路由时是必需项。参考样例中auth.tsxexamples/react/authenticated-routes/src/auth.tsx的AuthProvider它用 React Context 封装login/logout/user再通过useAuth()与RouterProvider桥接。2. 重定向式认证登录后回跳Redirect-Back把当前地址存入 search 参数登录成功后原路返回// src/routes/_authenticated.tsx import { createFileRoute, redirect } from tanstack/react-router export const Route createFileRoute(/_authenticated)({ beforeLoad: ({ context, location }) { if (!context.auth.isAuthenticated) { throw redirect({ to: /login, search: { redirect: location.href }, }) } }, })// src/routes/login.tsx import { createFileRoute, redirect } from tanstack/react-router import { useState, type FormEvent } from react // 校验回跳目标防止开放重定向攻击 function sanitizeRedirect(url: unknown): string { if (typeof url ! string || !url.startsWith(/) || url.startsWith(//)) { return / } return url } export const Route createFileRoute(/login)({ validateSearch: (search) ({ redirect: sanitizeRedirect(search.redirect), }), beforeLoad: ({ context, search }) { if (context.auth.isAuthenticated) { throw redirect({ to: search.redirect }) } }, component: LoginComponent, }) function LoginComponent() { const { auth } Route.useRouteContext() const search Route.useSearch() const navigate Route.useNavigate() const [username, setUsername] useState() const [password, setPassword] useState() const [error, setError] useState() const handleSubmit async (e: FormEvent) { e.preventDefault() try { await auth.login(username, password) navigate({ to: search.redirect }) } catch { setError(Invalid credentials) } } return ( form onSubmit{handleSubmit} {error div{error}/div} input value{username} onChange{(e) setUsername(e.target.value)} / input typepassword value{password} onChange{(e) setPassword(e.target.value)} / button typesubmitSign In/button /form ) }三点注意事项sanitizeRedirect必不可少只接受以/开头且不以//开头的相对路径杜绝把用户重定向到//evil.com之类的外部地址开放重定向漏洞。已登录用户访问/login也应被重定向走beforeLoad中throw redirect({ to: search.redirect })避免无意义的登录页停留。仓库示例 examples/react/authenticated-routes/src/routes/login.tsx 用 zod 的validateSearch做同样的事情z.string().optional().catch()并给出默认回跳目标/dashboard。3. 非重定向式认证内联登录Inline Login如果不想改变 URL可以在受保护布局中就地渲染登录表单代替Outlet// src/routes/_authenticated.tsx import { createFileRoute, Outlet } from tanstack/react-router export const Route createFileRoute(/_authenticated)({ component: AuthenticatedLayout, }) function AuthenticatedLayout() { const { auth } Route.useRouteContext() if (!auth.isAuthenticated) { return LoginForm / } return Outlet / }用户停留在同一页面看到登录表单认证成功后Outlet /开始渲染子路由随之出现。适合单页应用内的渐进式解锁场景如评论区、会员区。4. RBAC角色与权限控制在认证状态上扩展角色/权限辅助函数然后在beforeLoad中分层检查// src/auth.tsx interface User { id: string username: string email: string roles: string[] permissions: string[] } interface AuthState { isAuthenticated: boolean user: User | null hasRole: (role: string) boolean hasAnyRole: (roles: string[]) boolean hasPermission: (permission: string) boolean hasAnyPermission: (permissions: string[]) boolean login: (username: string, password: string) Promisevoid logout: () void }仅管理员可见的布局路由// src/routes/_authenticated/_admin.tsx import { createFileRoute, redirect } from tanstack/react-router export const Route createFileRoute(/_authenticated/_admin)({ beforeLoad: ({ context, location }) { if (!context.auth.hasRole(admin)) { throw redirect({ to: /unauthorized, search: { redirect: location.href }, }) } }, })多角色放行// src/routes/_authenticated/_moderator.tsx import { createFileRoute, redirect } from tanstack/react-router export const Route createFileRoute(/_authenticated/_moderator)({ beforeLoad: ({ context, location }) { if (!context.auth.hasAnyRole([admin, moderator])) { throw redirect({ to: /unauthorized, search: { redirect: location.href }, }) } }, })基于权限细粒度// src/routes/_authenticated/_users.tsx import { createFileRoute, redirect } from tanstack/react-router export const Route createFileRoute(/_authenticated/_users)({ beforeLoad: ({ context, location }) { if (!context.auth.hasAnyPermission([users:read, users:write])) { throw redirect({ to: /unauthorized, search: { redirect: location.href }, }) } }, })页面级权限检查嵌套在已受角色保护的布局下用抛错而非重定向表达无权// src/routes/_authenticated/_users/manage.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/_authenticated/_users/manage)({ beforeLoad: ({ context }) { if (!context.auth.hasPermission(users:write)) { throw new Error(Write permission required) } }, component: UserManagement, }) function UserManagement() { const { auth } Route.useRouteContext() const canDelete auth.hasPermission(users:delete) return ( div h1User Management/h1 {canDelete buttonDelete User/button} /div ) }注意这里的组合思路_authenticated管是否登录_admin/_moderator管角色_users管模块权限manage.tsx管页面级写权限——每一层只关注一个维度路由树天然形成权限分层。同时前端 RBAC 只是 UX 优化按钮显隐、页面拦截都可通过查看源码绕过真正的授权必须由服务端再次执行见下文路由守卫不保护 Server Function。5. 用 isRedirect 正确处理认证检查失败redirect()通过抛错实现导航因此在try/catch包裹的beforeLoad中重定向可能被catch吞掉。此时用isRedirect区分有意的重定向与真正的错误import { createFileRoute, redirect, isRedirect } from tanstack/react-router export const Route createFileRoute(/_authenticated)({ beforeLoad: async ({ context, location }) { try { const user await verifySession(context.auth) if (!user) { throw redirect({ to: /login, search: { redirect: location.href }, }) } return { user } } catch (error) { if (isRedirect(error)) throw error // 重新抛出重定向不要吞掉它 // 真正的错误 —— 重定向到登录页 throw redirect({ to: /login, search: { redirect: location.href }, }) } }, })isRedirect的实现非常直观见 redirect.tsexport function isRedirect(obj: any): obj is AnyRedirect { return obj instanceof Response !!(obj as any).options }也就是说redirect()创建的其实是一个携带导航选项的Response对象。查看 redirect.ts 的redirect()实现可以看到更多细节它会用opts.statusCode || opts.code || 307归一化 HTTP 状态码默认307把href写入Location头并将导航选项挂载到response.options上如果传了throw: true则直接抛出该 Response否则返回它。Redirect类型本身也继承了Response见同文件 L9-L17。理解这一点就理解了为什么重定向靠抛错、为什么isRedirect能靠instanceof Response判定、以及为什么在try/catch里必须显式 re-throw。常见错误清单CRITICAL路由守卫不保护 Server FunctionbeforeLoad的重定向只保护路由的 UI不保护声明在该路由上的Server Function。createServerFn生成的 RPC 端点可以用其声明的 HTTP 方法独立直达——攻击者根本不需要加载/_authenticated/orders页面直接调用 GET RPC 即可// 错误 —— handler 没有任何鉴权路由守卫帮不上忙 import { createServerFn } from tanstack/react-start import { createFileRoute, redirect } from tanstack/react-router const getMyOrders createServerFn({ method: GET }).handler(async () { return db.orders.findMany() // ← 任何人都能打到这个 RPC }) export const Route createFileRoute(/_authenticated/orders)({ beforeLoad: ({ context }) { if (!context.auth.isAuthenticated) throw redirect({ to: /login }) }, loader: () getMyOrders(), })// 正确 —— 鉴权落在 handler 本身通过中间件强制 import { createServerFn } from tanstack/react-start import { authMiddleware } from ~/server/auth-middleware const getMyOrders createServerFn({ method: GET }) .middleware([authMiddleware]) .handler(async ({ context }) { return db.orders.findMany({ where: { userId: context.session.userId } }) })经验法则凡是触碰用户数据的createServerFn、Server Route 或 API 端点都需要authMiddleware或等价的 handler 内检查。路由守卫管页面体验端点守卫管数据。authMiddleware的完整工厂模式参见 start-core/middleware那里给出了createMiddleware().server(...)在服务端校验会话如从 Cookie DB 查 session并通过next({ context: { session } })把用户身份传给 handler 的完整链路会话必须来自服务端可信来源Cookie DB绝不能来自客户端sendContext——客户端能发送的东西客户端就能伪造。会话与 Cookie 原语本身参见 start-core/auth-server-primitives。CRITICAL匿名目的地仍可能泄露受保护数据不只是 API 调用整个匿名响应都要保护。公开的登录页或未授权页如果它的标题、文案、search 参数或序列化的 loader 状态中包含了受保护的用户、租户、记录或资源名依然会泄露信息。请用直接匿名请求并跟随重定向来测试断言以下几点handler 在读取私有数据之前就拒绝没有受保护的 loader 被执行最终 HTML 与序列化状态中不含受保护的标识信息重定向只包含净化后的相对回跳 URL。HIGH把认证检查放在组件里而不是 beforeLoad组件级检查会导致受保护内容闪现后才重定向// 错误 —— 受保护内容会先渲染一瞬间再跳转 export const Route createFileRoute(/_authenticated/dashboard)({ component: () { const auth useAuth() if (!auth.isAuthenticated) return Navigate to/login / return Dashboard / }, }) // 正确 —— beforeLoad 在任何渲染之前执行 export const Route createFileRoute(/_authenticated/dashboard)({ beforeLoad: ({ context }) { if (!context.auth.isAuthenticated) { throw redirect({ to: /login }) } }, component: Dashboard, })HIGH在 try/catch 中没有重新抛出 redirectredirect()靠抛错工作beforeLoad里的try/catch会把重定向吞掉// 错误 —— 重定向被 catch 捕获并吞掉 beforeLoad: async ({ context }) { try { await validateSession(context.auth) } catch (e) { console.error(e) // 把重定向也吞了 } } // 正确 —— 用 isRedirect 区分有意的重定向与真正的错误 import { isRedirect } from tanstack/react-router beforeLoad: async ({ context }) { try { await validateSession(context.auth) } catch (e) { if (isRedirect(e)) throw e console.error(e) } }MEDIUM条件渲染根路由组件根路由无论如何都会渲染你无法条件性地渲染它的组件// 错误 —— 根路由始终渲染这保护不了任何东西 export const Route createRootRoute({ component: () { if (!isAuthenticated()) return Login / return Outlet / }, }) // 正确 —— 用无路径布局路由作为认证边界 // src/routes/_authenticated.tsx export const Route createFileRoute(/_authenticated)({ beforeLoad: ({ context }) { if (!context.auth.isAuthenticated) { throw redirect({ to: /login }) } }, })受保护路由应作为_authenticated布局路由的子路由公开路由login、home 等放在它外面。深入理解redirect() 的底层机制与边界情况状态码redirect({ statusCode })允许自定义 HTTP 状态码默认 307临时重定向旧的code字段已被标记废弃见 redirect.ts。外部重定向传href绝对 URL时router 会把它归类为整页导航并把href写入Location头见同文件 L113-L116。序列化重定向服务端渲染场景下重定向对象可被序列化/反序列化parseRedirect同文件 L146-L152会把序列化后的对象还原回 redirect Response。类型安全redirect的to/params/search都是类型约束的RedirectOptions泛型化错误的目标路径会在编译期报错这正是 TanStack Router 全类型安全路由体验的一部分。延伸阅读router-core/data-loadingbeforeLoad先于loader执行认证上下文通过 route context 流入 loader。start-core/auth-server-primitives认证的服务端半场——会话 Cookie、OAuthstate PKCE、CSRF、密码重置加固、限流。start-core/middlewareauthMiddleware工厂模式用于保护单个createServerFn调用。examples/react/authenticated-routes完整可运行的认证路由示例_auth布局 login页面 auth.tsxProvider。【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价