切换语言
切换主题

Next.js API 认证与安全:从 JWT 到速率限制的完整实战指南

凌晨三点,手机震了。是云服务商的账单提醒——7800美元。

我揉揉眼睛,以为看错了。上个月账单才120块。点开详情,API 调用次数:1800万次。我的个人项目,平时一天也就几百次调用。

原来是某个爬虫发现了我没加任何防护的 API 端点,整整刷了三天。那一刻我突然明白,为什么都说 API 安全不是可选项。

说真的,很多人(包括之前的我)搭 Next.js 项目时,只关心页面好不好看、交互流不流畅,觉得 API 安全是后端的事。但 Next.js 的 API Routes 本质上就是你的后端,你不保护它,谁来保护?

这篇文章,我想把这几年踩过的坑、研究过的方案,系统地整理一遍。从 JWT 认证到 CORS 配置,从速率限制到输入验证——不是空谈理论,而是能直接用在项目里的实战代码。

为什么 API 安全这么重要

API 安全的常见威胁

10.0
CVSS 评分

去年12月,React 官方发了个严重安全公告,编号 CVE-2025-55182,CVSS 评分直接拉满——10.0。

这是什么概念?满分。攻击者只要构造一个特殊的 HTTP 请求,就能在你的服务器上执行任意代码。如果你用的是 React Server Components,没及时更新,基本就是在裸奔。

这还不是个例。今年3月又出了个授权绕过漏洞 (CVE-2025-29927),评分 9.1。攻击者伪造一个请求头,就能绕过你的中间件鉴权。你以为设了认证就安全?抱歉,人家直接跳过。

除了这些高危漏洞,日常威胁更多:

恶意爬虫和 DDoS 攻击。没做限流的 API,分分钟被刷垮。我见过有人的登录接口一秒被请求3000次,暴力破解密码,服务器直接宕机。

数据泄露。没做好权限控制,用户 A 能看到用户 B 的订单信息。这种事上了新闻,品牌声誉直接完蛋。

注入攻击。SQL 注入、XSS、命令注入…听着老套,但每年还是有无数项目中招。你以为”我用 React 自动转义了”,但 API 这边没验证,照样白搭。

Next.js API Routes 的特点

Next.js 的 API Routes 跟传统后端不太一样,有些特点要注意:

Serverless 优先。部署到 Vercel 的话,每个 API 请求都是一个独立的 Serverless 函数。好处是自动扩展,坏处是无状态——你不能用传统的内存 Session,得换成 JWT 或数据库 Session。

和前端混在一起。代码都在一个仓库,环境变量一不小心就泄露到客户端。我见过有人把 DATABASE_URL 写在 .env 里,结果打包时被暴露到前端 bundle,直接上了 GitHub。

边缘计算的限制。如果你用 Edge Runtime,有些 Node.js API 用不了,加密库、数据库连接都要重新选。这时候安全方案也得跟着调整。

说白了,Next.js API 是你的后端,但它更轻、更灵活、也更容易出问题。

API 认证实战

认证方案选择指南

先聊个最实际的问题:JWT 和 Session,到底该用哪个?

我刚开始做 Next.js 项目时,也纠结过这个。网上说法一堆,有人说 JWT 是现代方案必须用,有人说 Session 更安全。后来我发现,这事得看场景。

JWT 适合这些情况:

  • 你的应用要部署到多个服务器 (Serverless、边缘节点)
  • 需要跨域认证,比如前端在 app.com,API 在 api.com
  • 不想管理 Session 存储,越简单越好

JWT 的本质就是把用户信息编码成一个 token,服务器不存状态,每次请求带上 token 就行。横向扩展超级方便。

Session 适合这些情况:

  • 需要服务端主动控制,比如踢人下线、权限实时变更
  • 对安全要求极高,不想让客户端持有任何用户信息
  • 已经有 Redis 或数据库,管理 Session 不是问题

Session 的特点是状态在服务端,客户端只有个 Session ID。你想让某个用户失效,删掉 Session 就行,JWT 做不到这点。

我自己的选择标准:小项目、个人项目用 JWT,省事;企业级、需要精细控制的用 Session。别纠结,先上手一个,遇到问题再换也来得及。

JWT 认证完整实现

好,假设你决定用 JWT。怎么在 Next.js 里实现?

第一步:生成和验证 Token

先装个库:

npm install jose

为什么不用 jsonwebtoken?那个库不支持 Edge Runtime。jose 是 Web 标准实现,啥环境都能跑。

创建 lib/auth.ts:

import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(
  process.env.JWT_SECRET || 'your-secret-key-at-least-32-characters'
);

export async function createToken(payload: { userId: string }) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m') // 15分钟后过期
    .sign(secret);
}

export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch {
    return null;
  }
}

注意那个 15m 过期时间。很多人设成 7 天、30 天,方便是方便,但 token 一旦泄露,攻击者能用很久。短期 Access Token + 长期 Refresh Token,才是正经做法。

第二步:存储 Token——别用 localStorage

这是重点。网上很多教程教你把 token 存 localStorage,然后 XSS 攻击一来,token 全被偷走。

正确做法:HttpOnly Cookie

登录接口返回 token 时:

// app/api/login/route.ts
import { NextResponse } from 'next/server';
import { createToken } from '@/lib/auth';

export async function POST(request: Request) {
  // 验证用户名密码...

  const token = await createToken({ userId: user.id });

  const response = NextResponse.json({ success: true });
  response.cookies.set('token', token, {
    httpOnly: true,    // JS 读不到,防 XSS
    secure: true,      // 只能 HTTPS 传输
    sameSite: 'lax',   // 防 CSRF
    maxAge: 900,       // 15分钟,和 token 过期时间一致
  });

  return response;
}

HttpOnly 是关键:JavaScript 完全读不到这个 Cookie,XSS 攻击拿不走。

第三步:中间件保护 API

现在 token 有了,怎么让某些 API 必须登录才能访问?用 Next.js 的 Middleware。

创建 middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyToken } from './lib/auth';

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value;

  if (!token) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }

  const payload = await verifyToken(token);
  if (!payload) {
    return NextResponse.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }

  // 验证通过,把用户信息传给 API
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-user-id', payload.userId as string);

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: '/api/protected/:path*',
};

这样一来,所有 /api/protected/* 的接口都自动受保护了。在 API 里取用户信息:

// app/api/protected/profile/route.ts
import { headers } from 'next/headers';

export async function GET() {
  const headersList = await headers();
  const userId = headersList.get('x-user-id');

  // 去数据库查用户信息...
}

第四步:Token 刷新机制

15分钟就过期,用户不得频繁登录?这里要引入 Refresh Token。

Access Token 短期 (15分钟),Refresh Token 长期 (30天)。Access Token 过期了,用 Refresh Token 换一个新的,不用重新登录。

具体实现稍微复杂点,但思路就是这样。网上有很多现成方案,比如 next-auth 内置了这套逻辑。

使用 NextAuth.js 快速集成

说实话,上面那套自己写一遍,能理解原理,但工程量不小。如果你想快速上手,直接用 NextAuth.js (现在叫 Auth.js)。

这库牛在哪?开箱即用的安全默认配置:

  • 自动处理 CSRF 保护
  • Session 签名和加密
  • 支持 JWT 和数据库 Session
  • 自带 Google、GitHub 等第三方登录

装一下:

npm install next-auth

创建 app/api/auth/[...nextauth]/route.ts:

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

const handler = NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        // 验证用户名密码...
        if (user) {
          return { id: user.id, email: user.email };
        }
        return null;
      }
    })
  ],
  session: {
    strategy: 'jwt',  // 用 JWT,适合 Serverless
    maxAge: 30 * 24 * 60 * 60, // 30 天
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.userId = user.id;
      }
      return token;
    },
    async session({ session, token }) {
      session.userId = token.userId;
      return session;
    }
  }
});

export { handler as GET, handler as POST };

然后在 API 里检查登录状态:

import { getServerSession } from 'next-auth';

export async function GET() {
  const session = await getServerSession();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 已登录,继续处理...
}

就这么简单。NextAuth 帮你把 token 管理、Session 刷新全搞定了。

CORS 配置详解

CORS 的本质和常见问题

CORS(跨域资源共享)是个让很多人头疼的东西。开发环境好好的,一部署就报错:

Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.

简单讲,浏览器有个安全策略:网页 A 不能随便访问网站 B 的资源。比如你在 app.com 的页面,想调 api.com 的接口,浏览器会先问 api.com:“这个请求来自 app.com,你同意吗?”API 必须明确回复”我同意”,请求才能通过。

为什么开发环境不报错?

Next.js 开发时,前端和 API 都在 localhost:3000,同源,不涉及跨域。一部署,前端在 Vercel,API 在别的服务器,就跨域了。

Preflight 请求是啥?

你发个 POST 请求,携带自定义请求头 (比如 Authorization),浏览器会先发个 OPTIONS 请求试探一下。这就是 Preflight。API 如果没处理 OPTIONS,直接 404,CORS 就失败了。

我之前就踩过这坑:写好了 POST 接口,但忘了处理 OPTIONS,前端一直报 CORS 错误,找了半天才发现。

Next.js 中配置 CORS 的三种方法

方法一:next.config.js 全局配置

适合所有 API 都允许同样的跨域来源。

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ];
  },
};

优点:一次配置,全局生效。缺点:不够灵活,不能针对单个 API 定制。

方法二:Middleware 中间件配置

适合需要动态判断、统一处理的场景。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 处理 Preflight 请求
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 200,
      headers: {
        'Access-Control-Allow-Origin': 'https://app.example.com',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      },
    });
  }

  // 正常请求,添加 CORS 头
  const response = NextResponse.next();
  response.headers.set('Access-Control-Allow-Origin', 'https://app.example.com');

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

这种方式能拿到 request 对象,可以根据来源动态决定是否允许。

方法三:API Route 内配置

适合单个 API 有特殊需求的情况。

// app/api/public/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const data = { message: 'Hello' };

  return NextResponse.json(data, {
    headers: {
      'Access-Control-Allow-Origin': '*', // 公开 API,允许所有来源
    },
  });
}

export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET',
      'Access-Control-Allow-Headers': 'Content-Type',
    },
  });
}

注意每个 API 都要写 OPTIONS 处理,不然 Preflight 过不了。

CORS 安全最佳实践

1. 不要乱用通配符

看到很多人这么写:

'Access-Control-Allow-Origin': '*'

这意味着任何网站都能调你的 API。公开数据无所谓,但如果涉及用户信息、敏感操作,这就是在裸奔。

正确做法:明确指定允许的域名。

const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];

const origin = request.headers.get('origin');
if (origin && allowedOrigins.includes(origin)) {
  response.headers.set('Access-Control-Allow-Origin', origin);
}

2. 凭证传递要小心

如果你的 API 需要读 Cookie (比如 Session 认证),前端要这么写:

fetch('https://api.example.com', {
  credentials: 'include',
});

后端必须配合:

response.headers.set('Access-Control-Allow-Credentials', 'true');

但是,Access-Control-Allow-Origin 不能是 *。浏览器直接拒绝这种组合,你必须指定具体域名。

3. 处理 Preflight 请求

记住:带 Authorization 头、Content-Type: application/json 的请求,基本都会触发 Preflight。你的 API 必须响应 OPTIONS 方法。

可以写个通用函数:

export function corsHeaders(origin?: string) {
  return {
    'Access-Control-Allow-Origin': origin || 'https://app.example.com',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Max-Age': '86400', // Preflight 结果缓存 24 小时
  };
}

然后每个 API 复用就行了。

API 速率限制

为什么需要速率限制

回到开头那个故事。我的 API 被刷了1800万次,如果当时做了限流,每个 IP 每分钟最多请求100次,损失可能就几十块,不会是7800美元。

速率限制(Rate Limiting)就是限制用户在一定时间内能调用多少次 API。听着简单,作用可不小:

防 DDoS 攻击。攻击者想用海量请求把你服务器冲垮?限流一开,每秒超过阈值的请求直接拒绝,服务器稳如狗。

防暴力破解。登录接口不限流,黑客写个脚本一秒试10000个密码。限流后,每个 IP 每分钟只能尝试5次,破解难度指数级上升。

保护资源。你的数据库、第三方 API 调用都有成本。限流能防止单个用户把资源耗光,保证服务对所有人公平。

速率限制方案对比

Next.js 做限流,主流有几个方案:

方案一:@upstash/ratelimit + Vercel KV

这是我现在最常用的方案。Upstash 是个 Serverless Redis,Vercel 官方合作伙伴,集成超级简单。

优点:

  • Serverless 友好,不用自己管 Redis 服务器
  • 支持多种算法:固定窗口、滑动窗口、令牌桶
  • 免费额度够个人项目用

缺点:

  • 大流量项目需要付费
  • 依赖第三方服务

方案二:自托管 Redis

如果你已经有 Redis,或者不想依赖第三方,可以自己实现。

优点:

  • 完全控制,没有额外费用
  • 可以定制各种复杂逻辑

缺点:

  • 需要维护 Redis 服务器
  • Serverless 环境下配置复杂

方案三:内存限流

不想装 Redis?纯内存也能实现简单限流。

优点:

  • 零依赖,代码几行搞定
  • 适合开发环境和小项目

缺点:

  • Serverless 环境每次请求可能是新实例,内存不共享,限流失效
  • 重启服务器限流数据全丢

我的建议:个人项目、Serverless 部署,直接上 Upstash;企业项目、自己管服务器,用 Redis;Demo 或本地开发,内存方案凑合。

实战代码示例

以 Upstash 为例,手把手教你实现。

第一步:安装和配置

npm install @upstash/ratelimit @upstash/redis

去 Upstash 官网创建个 Redis 数据库,拿到 UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN,放进 .env:

UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token

第二步:创建限流器

创建 lib/rate-limit.ts:

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

// 创建 Redis 客户端
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// 创建限流器:滑动窗口,10秒内最多10次请求
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '10 s'),
  analytics: true,
});

slidingWindow(10, '10 s') 意思是:10秒内最多10次请求。滑动窗口比固定窗口更平滑,不会出现窗口边界突刺。

第三步:在 API 中使用

// app/api/protected/route.ts
import { NextResponse } from 'next/server';
import { ratelimit } from '@/lib/rate-limit';

export async function GET(request: Request) {
  // 获取用户 IP
  const ip = request.headers.get('x-forwarded-for') || 'unknown';

  // 检查限流
  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      {
        error: 'Too many requests',
        limit,
        remaining,
        reset: new Date(reset),
      },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': limit.toString(),
          'X-RateLimit-Remaining': remaining.toString(),
          'X-RateLimit-Reset': reset.toString(),
        },
      }
    );
  }

  // 限流通过,正常处理
  return NextResponse.json({ data: 'Success' });
}

这里用 IP 作为限流标识。如果你有用户登录,可以用 userId:

const identifier = session?.userId || ip;
const { success } = await ratelimit.limit(identifier);

这样登录用户按用户限流,未登录按 IP 限流,更精准。

第四步:中间件全局限流

不想每个 API 都写一遍?在 Middleware 里统一处理:

// middleware.ts
import { ratelimit } from '@/lib/rate-limit';

export async function middleware(request: NextRequest) {
  const ip = request.ip || 'unknown';
  const { success } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/:path*',
};

所有 API 自动受保护,省事。

输入验证与防御

为什么输入验证是第一道防线

“永远不要信任用户输入”——这是安全领域的金科玉律。

你的前端有各种表单验证?没用。打开开发者工具,改改代码,验证直接绕过。真正的防御在服务端。

SQL 注入。用户在输入框写 '; DROP TABLE users; --,如果你直接拼接 SQL,数据库就炸了。虽然现在大家都用 ORM,但原生 SQL 场景还是很多。

XSS 攻击。用户提交 <script>alert('hacked')</script>,你存到数据库,其他用户打开页面,脚本执行,Cookie 被偷。React 确实会自动转义,但如果你用 dangerouslySetInnerHTML,照样中招。

DoS 攻击。用户提交个10MB 的 JSON,你的 Serverless 函数内存直接爆掉。或者发个超长字符串,正则表达式回溯到天荒地老。

输入验证能挡住绝大部分低级攻击。不验证,再多防御措施都是筛子。

使用 Zod 进行类型安全验证

Zod 是我现在最爱用的验证库。TypeScript 写的,和类型系统完美配合。

装一下:

npm install zod

基础用法

定义一个 schema:

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  age: z.number().int().min(18).max(120),
});

在 API 里验证:

// app/api/register/route.ts
import { NextResponse } from 'next/server';
import { userSchema } from '@/lib/schemas';

export async function POST(request: Request) {
  const body = await request.json();

  // 验证数据
  const result = userSchema.safeParse(body);

  if (!result.success) {
    return NextResponse.json(
      {
        error: 'Validation failed',
        details: result.error.format(),
      },
      { status: 400 }
    );
  }

  // 验证通过,拿到类型安全的数据
  const { email, password, age } = result.data;

  // 继续处理...
}

注意用 safeParse,不会抛异常。parse 会抛异常,你得 try-catch。

为什么比手写验证好?

手写验证:

if (!body.email || typeof body.email !== 'string') {
  return error;
}
if (!body.email.includes('@')) {
  return error;
}
// 写到天荒地老...

Zod:

z.string().email()

一行搞定,类型还自动推导。

完整的输入验证方案

验证不只是检查字段类型,还要考虑业务逻辑和边界情况。

1. 验证请求体 (body)

const postSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().max(10000), // 限制长度,防止超大输入
  tags: z.array(z.string()).max(10), // 限制数组长度
  publishedAt: z.string().datetime().optional(),
});

2. 验证查询参数 (query)

// app/api/posts/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);

  const querySchema = z.object({
    page: z.coerce.number().int().min(1).default(1),
    limit: z.coerce.number().int().min(1).max(100).default(20),
    sort: z.enum(['asc', 'desc']).default('desc'),
  });

  const params = querySchema.parse({
    page: searchParams.get('page'),
    limit: searchParams.get('limit'),
    sort: searchParams.get('sort'),
  });

  // params.page 一定是数字,类型安全
}

z.coerce.number() 会自动把字符串转数字,超级方便。

3. 自定义验证规则

const passwordSchema = z.string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), 'Must contain uppercase')
  .refine((val) => /[a-z]/.test(val), 'Must contain lowercase')
  .refine((val) => /[0-9]/.test(val), 'Must contain number');

甚至可以异步验证:

const emailSchema = z.string().email().refine(
  async (email) => {
    const exists = await checkEmailExists(email);
    return !exists;
  },
  'Email already taken'
);

4. 错误处理

Zod 的错误信息挺友好,但可以定制:

if (!result.success) {
  const errors = result.error.errors.map(err => ({
    field: err.path.join('.'),
    message: err.message,
  }));

  return NextResponse.json({ errors }, { status: 400 });
}

返回结构化错误,前端容易展示。

其他安全措施

验证之外,还有些重要安全措施:

1. CSRF 保护

Next.js 的 Server Actions 内置了 CSRF 保护。它会比较请求的 OriginHost 头,不匹配就拒绝。

API Routes 需要自己做。如果你用 NextAuth,它会自动处理。手动实现的话,可以用 CSRF token:

// 生成 token 放在 Cookie 里,前端发请求时带上,服务端对比

2. Content Security Policy (CSP)

next.config.js 配置 CSP,限制页面能加载哪些资源:

{
  headers: [
    {
      key: 'Content-Security-Policy',
      value: "default-src 'self'; script-src 'self'; style-src 'self';",
    },
  ],
}

这样就算有 XSS 漏洞,恶意脚本也加载不了。

3. 环境变量安全

Next.js 环境变量分两种:

  • NEXT_PUBLIC_* 开头的会暴露到前端
  • 不带 NEXT_PUBLIC_ 的只在服务端可用

千万不要把密钥放在 NEXT_PUBLIC_。我见过有人把 API 密钥写成 NEXT_PUBLIC_API_KEY,直接泄露。

4. SQL 注入防护

用 Prisma、Drizzle 这类 ORM,自动参数化查询,基本不会有问题。

如果非要写原生 SQL,用参数化:

// ❌ 危险
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// ✅ 安全
db.query('SELECT * FROM users WHERE id = ?', [userId]);

5. 定期更新依赖

安全漏洞经常出在依赖库里。定期跑:

npm audit
npm update

今年2月那个 React 漏洞,只要更新到最新版就修复了。别嫌麻烦,更新一下能省很多麻烦。

完整安全检查清单

说了这么多,给你整理个清单,对照着检查自己的项目:

认证安全清单

  • ✅ Token 存储在 HttpOnly Cookie,不用 localStorage
  • ✅ Access Token 有效期 ≤ 30 分钟
  • ✅ 实现 Refresh Token 机制
  • ✅ JWT 密钥至少 32 字符,存在环境变量
  • ✅ 启用 Cookie 的 securesameSite 属性
  • ✅ 使用 Middleware 保护敏感 API

CORS 配置清单

  • ✅ 不对敏感 API 使用 Access-Control-Allow-Origin: *
  • ✅ 明确指定允许的域名列表
  • ✅ 正确处理 OPTIONS Preflight 请求
  • ✅ 需要凭证传递时,设置 Access-Control-Allow-Credentials: true
  • ✅ 生产环境验证 CORS 配置是否生效

限流配置清单

  • ✅ 关键端点(登录、注册、密码重置)启用严格限流
  • ✅ 区分认证用户和未认证用户的限流策略
  • ✅ 返回 429 状态码和 Retry-After
  • ✅ 使用 Redis 或 Upstash 等持久化存储,避免 Serverless 失效
  • ✅ 监控限流触发情况,及时调整阈值

输入验证清单

  • ✅ 所有用户输入都经过服务端验证
  • ✅ 使用 Zod 或类似工具进行类型安全验证
  • ✅ 限制字符串、数组、对象的最大长度
  • ✅ 验证数据格式(邮箱、URL、日期等)
  • ✅ 返回清晰的验证错误信息

定期审计清单

  • ✅ 每月至少运行一次 npm audit,修复高危漏洞
  • ✅ 及时更新 Next.js 和 React 到最新稳定版
  • ✅ 订阅 Next.js 安全公告,关注新漏洞
  • ✅ 审查环境变量,确保没有密钥泄露到前端
  • ✅ Code Review 时重点检查认证和权限逻辑

把这个清单打印出来贴在桌上,新项目开始前过一遍,部署前再过一遍。

结论

写了这么多,核心就一句话:API 安全是系统工程,不是一劳永逸的事

你可能觉得麻烦,认证、CORS、限流、验证,每样都要配置。但等到出了问题,数据泄露、服务器被打垮、收到天价账单,那时候再补救,代价就大了。

我自己的经验:从零开始配置这一套,大概半天到一天时间。但配好了之后,基本就是”复制粘贴改改参数”的事,新项目十几分钟搞定。最重要的是,晚上能睡个安稳觉,不用担心哪天醒来发现被攻击了。

行动建议:

  1. 立即检查现有项目,对照清单看看缺了什么
  2. 从最关键的开始,先加认证和限流,再完善其他
  3. 订阅安全资讯,Next.js 官方博客、GitHub Security Advisories 都看看
  4. 分享给团队,安全是大家的事,不是一个人的

最后再啰嗦一句:2025年12月那个 CVSS 10.0 的 React 漏洞,影响范围超级大。如果你还没更新,赶紧升级到最新版。安全更新,真的不能拖。

API 安全这条路很长,但每一步都值得。希望这篇文章能帮你少踩些坑,早点把安全防护做到位。

常见问题

Next.js API 应该使用 JWT 还是 Session 认证?
取决于场景。Serverless 部署、跨域认证场景适合 JWT,因为无状态、易扩展;需要服务端主动控制(踢人下线)、对安全要求极高的场景适合 Session。个人项目推荐 JWT,企业级应用推荐 Session。
为什么不能把 JWT token 存在 localStorage?
因为 localStorage 可以被 JavaScript 读取,XSS 攻击一旦成功就能窃取 token。应该使用 HttpOnly Cookie 存储,JavaScript 完全读不到,即使有 XSS 漏洞也无法窃取 token。
开发环境不报 CORS 错误,生产环境为什么会报?
开发时前端和 API 都在 localhost:3000,属于同源,不涉及跨域。生产环境前端和 API 可能在不同域名,浏览器会执行 CORS 检查。解决方法是在 API 端正确配置 Access-Control-Allow-Origin 头。
Serverless 环境下如何实现速率限制?
推荐使用 @upstash/ratelimit + Vercel KV 方案。Serverless 函数每次请求可能是新实例,内存不共享,纯内存限流会失效。Upstash Redis 提供持久化存储,所有实例共享限流数据,完美适配 Serverless。
如何防止 API 被暴力破解密码?
实施多层防护:1) 对登录接口启用严格限流(如每 IP 每分钟最多 5 次);2) 使用 Zod 验证密码强度;3) 实施账户锁定机制(连续失败 5 次锁定 30 分钟);4) 添加验证码(CAPTCHA)增加破解成本。

17 分钟阅读 · 发布于: 2026年1月5日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章