auth-and-guards
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAuth and Guards
认证与守卫
This skill covers the routing side of auth. Route guards are UX and navigation control; the data/API boundary still belongs in the server function, server route, or API endpoint that reads or mutates private data. For the server-side primitives — session cookies (/HttpOnly/Secure),SameSite-style helpers, OAuthuseSession+ PKCE, password-reset enumeration defense, CSRF, rate limiting — see start-core/auth-server-primitives.stateCRITICAL: A route guard () does NOT protect abeforeLoaddeclared on that route. Server functions are API endpoints reachable independently of the route that calls them. See "Route guards do not protect server functions" below.createServerFn
本技能涵盖认证的路由层面。 路由守卫负责用户体验与导航控制;数据/API边界仍然属于读取或修改私有数据的服务器函数、服务器路由或API端点。关于服务端原语——会话Cookie(/HttpOnly/Secure)、SameSite风格的辅助函数、OAuthuseSession+ PKCE、密码重置枚举防御、CSRF、速率限制——请参见 start-core/auth-server-primitives。state重要提示:路由守卫()不会保护在该路由上声明的beforeLoad。服务器函数是独立于调用它们的路由而可访问的API端点。请参见下文“路由守卫不保护服务器函数”。createServerFn
Setup
安装设置
Protect routes with + in a pathless layout route ():
beforeLoadredirect()_authenticatedtsx
// 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 defaults to Outlet — no need to declare it
})Any route file placed under is automatically protected:
src/routes/_authenticated/tsx
// 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 <div>Welcome, {auth.user?.username}</div>
}在无路径布局路由()中使用 + 保护路由:
_authenticatedbeforeLoadredirect()tsx
// 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 defaults to Outlet — no need to declare it
})放置在 下的任何路由文件都会自动受到保护:
src/routes/_authenticated/tsx
// 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 <div>Welcome, {auth.user?.username}</div>
}Core Patterns
核心模式
Router Context for Auth State
用于认证状态的路由器上下文
Auth state flows into the router via and 's prop:
createRootRouteWithContextRouterProvidercontexttsx
// 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) => Promise<void>
logout: () => void
}
interface MyRouterContext {
auth: AuthState
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
context: {
auth: undefined!, // placeholder — filled by RouterProvider context prop
},
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}tsx
// src/App.tsx
import { RouterProvider } from '@tanstack/react-router'
import { AuthProvider, useAuth } from './auth'
import { router } from './router'
function InnerApp() {
const auth = useAuth()
// context prop injects live auth state WITHOUT recreating the router
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}The router is created once with a placeholder. 's prop injects the live auth state on each render — this avoids recreating the router on auth changes (which would reset caches and rebuild the route tree).
RouterProvidercontext认证状态通过 和 的 属性流入路由器:
createRootRouteWithContextRouterProvidercontexttsx
// 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) => Promise<void>
logout: () => void
}
interface MyRouterContext {
auth: AuthState
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
component: () => <Outlet />,
})tsx
// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({
routeTree,
context: {
auth: undefined!, // placeholder — filled by RouterProvider context prop
},
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}tsx
// src/App.tsx
import { RouterProvider } from '@tanstack/react-router'
import { AuthProvider, useAuth } from './auth'
import { router } from './router'
function InnerApp() {
const auth = useAuth()
// context prop injects live auth state WITHOUT recreating the router
return <RouterProvider router={router} context={{ auth }} />
}
function App() {
return (
<AuthProvider>
<InnerApp />
</AuthProvider>
)
}路由器使用占位符一次性创建。 的 属性在每次渲染时注入实时认证状态——这避免了在认证状态变化时重新创建路由器(否则会重置缓存并重建路由树)。
RouterProvidercontextRedirect-Based Auth with Redirect-Back
带回跳的重定向式认证
Save the current location in search params so you can redirect back after login:
tsx
// 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 },
})
}
},
})tsx
// src/routes/login.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState, type FormEvent } from 'react'
// Validate redirect target to prevent open redirect attacks
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
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign In</button>
</form>
)
}将当前位置保存在搜索参数中,以便登录后可以重定向回原页面:
tsx
// 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 },
})
}
},
})tsx
// src/routes/login.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState, type FormEvent } from 'react'
// Validate redirect target to prevent open redirect attacks
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
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign In</button>
</form>
)
}Non-Redirect Auth (Inline Login)
非重定向式认证(内联登录)
Instead of redirecting, show a login form in place of the :
Outlettsx
// 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 />
}This keeps the URL unchanged — the user stays on the same page and sees a login form instead of protected content. After authentication, renders and child routes appear.
<Outlet />无需重定向,直接用登录表单替换 的位置:
Outlettsx
// 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 />
}这种方式会保持URL不变——用户停留在同一页面,看到的是登录表单而非受保护的内容。认证通过后, 会渲染,子路由随之显示。
<Outlet />RBAC with Roles and Permissions
基于角色与权限的RBAC
Extend auth state with role/permission helpers, then check in :
beforeLoadtsx
// 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) => Promise<void>
logout: () => void
}Admin-only layout route:
tsx
// 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 },
})
}
},
})Multi-role access:
tsx
// 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 },
})
}
},
})Permission-based:
tsx
// 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 },
})
}
},
})Page-level permission check (nested under an already-role-protected layout):
tsx
// 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>
<h1>User Management</h1>
{canDelete && <button>Delete User</button>}
</div>
)
}使用角色/权限辅助方法扩展认证状态,然后在 中进行检查:
beforeLoadtsx
// 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) => Promise<void>
logout: () => void
}仅管理员可访问的布局路由:
tsx
// 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 },
})
}
},
})多角色访问:
tsx
// 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 },
})
}
},
})基于权限的控制:
tsx
// 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 },
})
}
},
})页面级权限检查(嵌套在已受角色保护的布局下):
tsx
// 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>
<h1>User Management</h1>
{canDelete && <button>Delete User</button>}
</div>
)
}Handling Auth Check Failures (isRedirect)
处理认证检查失败(isRedirect)
When has a try/catch, redirects (which work by throwing) can get swallowed. Use to re-throw:
beforeLoadisRedirecttsx
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 // re-throw redirect, don't swallow it
// Actual error — redirect to login
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})当 中使用 try/catch 时,重定向(通过抛出异常实现)可能会被捕获吞掉。使用 重新抛出重定向:
beforeLoadisRedirecttsx
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 // re-throw redirect, don't swallow it
// Actual error — redirect to login
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})Common Mistakes
常见错误
CRITICAL: Route guards do not protect server functions
严重:路由守卫不保护服务器函数
A redirect protects the route's UI, not the server functions declared on it. produces an RPC endpoint reachable directly with its declared HTTP method regardless of which route renders the calling UI. An attacker doesn't have to load — they can call this GET RPC endpoint directly.
beforeLoadcreateServerFn/_authenticated/orderstsx
// WRONG — handler has no auth check; the route guard doesn't help
import { createServerFn } from '@tanstack/react-start'
import { createFileRoute, redirect } from '@tanstack/react-router'
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
return db.orders.findMany() // ← anyone can hit the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
loader: () => getMyOrders(),
})tsx
// CORRECT — auth enforced on the handler itself, via middleware
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 } })
})Rule of thumb: every , server route, or API endpoint that touches user data needs (or an equivalent in-handler check). The route guard is for the page experience; the endpoint guard is for the data. See start-core/auth-server-primitives for the full session/middleware pattern.
createServerFnauthMiddlewarebeforeLoadcreateServerFn/_authenticated/orderstsx
// WRONG — handler has no auth check; the route guard doesn't help
import { createServerFn } from '@tanstack/react-start'
import { createFileRoute, redirect } from '@tanstack/react-router'
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
return db.orders.findMany() // ← anyone can hit the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
},
loader: () => getMyOrders(),
})tsx
// CORRECT — auth enforced on the handler itself, via middleware
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 } })
})经验法则:每个涉及用户数据的 、服务器路由或API端点都需要 (或等效的处理函数内检查)。路由守卫用于页面体验;端点守卫用于数据安全。完整的会话/中间件模式请参见 start-core/auth-server-primitives。
createServerFnauthMiddlewareCRITICAL: The anonymous destination can still disclose protected data
严重:匿名目标页面仍可能泄露受保护数据
Protect the entire anonymous response, not only the API call. A public login or unauthorized page still leaks data if its title, copy, search params, or serialized loader state names the protected user, tenant, record, or resource. Test a direct anonymous request and follow redirects. Assert that the handler rejects before reading private data, no protected loader runs, the final HTML and serialized state contain no protected identity, and the redirect contains only a sanitized relative return URL.
要保护整个匿名响应,而不仅仅是API调用。如果公开的登录或未授权页面的标题、文案、搜索参数或序列化的loader状态中包含受保护的用户、租户、记录或资源信息,仍然会造成数据泄露。请测试直接的匿名请求并跟踪重定向。确保处理函数在读取私有数据前就拒绝请求、没有受保护的loader运行、最终的HTML和序列化状态中不包含受保护的身份信息,并且重定向仅包含经过清理的相对返回URL。
HIGH: Auth check in component instead of beforeLoad
高危:在组件而非beforeLoad中进行认证检查
Component-level auth checks cause a flash of protected content before the redirect:
tsx
// WRONG — protected content renders briefly before redirect
export const Route = createFileRoute('/_authenticated/dashboard')({
component: () => {
const auth = useAuth()
if (!auth.isAuthenticated) return <Navigate to="/login" />
return <Dashboard />
},
})
// CORRECT — beforeLoad runs before any rendering
export const Route = createFileRoute('/_authenticated/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
component: Dashboard,
})组件级别的认证检查会导致重定向前出现受保护内容闪烁的问题:
tsx
// WRONG — protected content renders briefly before redirect
export const Route = createFileRoute('/_authenticated/dashboard')({
component: () => {
const auth = useAuth()
if (!auth.isAuthenticated) return <Navigate to="/login" />
return <Dashboard />
},
})
// CORRECT — beforeLoad runs before any rendering
export const Route = createFileRoute('/_authenticated/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
component: Dashboard,
})HIGH: Not re-throwing redirects in try/catch
高危:在try/catch中未重新抛出重定向
redirect()beforeLoadtsx
// WRONG — redirect is caught and swallowed
beforeLoad: async ({ context }) => {
try {
await validateSession(context.auth)
} catch (e) {
console.error(e) // swallows the redirect!
}
}
// CORRECT — use isRedirect to distinguish intentional redirects from errors
import { isRedirect } from '@tanstack/react-router'
beforeLoad: async ({ context }) => {
try {
await validateSession(context.auth)
} catch (e) {
if (isRedirect(e)) throw e
console.error(e)
}
}redirect()beforeLoadtsx
// WRONG — redirect is caught and swallowed
beforeLoad: async ({ context }) => {
try {
await validateSession(context.auth)
} catch (e) {
console.error(e) // swallows the redirect!
}
}
// CORRECT — use isRedirect to distinguish intentional redirects from errors
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: Conditionally rendering root route component
中危:条件性渲染根路由组件
The root route always renders regardless of auth state. You cannot conditionally render its component:
tsx
// WRONG — root route always renders, this doesn't protect anything
export const Route = createRootRoute({
component: () => {
if (!isAuthenticated()) return <Login />
return <Outlet />
},
})
// CORRECT — use a pathless layout route for auth boundaries
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
})Place protected routes as children of the layout route. Public routes (login, home, etc.) live outside it.
_authenticated无论认证状态如何,根路由始终会渲染。你不能条件性地渲染它的组件:
tsx
// WRONG — root route always renders, this doesn't protect anything
export const Route = createRootRoute({
component: () => {
if (!isAuthenticated()) return <Login />
return <Outlet />
},
})
// CORRECT — use a pathless layout route for auth boundaries
// src/routes/_authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login' })
}
},
})将受保护的路由作为 布局路由的子路由。公开路由(登录、首页等)放在它的外面。
_authenticatedCross-References
交叉参考
- See also: router-core/data-loading/SKILL.md — runs before
beforeLoad; auth context flows into loader via route contextloader - See also: start-core/auth-server-primitives/SKILL.md — server-side session cookies, OAuth state + PKCE, CSRF, password-reset hardening, rate limiting (the server half of authentication)
- See also: start-core/middleware/SKILL.md — factory pattern for protecting individual
authMiddlewarecallscreateServerFn
- 另见:router-core/data-loading/SKILL.md —— 在
beforeLoad之前运行;认证上下文通过路由上下文流入loaderloader - 另见:start-core/auth-server-primitives/SKILL.md —— 服务端会话Cookie、OAuth state + PKCE、CSRF、密码重置加固、速率限制(认证的服务端部分)
- 另见:start-core/middleware/SKILL.md —— 用于保护单个 调用的
createServerFn工厂模式authMiddleware