Skip to content
认证与路由守卫

认证与路由守卫

TanStack Router 通过 beforeLoadcontextredirect() 提供了灵活的认证与授权机制。推荐的架构是三层模式: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>
  )
}

🚨 陷阱createRoutercontext.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 鉴权是真正的安全防线。永远不要仅依赖前两层。