中间件与认证模式
学习 Next.js 中间件的使用,实现路由拦截与用户认证功能。
📚 学习目标
学完这篇文章后,你将能够:
- 理解 Next.js 中间件的作用
- 掌握本项目的
withAuth认证模式 - 学会保护 API Routes
- 了解 Token 验证流程
- 掌握认证最佳实践
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- HTTP 请求和响应
- Token 认证概念
- Cookie 和 Header
1️⃣ 中间件概述
1.1 什么是中间件?
中间件是在请求到达处理器之前执行的函数,用于拦截和处理请求。
中间件的作用:
- 🔐 认证和授权
- 📝 日志记录
- 🚫 请求拦截
- 🔧 请求修改
1.2 Next.js 中间件类型
| 类型 | 文件 | 作用范围 |
|---|---|---|
| 全局中间件 | middleware.ts | 所有请求 |
| 包装器中间件 | 自定义函数 | 特定 API Routes |
本项目选择:包装器中间件 withAuth()
2️⃣ 本项目的认证中间件
2.1 认证流程图
2.2 认证中间件实现
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
// 初始化 Supabase 客户端(用于服务端鉴权)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey =
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!;
const supabase = createClient(supabaseUrl, supabaseAnonKey);
// Cookie 名称
const COOKIE_NAME = 'sb-access-token';
/**
* 认证用户信息接口
*/
export interface AuthUser {
id: string;
email: string;
[key: string]: any;
}
/**
* 认证结果接口
*/
export interface AuthResult {
user: AuthUser | null;
token: string | null;
client: any | null;
error?: string;
}
/**
* 从请求中提取和验证 token
* 优先从 cookie 读取,如果没有则从 Authorization header 读取
* 返回用户信息、token 和认证后的客户端
*
* @param request - Next.js 请求对象
* @returns 认证结果
*/
export async function authenticateRequest(
request: NextRequest,
): Promise<AuthResult> {
try {
// 1. 优先从 cookie 获取 token
let token = request.cookies.get(COOKIE_NAME)?.value;
// 2. 如果 cookie 中没有,尝试从 Authorization header 获取(兼容旧客户端)
if (!token) {
const authHeader = request.headers.get('authorization');
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
}
if (!token) {
return {
user: null,
token: null,
client: null,
error: '缺少认证 token',
};
}
// 3. 验证 token 并获取用户信息
const { data, error } = await supabase.auth.getUser(token);
if (error || !data.user) {
return {
user: null,
token: null,
client: null,
error: 'Token 无效或已过期',
};
}
// 4. 创建带有认证的 Supabase 客户端(用于 RLS 策略)
const authenticatedClient = createClient(supabaseUrl, supabaseAnonKey, {
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
});
// 5. 返回认证结果
return {
user: {
id: data.user.id,
email: data.user.email || '',
...data.user.user_metadata,
},
token,
client: authenticatedClient,
};
} catch (error) {
console.error('认证过程出错:', error);
return {
user: null,
token: null,
client: null,
error: '认证过程出错',
};
}
}
/**
* 创建未授权响应
*/
export function unauthorizedResponse(message: string = '未授权') {
return NextResponse.json({ error: message }, { status: 401 });
}
/**
* withAuth 是 createAuthMiddleware 的语义化包装
* 用于路由层"包裹"业务逻辑,实现统一鉴权
*/
export function withAuth(handler: AuthedHandler) {
return async (request: NextRequest): Promise<Response> => {
// 执行认证
const auth = await authenticateRequest(request);
// 如果认证失败,返回 401
if (!auth.user) {
return unauthorizedResponse(auth.error || '未授权');
}
// 认证成功,调用处理器
return handler(request, auth);
};
}代码解析:
-
Token 提取:
- 优先从 Cookie 获取(
sb-access-token) - 如果没有,从
Authorizationheader 获取(兼容性) - 格式:
Bearer <token>
- 优先从 Cookie 获取(
-
Token 验证:
- 使用 Supabase
getUser()方法验证 - 获取用户信息和元数据
- 使用 Supabase
-
认证客户端:
- 创建带有 Token 的 Supabase 客户端
- 用于后续的数据库操作(支持 RLS 策略)
-
响应处理:
- 认证失败 → 返回 401
- 认证成功 → 调用 handler
3️⃣ 使用 withAuth 保护 API
3.1 基本用法
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@/app/middleware/auth';
/**
* GET /api/user
* 获取当前用户信息
*/
export const GET = withAuth(async (request: NextRequest, auth) => {
return NextResponse.json({ user: auth.user });
});
/**
* POST /api/user
* 更新用户信息
*/
export const POST = withAuth(async (request: NextRequest, auth) => {
const body = await request.json();
// 更新用户信息...
return NextResponse.json({ success: true });
});3.2 本项目的实际应用
查看会话管理 API:app/api/chat/sessions/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { sessionService } from '@/app/services';
import { withAuth } from '@/app/middleware/auth';
import type { SessionType } from '@/app/database/sessions';
/**
* GET /api/chat/sessions
* 获取当前用户的所有会话列表
*/
export const GET = withAuth(async (request: NextRequest, auth) => {
try {
const { searchParams } = new URL(request.url);
const type = searchParams.get('type') as SessionType | undefined;
// 使用认证客户端获取会话列表
const sessions = await sessionService.getAllSessions(auth.client, type);
return NextResponse.json({ sessions });
} catch (e) {
return NextResponse.json(
{ error: '获取会话列表失败', detail: String(e) },
{ status: 500 },
);
}
});
/**
* POST /api/chat/sessions
* 创建新会话
*/
export const POST = withAuth(async (request: NextRequest, auth) => {
try {
const { name, type = 'chat' } = await request.json();
const result = await sessionService.createSession(
{ name, type },
auth.user.id,
auth.client,
);
return NextResponse.json(result);
} catch (e) {
return NextResponse.json(
{ error: '新建会话失败', detail: String(e) },
{ status: 500 },
);
}
});说明:
- 所有方法都用
withAuth包装 - Handler 接收
auth参数(包含 user、token、client) - 使用
auth.client进行数据库操作(自动携带认证信息)
4️⃣ Token 管理与存储
4.1 Cookie 存储(推荐)
设置 Cookie:
// app/api/auth/login/route.ts
export async function POST(request: Request) {
const { email, password } = await request.json();
// 登录 Supabase
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
return NextResponse.json({ error: error.message }, { status: 401 });
}
// 设置 Cookie
const response = NextResponse.json({ user: data.user });
response.cookies.set('sb-access-token', data.session.access_token, {
httpOnly: true, // 客户端 JS 无法访问
secure: process.env.NODE_ENV === 'production', // HTTPS 时设置
sameSite: 'strict', // CSRF 保护
maxAge: 60 * 60 * 24 * 7, // 7 天
path: '/',
});
return response;
}删除 Cookie:
// app/api/auth/logout/route.ts
export async function POST(request: Request) {
const response = NextResponse.json({ success: true });
response.cookies.delete('sb-access-token');
return response;
}4.2 Header 存储(兼容性)
客户端设置 Header:
'use client';
export default function ApiExample() {
const fetchData = async () => {
const token = localStorage.getItem('token');
const response = await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
const data = await response.json();
console.log(data);
};
return <button onClick={fetchData}>获取数据</button>;
}4.3 本项目的 Token 管理
认证上下文:app/contexts/AuthContext.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
interface User {
id: string;
email: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
// 检查认证状态
checkAuth();
}, []);
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/me');
if (response.ok) {
const data = await response.json();
setUser(data.user);
}
} catch (error) {
console.error('认证检查失败:', error);
} finally {
setLoading(false);
}
};
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('登录失败');
}
await checkAuth();
router.push('/');
};
const logout = async () => {
await fetch('/api/auth/logout', { method: 'POST' });
setUser(null);
router.push('/login');
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}5️⃣ 认证最佳实践
5.1 安全性检查清单
- ✅ Token 存储在 HttpOnly Cookie
- ✅ 使用 HTTPS(生产环境)
- ✅ 设置
sameSite: strict防止 CSRF - ✅ Token 有过期时间
- ✅ 服务端验证 Token
- ✅ 不在前端暴露敏感数据
5.2 错误处理
export const GET = withAuth(async (request: NextRequest, auth) => {
try {
// 业务逻辑
const data = await someOperation(auth.client);
return NextResponse.json({ data });
} catch (error) {
if (error instanceof UnauthorizedError) {
return NextResponse.json({ error: '未授权' }, { status: 401 });
}
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
}
});5.3 性能优化
缓存认证结果:
// 简单的内存缓存(生产环境建议使用 Redis)
const authCache = new Map<string, { user: AuthUser; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
export async function authenticateRequest(
request: NextRequest,
): Promise<AuthResult> {
const token = request.cookies.get('sb-access-token')?.value;
if (!token) {
return { user: null, token: null, client: null, error: '缺少 token' };
}
// 检查缓存
const cached = authCache.get(token);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return { user: cached.user, token, client: null };
}
// 验证 Token
const { data, error } = await supabase.auth.getUser(token);
if (error || !data.user) {
return { user: null, token: null, client: null, error: 'Token 无效' };
}
// 更新缓存
authCache.set(token, { user: data.user, timestamp: Date.now() });
return { user: data.user, token, client: null };
}💡 练习题
-
选择题:本项目的认证中间件叫什么?
- A. authenticate()
- B. withAuth()
- C. protect()
- D. authMiddleware()
-
代码题:创建一个 API Route,使用
withAuth保护。 -
分析题:查看本项目的 app/middleware/auth.ts,说明它如何提取和验证 Token。
-
实践题:实现一个用户注册和登录的完整流程。
📚 参考资源
官方文档
本项目相关文件
- app/middleware/auth.ts - 认证中间件
- app/contexts/AuthContext.tsx - 认证上下文
- app/components/ProtectedRoute.tsx - 路由保护
✅ 总结
中间件:
- 请求拦截和处理
- 认证和授权
- 日志和监控
本项目的认证模式:
withAuth()包装器- 优先从 Cookie 获取 Token
- Supabase Token 验证
- 返回 401 如果认证失败
Token 管理:
- Cookie 存储(推荐)
- Header 存储(兼容)
- HttpOnly Cookie 安全性
- Token 过期时间
使用方法:
export const GET = withAuth(async (request, auth) => {
// auth.user, auth.token, auth.client
return NextResponse.json({ data });
});下一步:阅读下一篇文章《企业级分层架构》,学习完整的项目架构设计。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。