中间件与认证模式

学习 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 认证中间件实现

查看实现:app/middleware/auth.ts

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);
  };
}

代码解析

  1. Token 提取

    • 优先从 Cookie 获取(sb-access-token
    • 如果没有,从 Authorization header 获取(兼容性)
    • 格式:Bearer <token>
  2. Token 验证

    • 使用 Supabase getUser() 方法验证
    • 获取用户信息和元数据
  3. 认证客户端

    • 创建带有 Token 的 Supabase 客户端
    • 用于后续的数据库操作(支持 RLS 策略)
  4. 响应处理

    • 认证失败 → 返回 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 管理与存储

设置 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 };
}

💡 练习题

  1. 选择题:本项目的认证中间件叫什么?

    • A. authenticate()
    • B. withAuth()
    • C. protect()
    • D. authMiddleware()
  2. 代码题:创建一个 API Route,使用 withAuth 保护。

  3. 分析题:查看本项目的 app/middleware/auth.ts,说明它如何提取和验证 Token。

  4. 实践题:实现一个用户注册和登录的完整流程。


📚 参考资源

官方文档

本项目相关文件


✅ 总结

中间件

  • 请求拦截和处理
  • 认证和授权
  • 日志和监控

本项目的认证模式

  • 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 });
});

下一步:阅读下一篇文章《企业级分层架构》,学习完整的项目架构设计。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page