middleware-protection
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMiddleware Route Protection
中间件路由保护
Check auth once, protect routes declaratively.
只需验证一次身份,即可声明式地保护路由。
When to Use This Skill
何时使用该方案
- Need to protect multiple routes
- Want centralized auth checking
- Tired of repeating auth logic in every route
- Need role-based access control
- 需要保护多个路由
- 希望集中处理身份验证检查
- 厌倦了在每个路由中重复编写身份验证逻辑
- 需要基于角色的访问控制
Core Concepts
核心概念
- Middleware intercepts - All requests pass through middleware
- Declarative routes - Define protected/public routes in config
- Session refresh - Keep sessions alive automatically
- Consistent errors - API routes get JSON, pages get redirects
- 中间件拦截 - 所有请求都会经过中间件
- 声明式路由 - 在配置中定义受保护/公共路由
- 会话刷新 - 自动保持会话活跃
- 统一错误处理 - API路由返回JSON,页面执行重定向
TypeScript Implementation
TypeScript实现
middleware.ts
middleware.ts
typescript
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
// Routes that require authentication
const PROTECTED_ROUTES = [
'/dashboard',
'/settings',
'/api/user',
'/api/predictions',
];
// Routes that are always public
const PUBLIC_ROUTES = [
'/',
'/login',
'/signup',
'/api/health',
'/api/public',
];
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Skip static files and Next.js internals
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/favicon') ||
pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico)$/)
) {
return NextResponse.next();
}
// Skip explicitly public routes
if (PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route + '/'))) {
return NextResponse.next();
}
// Create response that we'll modify
let response = NextResponse.next({ request });
// Create Supabase client with cookie handling
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
response = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
// Refresh session (important for SSR)
const { data: { user } } = await supabase.auth.getUser();
// Check if route requires auth
const requiresAuth = PROTECTED_ROUTES.some(route =>
pathname === route || pathname.startsWith(route + '/')
);
if (requiresAuth && !user) {
// API routes: return 401 JSON
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{
error: 'Authentication required',
code: 'AUTH_REQUIRED',
loginUrl: '/login',
},
{ status: 401 }
);
}
// Pages: redirect to login with return URL
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('redirectTo', pathname);
return NextResponse.redirect(url);
}
// Add user ID to headers for downstream use
if (user) {
response.headers.set('x-user-id', user.id);
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};typescript
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
// Routes that require authentication
const PROTECTED_ROUTES = [
'/dashboard',
'/settings',
'/api/user',
'/api/predictions',
];
// Routes that are always public
const PUBLIC_ROUTES = [
'/',
'/login',
'/signup',
'/api/health',
'/api/public',
];
export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Skip static files and Next.js internals
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/favicon') ||
pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico)$/)
) {
return NextResponse.next();
}
// Skip explicitly public routes
if (PUBLIC_ROUTES.some(route => pathname === route || pathname.startsWith(route + '/'))) {
return NextResponse.next();
}
// Create response that we'll modify
let response = NextResponse.next({ request });
// Create Supabase client with cookie handling
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
response = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
);
},
},
}
);
// Refresh session (important for SSR)
const { data: { user } } = await supabase.auth.getUser();
// Check if route requires auth
const requiresAuth = PROTECTED_ROUTES.some(route =>
pathname === route || pathname.startsWith(route + '/')
);
if (requiresAuth && !user) {
// API routes: return 401 JSON
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{
error: 'Authentication required',
code: 'AUTH_REQUIRED',
loginUrl: '/login',
},
{ status: 401 }
);
}
// Pages: redirect to login with return URL
const url = request.nextUrl.clone();
url.pathname = '/login';
url.searchParams.set('redirectTo', pathname);
return NextResponse.redirect(url);
}
// Add user ID to headers for downstream use
if (user) {
response.headers.set('x-user-id', user.id);
}
return response;
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};Using User ID in API Routes
在API路由中使用用户ID
typescript
// app/api/user/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerSupabaseClient } from '@/lib/supabase-server';
export async function GET(request: NextRequest) {
// User ID was added by middleware
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const supabase = await createServerSupabaseClient();
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', userId)
.single();
return NextResponse.json({ profile });
}typescript
// app/api/user/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createServerSupabaseClient } from '@/lib/supabase-server';
export async function GET(request: NextRequest) {
// User ID was added by middleware
const userId = request.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const supabase = await createServerSupabaseClient();
const { data: profile } = await supabase
.from('user_profiles')
.select('*')
.eq('id', userId)
.single();
return NextResponse.json({ profile });
}Role-Based Protection
基于角色的保护
typescript
// middleware.ts (extended)
const ROUTE_ROLES: Record<string, string[]> = {
'/admin': ['admin'],
'/dashboard': ['user', 'admin'],
'/api/admin': ['admin'],
};
// After getting user, check role
if (user) {
const userRole = user.user_metadata?.role || 'user';
const requiredRoles = Object.entries(ROUTE_ROLES)
.find(([route]) => pathname.startsWith(route))?.[1];
if (requiredRoles && !requiredRoles.includes(userRole)) {
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{ error: 'Forbidden', code: 'FORBIDDEN' },
{ status: 403 }
);
}
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}typescript
// middleware.ts (extended)
const ROUTE_ROLES: Record<string, string[]> = {
'/admin': ['admin'],
'/dashboard': ['user', 'admin'],
'/api/admin': ['admin'],
};
// After getting user, check role
if (user) {
const userRole = user.user_metadata?.role || 'user';
const requiredRoles = Object.entries(ROUTE_ROLES)
.find(([route]) => pathname.startsWith(route))?.[1];
if (requiredRoles && !requiredRoles.includes(userRole)) {
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{ error: 'Forbidden', code: 'FORBIDDEN' },
{ status: 403 }
);
}
return NextResponse.redirect(new URL('/unauthorized', request.url));
}
}Pattern-Based Routes
基于模式的路由
typescript
// For more complex route matching
const PROTECTED_PATTERNS = [
/^\/dashboard(\/.*)?$/,
/^\/api\/user\/.*$/,
/^\/settings$/,
/^\/api\/v\d+\/private\/.*/, // /api/v1/private/*, /api/v2/private/*
];
const requiresAuth = PROTECTED_PATTERNS.some(pattern =>
pattern.test(pathname)
);typescript
// For more complex route matching
const PROTECTED_PATTERNS = [
/^\/dashboard(\/.*)?$/,
/^\/api\/user\/.*$/,
/^\/settings$/,
/^\/api\/v\d+\/private\/.*/, // /api/v1/private/*, /api/v2/private/*
];
const requiresAuth = PROTECTED_PATTERNS.some(pattern =>
pattern.test(pathname)
);Python Implementation (FastAPI)
Python实现(FastAPI)
python
undefinedpython
undefinedmiddleware/auth.py
middleware/auth.py
from fastapi import Request, HTTPException
from fastapi.responses import RedirectResponse
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Set
PROTECTED_ROUTES: Set[str] = {
"/dashboard",
"/settings",
"/api/user",
}
PUBLIC_ROUTES: Set[str] = {
"/",
"/login",
"/signup",
"/api/health",
}
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Skip public routes
if path in PUBLIC_ROUTES or any(path.startswith(r + "/") for r in PUBLIC_ROUTES):
return await call_next(request)
# Check if route requires auth
requires_auth = path in PROTECTED_ROUTES or any(
path.startswith(r + "/") for r in PROTECTED_ROUTES
)
if not requires_auth:
return await call_next(request)
# Get user from session/token
user = await get_user_from_request(request)
if not user:
if path.startswith("/api/"):
raise HTTPException(
status_code=401,
detail={
"error": "Authentication required",
"code": "AUTH_REQUIRED",
"login_url": "/login",
}
)
return RedirectResponse(f"/login?redirectTo={path}")
# Add user to request state
request.state.user = user
request.state.user_id = user.id
return await call_next(request)async def get_user_from_request(request: Request):
"""Extract and validate user from request."""
token = request.cookies.get("session") or request.headers.get("Authorization")
if not token:
return None
# Validate token and return user
return await validate_session(token)
```pythonfrom fastapi import Request, HTTPException
from fastapi.responses import RedirectResponse
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Set
PROTECTED_ROUTES: Set[str] = {
"/dashboard",
"/settings",
"/api/user",
}
PUBLIC_ROUTES: Set[str] = {
"/",
"/login",
"/signup",
"/api/health",
}
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Skip public routes
if path in PUBLIC_ROUTES or any(path.startswith(r + "/") for r in PUBLIC_ROUTES):
return await call_next(request)
# Check if route requires auth
requires_auth = path in PROTECTED_ROUTES or any(
path.startswith(r + "/") for r in PROTECTED_ROUTES
)
if not requires_auth:
return await call_next(request)
# Get user from session/token
user = await get_user_from_request(request)
if not user:
if path.startswith("/api/"):
raise HTTPException(
status_code=401,
detail={
"error": "Authentication required",
"code": "AUTH_REQUIRED",
"login_url": "/login",
}
)
return RedirectResponse(f"/login?redirectTo={path}")
# Add user to request state
request.state.user = user
request.state.user_id = user.id
return await call_next(request)async def get_user_from_request(request: Request):
"""Extract and validate user from request."""
token = request.cookies.get("session") or request.headers.get("Authorization")
if not token:
return None
# Validate token and return user
return await validate_session(token)
```pythonUsing in routes
在路由中使用
from fastapi import Request, Depends
@app.get("/api/user/profile")
async def get_profile(request: Request):
user_id = request.state.user_id
profile = await db.get_profile(user_id)
return {"profile": profile}
undefinedfrom fastapi import Request, Depends
@app.get("/api/user/profile")
async def get_profile(request: Request):
user_id = request.state.user_id
profile = await db.get_profile(user_id)
return {"profile": profile}
undefinedError Response Format
错误响应格式
typescript
// Consistent error format for API routes
interface AuthError {
error: string;
code: 'AUTH_REQUIRED' | 'SESSION_EXPIRED' | 'FORBIDDEN';
message?: string;
loginUrl: string;
}
// 401 - Not authenticated
{
"error": "Authentication required",
"code": "AUTH_REQUIRED",
"loginUrl": "/login"
}
// 403 - Authenticated but not authorized
{
"error": "Forbidden",
"code": "FORBIDDEN",
"message": "Admin access required"
}typescript
// Consistent error format for API routes
interface AuthError {
error: string;
code: 'AUTH_REQUIRED' | 'SESSION_EXPIRED' | 'FORBIDDEN';
message?: string;
loginUrl: string;
}
// 401 - 未认证
{
"error": "Authentication required",
"code": "AUTH_REQUIRED",
"loginUrl": "/login"
}
// 403 - 已认证但未授权
{
"error": "Forbidden",
"code": "FORBIDDEN",
"message": "Admin access required"
}Best Practices
最佳实践
- Refresh sessions - Call in middleware to refresh
getUser() - Pass user ID - Add to headers for downstream routes
- JSON for APIs - Never redirect API routes
- Return URL - Include param for login redirects
redirectTo - Skip static - Don't process static files
- 刷新会话 - 在中间件中调用以刷新会话
getUser() - 传递用户ID - 将用户ID添加到请求头,供下游路由使用
- API返回JSON - 切勿重定向API路由
- 返回URL - 在登录重定向中包含参数
redirectTo - 跳过静态资源 - 不要对静态文件进行身份验证处理
Common Mistakes
常见误区
- Redirecting API routes (should return 401 JSON)
- Not refreshing session in middleware
- Processing static files through auth
- Missing return URL on login redirect
- Not handling role-based access
- 重定向API路由(应返回401 JSON)
- 未在中间件中刷新会话
- 对静态文件进行身份验证处理
- 登录重定向时缺少返回URL
- 未处理基于角色的访问控制
Related Skills
相关方案
- Supabase Auth
- JWT Auth
- Row Level Security
- Supabase Auth
- JWT Auth
- Row Level Security