Next.js API 代理模式深度解析:为什么需要 /api/client-proxy?

·笔记

引言:从一个 308 重定向说起

在部署一个 Next.js + FastAPI 全栈项目时,我遇到了一个令人困惑的现象:前端页面调用 /api/client-proxy/api/v1/api-keys/ 返回 308 重定向(正常工作),而直接调用 /api/v1/api-keys/ 却返回 403 Forbidden。

为什么同样的后端接口,走不同的路径会得到截然不同的结果?/api/client-proxy/ 这个看似多余的路径究竟扮演了什么角色?

这篇文章将深入剖析这一代理模式的设计动机、实现细节和安全性考量。


一、核心概念:/api/client-proxy 是什么?

简单来说,/api/client-proxy/ 是前端自动生成的 SDK 和 Next.js 服务器之间的一条"秘密通道",它专门处理浏览器端的 API 请求。

它的工作原理可以概括为三步:

  1. 拦截:浏览器端的 SDK 自动检测运行环境,将原本指向后端 API 的请求拦截下来
  2. 重写:剥离后端域名,将路径拼接为 /api/client-proxy/ + 原始 API 路径
  3. 转发:Next.js 服务器收到请求后,附带 HttpOnly Cookie 转发到真正的后端

关键命名约定:路径中的 client-proxy 表明了它的职责——为客户端(浏览器)提供代理服务。这不是 Next.js 的内置功能,而是项目架构中的自定义设计。


二、完整请求链路

以下是一次完整的浏览器 API 调用过程:

用户点击 "获取 API Keys"
        │
        ▼
┌─ 浏览器 ─────────────────────────────────────────────┐
│ SDK 构造请求:                                          │
│   GET http://backend:8000/api/v1/api-keys/            │
│                                                        │
│ clientFetch() 拦截:                                    │
│   1. 检测是浏览器环境 → 走 clientFetch                 │
│   2. 剥离 API_URL 前缀 → api/v1/api-keys/              │
│   3. 拼接代理路径 → /api/client-proxy/api/v1/api-keys/ │
│   4. 发请求到同源地址(无跨域问题)                    │
└───────────────────────────────────────────────────────┘
        │ GET /api/client-proxy/api/v1/api-keys/
        ▼
┌─ Nginx ───────────────────────────────────────────────┐
│ location /api/client-proxy/ → frontend:3000           │
│ (必须在 /api/ 前面,否则被后端拦截)                  │
└───────────────────────────────────────────────────────┘
        │
        ▼
┌─ Next.js 服务器 ──────────────────────────────────────┐
│ [...path]/route.ts (proxyHandler):                    │
│   1. path = ['api', 'v1', 'api-keys']                 │
│   2. 拼接 + 补充尾部斜杠(FastAPI 要求)              │
│   3. 转发到 backend:8000/api/v1/api-keys/             │
│   4. 附带原始 Cookie(serverFetch 注入)              │
│   5. 返回后端响应给浏览器                              │
└───────────────────────────────────────────────────────┘
        │
        ▼
┌─ FastAPI 后端 ────────────────────────────────────────┐
│ 收到带 Cookie 的请求 → 认证通过 → 返回数据            │
└───────────────────────────────────────────────────────┘

两条路径的本质差异

维度 /api/client-proxy/... /api/v1/...
调用方 浏览器端 JS(SDK 自动) 服务端 SSR / curl
Nginx 转发到 frontend:3000(Next.js) backend:8000(FastAPI)
认证方式 Next.js 代理注入 Cookie → 转发后端 后端直接从请求读取 Cookie
典型结果 308 重定向(正常) 403(未登录,无 Cookie)

三、关键代码实现

3.1 SDK 请求拦截 —— clientFetch

浏览器的请求在发出前会被 clientFetch 函数拦截和改写:

// hey-api.ts — 核心拦截逻辑
const clientFetch: FetchFunction = async (request) => {
  const url = new URL(request.url);
  
  // 剥离后端域名,提取纯 API 路径
  const apiPath = url.pathname + url.search;
  
  // 拼接代理路径,让请求走 Next.js 代理
  const proxyUrl = `/api/client-proxy${apiPath}`;
  
  // 构造同源请求(无需处理跨域)
  return fetch(proxyUrl, {
    ...request,
    url: proxyUrl,
  });
};

关键点:通过剥离后端域名并拼接 /api/client-proxy,浏览器发往不同域(如 backend:8000)的请求被改写为同源请求。由于浏览器在同源请求中会自动携带 HttpOnly Cookie,认证信息就自然传递到了 Next.js 服务器。

3.2 服务端请求转发 —— serverFetch

Next.js 服务端(SSR/SSG)不需要走代理,但需要用 serverFetch 将浏览器的 Cookie 手动注入到后端请求中:

// hey-api.ts — 服务端直接调用
const serverFetch: FetchFunction = async (request) => {
  const { cookies } = await import('next/headers');
  const cookieStore = cookies();
  const cookieString = cookieStore.toString();
  
  return fetch(request.url, {
    ...request,
    headers: {
      ...request.headers,
      Cookie: cookieString,  // 手动注入浏览器 Cookie
    },
  });
};

3.3 代理路由处理器 —— route.ts

Next.js App Router 的 catch-all 路由负责接收代理请求并转发:

// app/api/client-proxy/[...path]/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(
  request: NextRequest,
  { params }: { params: { path: string[] } }
) {
  const { path } = params;
  const apiPath = path.join('/');
  
  // 拼接后端地址 + 补充尾部斜杠(FastAPI 路由要求)
  const backendUrl = `${process.env.API_URL}/api/v1/${apiPath}/`;
  
  // 转发原始请求头(包含 Cookie)到后端
  const response = await fetch(backendUrl, {
    headers: {
      'Content-Type': 'application/json',
      Cookie: request.headers.get('cookie') || '',
    },
  });
  
  const data = await response.json();
  return NextResponse.json(data, { status: response.status });
}

3.4 环境判断逻辑

SDK 自动判断当前运行环境,选择合适的请求策略:

// 伪代码体现核心判断逻辑
export function createClient() {
  const fetchFn = typeof window === 'undefined' 
    ? serverFetch   // SSR/服务端:直接调后端 + 注入 Cookie
    : clientFetch;  // 浏览器:走 /api/client-proxy/ 代理
  
  return new ApiClient({ fetch: fetchFn });
}

四、与传统 React 方案的架构对比

方案 A:传统 React(简单但不够安全)

浏览器 localStorage 存 token
        │
        │  Authorization: Bearer xxx  ← JS 手动附加 Header
        ▼
   backend:8000  ← 直接跨域调用(需配置 CORS)
  • Token 存在 localStorageJavaScript 可读可写
  • 每次请求手动在 Header 里添加 Authorization: Bearer xxx
  • 实现简单直接,但 XSS 攻击可轻易窃取 token

方案 B:本项目方案(安全但多一层代理)

浏览器 Cookie (HttpOnly)  ← JS 完全无法读取!
        │
        │  Cookie 自动附带(同源请求)
        ▼
Next.js 服务器 ──→ 转发 + 注入 Cookie ──→ backend:8000
(/api/client-proxy/)
  • JWT 存储在 HttpOnly Cookie 中,浏览器自动发送但 JS 无法读取
  • XSS 攻击无法窃取 token(安全性大幅提升)
  • 代价:浏览器 JS 无法直接带 Cookie 跨域调后端 → 必须通过 Next.js 代理转发

对比总结

维度 传统 React 本项目
Token 存储 localStorage(JS 可读) HttpOnly Cookie(JS 不可读)
浏览器调 API 直接调后端 + 手动加 Header 通过 Next.js 代理转发
XSS 防护 弱(token 可被窃取) 强(token 不可读取)
架构复杂度 简单 多一层代理
CORS 配置 必须配置 同源请求,无需 CORS

五、Nginx 关键配置

Nginx 的 location 指令遵循前缀匹配优先级规则。如果配置不当,代理路径会被普通 /api/ 规则拦截。

正确配置顺序:

# ✅ 长前缀规则必须放在短前缀规则前面
location = /health { ... }                    # 最高优先级(精确匹配)
location /api/client-proxy/ { ... }           # 长前缀,先于 /api/ 匹配
location /api/ { ... }                        # 短前缀
location / { ... }                            # 最低优先级

如果交换 location /api/location /api/client-proxy/ 的顺序,所有以 /api/ 开头的请求(包括代理请求)都会被直接转发到后端,而不会经过 Next.js 代理处理。结果就是:代理路径返回 404 Not Found,因为后端根本不存在 /client-proxy/ 这个路由。


六、这个模式的优势与注意事项

优势

  1. 安全性提升:HttpOnly Cookie 防止 XSS 攻击窃取 Token,这是该模式存在的首要原因
  2. 同源策略简化:浏览器端所有请求变成同源请求,无需处理跨域问题
  3. 关注点分离:SDK 层自动判断环境并选择策略,业务开发者无需关心请求路径
  4. SSR 兼容serverFetch 保证服务端渲染时也能正确携带认证信息

注意事项

  1. 请求延迟增加:多了一层代理转发,每次客户端请求增加一次服务端 → 后端的网络往返
  2. Nginx 配置敏感location 顺序必须正确,否则整个代理机制失效且难以排查
  3. 调试需要区分场景:同样一个 API 接口,浏览器调用和 SSR 调用走的是不同的代码路径,排查问题时要先确认运行环境
  4. Next.js 服务器压力:所有浏览器请求都经过 Next.js 服务端转发,高并发场景下需要关注服务端资源消耗
  5. Cookie 域名限制:HttpOnly Cookie 的 Domain 属性必须覆盖前后端域名,否则代理转发时 Cookie 无法传递

七、总结

/api/client-proxy/ 不是一个无意义的中间层,而是 HttpOnly Cookie 认证方案下必然产生的架构代价。它在"安全性"和"简单性"之间做出了明确的选择——用一层代理换取了 Token 不被 JavaScript 读取的安全保障。

理解这一模式的关键是认清一个事实:当认证信息存储在 HttpOnly Cookie 中时,浏览器端的 JavaScript 根本无法直接调用跨域后端 API。这不是 Next.js 的限制,而是浏览器安全模型的设计原则。

如果你的项目对安全性要求不高,传统的 localStorage + Authorization Header 方案依然有效且更简单。但如果安全性是优先级更高的考量,/api/client-proxy/ 代理模式是一个值得采纳的成熟方案。


延伸思考:这种代理模式本质上是一种 BFF(Backend For Frontend)模式的轻量实现——前端服务器充当中间层,处理认证、聚合等后端不关心的横切关注点。在微服务架构中,BFF 层通常会承担更多职责,而本项目中的代理层则是 BFF 思想的精简应用。