认证与路由守卫
TanStack Router 通过 beforeLoad、context 和 redirect() 提供了灵活的认证与授权机制。推荐的架构是三层模式:AuthContext(React 状态)→ Router Context(路由上下文)→ beforeLoad(路由守卫)。
三层认证架构
React AuthContext (useAuth Hook)
↓ 提供 auth 状态和方法(login/logout/user/hasRole)
Router Context (createRootRouteWithContext)
↓ 将 auth 注入路由系统,使所有 loader/beforeLoad 可访问
beforeLoad Guards
↓ 在路由级别执行认证检查和重定向第一步:创建 AuthProvider
// src/auth.tsx
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
interface AuthState {
isAuthenticated: boolean
user: User | null
accessToken: string | null
}
interface AuthContextValue extends AuthState {
login: (email: string, password: string) => Promise<void>
logout: () => void
hasRole: (role: string) => boolean
}
const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<AuthState>({
isAuthenticated: false,
user: null,
accessToken: localStorage.getItem('token'),
})
// 恢复会话
useEffect(() => {
if (state.accessToken) {
fetchUser(state.accessToken)
.then((user) => setState((s) => ({ ...s, isAuthenticated: true, user })))
.catch(() => localStorage.removeItem('token'))
}
}, [])
const login = async (email: string, password: string) => {
const { user, token } = await loginApi(email, password)
localStorage.setItem('token', token)
setState({ isAuthenticated: true, user, accessToken: token })
}
const logout = () => {
localStorage.removeItem('token')
setState({ isAuthenticated: false, user: null, accessToken: null })
}
const hasRole = (role: string) => {
return state.user?.roles?.includes(role) ?? false
}
return (
<AuthContext.Provider value={{ ...state, login, logout, hasRole }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}第二步:路由 Context 注入
// src/routes/__root.tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
// 定义 Router Context 的类型
interface MyRouterContext {
auth: ReturnType<typeof useAuth>
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({
routeTree,
// 🚨 初始时为 undefined!(在 RouterProvider 中注入真实值)
context: { auth: undefined! },
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export { router }// src/App.tsx
import { RouterProvider } from '@tanstack/react-router'
import { useAuth } from './auth'
import { router } from './router'
function InnerApp() {
const auth = useAuth()
// 将 auth 注入路由 context
return <RouterProvider router={router} context={{ auth }} />
}
export default function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}🚨 陷阱:
createRouter时context.auth必须设为undefined!,然后在<RouterProvider context={{ auth }}>中注入真实值。这是因为 context 的类型声明早于组件渲染,需要用undefined!的断言语法。
第三步:beforeLoad 守卫
路径无关认证布局
// src/routes/_authenticated.tsx
import { createFileRoute, Outlet, 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/login.tsx
export const Route = createFileRoute('/login')({
beforeLoad: ({ context }) => {
if (context.auth.isAuthenticated) {
// 已登录 → 无需再登录
throw redirect({ to: '/dashboard' })
}
},
component: LoginPage,
})角色访问控制(RBAC)
角色守卫布局
// src/routes/_authenticated/_admin.tsx
export const Route = createFileRoute('/_authenticated/_admin')({
beforeLoad: ({ context }) => {
if (!context.auth.hasRole('admin')) {
throw redirect({ to: '/unauthorized' })
}
},
component: () => <Outlet />,
})权限守卫(细粒度)
// src/routes/_authenticated/_admin/users.tsx
export const Route = createFileRoute('/_authenticated/_admin/users')({
beforeLoad: ({ context }) => {
// 检查更细粒度的权限
if (!context.auth.user?.permissions.includes('users:read')) {
throw redirect({ to: '/forbidden' })
}
},
component: UsersPage,
})组件级权限控制
对于非路由级别的 UI 权限控制:
function AdminActions() {
const auth = useAuth()
if (!auth.hasRole('admin')) return null
return (
<div>
<button>删除</button>
<button>编辑</button>
</div>
)
}💡 最佳实践:将路由守卫(
beforeLoad+redirect)和 UI 权限控制(条件渲染)结合起来:
beforeLoad阻止越权访问整个页面(安全层)- 条件渲染控制按钮和菜单的可见性(体验层)
- 服务器的 API 鉴权才是一道真正的防线(安全底线)
错误处理
区分 redirect 和真实错误
import { isRedirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ context }) => {
try {
const isValid = await context.auth.verifyToken()
if (!isValid) {
throw redirect({ to: '/login' })
}
} catch (err) {
if (isRedirect(err)) throw err // 透传 redirect
// 处理真实错误(网络故障等)
console.error('Auth check failed:', err)
throw redirect({ to: '/error' })
}
},
})完整项目结构
src/
├── auth.tsx # AuthProvider + useAuth Hook
├── router.tsx # createRouter + 类型注册
├── App.tsx # AuthProvider > RouterProvider
└── routes/
├── __root.tsx # createRootRouteWithContext
├── index.tsx # 公开首页
├── login.tsx # 登录页(反向守卫)
├── unauthorized.tsx # 无权限页面
├── _authenticated.tsx # 认证守卫布局
├── _authenticated/
│ ├── dashboard.tsx
│ ├── profile.tsx
│ ├── _admin.tsx # 管理员守卫布局
│ ├── _admin/
│ │ └── users.tsx
│ └── settings.tsx
└── $.tsx # 404认证守卫 vs UI 权限控制 vs 后端鉴权
| 层级 | 实现方式 | 目的 | 安全等级 |
|---|---|---|---|
| 路由守卫 | beforeLoad + redirect |
阻止越权访问页面 | 中(客户端可绕过) |
| UI 权限 | 条件渲染 | 隐藏不可用的操作 | 低(纯展示层) |
| API 鉴权 | 后端中间件 + JWT/Token | 真正阻止未授权操作 | 高(安全基线) |
💡 最佳实践:三层都要有。路由守卫提供良好的用户体验(不会闪现无权限页面),UI 权限避免困惑,API 鉴权是真正的安全防线。永远不要仅依赖前两层。