ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

全栈开发:TypeScript、React、Next.js、MongoDB、Docker 完全教程指南 - 第十二章 构建中间件(2):用 TaoToken 统一 Key 打通鉴权与限流链路

全栈开发:TypeScript、React、Next.js、MongoDB、Docker 完全教程指南 - 第十二章 构建中间件(2):用 TaoToken 统一 Key 打通鉴权与限流链路 1. 从一次 401 说起Next.js 中间件里鉴权与限流到底卡在哪如果你正在用 TypeScript React Next.js MongoDB Docker 这套组合做全栈项目大概率会在某个阶段遇到这样的场景前端 React 页面发请求Next.js 的 API 路由或中间件需要先判断用户有没有登录再决定要不要放行同时你还想给接口加一层限流防止某个用户疯狂刷接口。听起来是两个独立功能但真正写起来鉴权和限流的执行顺序、Key 的统一管理、MongoDB 会话校验的异步等待很容易把链路搞乱。我自己在本地用 Docker 起 MongoDB 和 Next.js 服务时就踩过一个典型的坑中间件里先查了 MongoDB 会话发现没登录直接返回 401但限流逻辑写在后面结果未登录请求根本没被计数攻击者可以无限次尝试登录接口。后来把顺序调成「先限流、再鉴权」又发现限流用的 Key 如果每个请求都重新生成根本起不到限制作用。问题的核心在于鉴权和限流都需要一个稳定、可复用的标识而这个标识最好由统一的 Key 通道来提供。这一章要解决的就是这件事。我会用 TypeScript 写一个可复用的 middleware 模块把 React 前端请求、Next.js 中间件、MongoDB 会话校验串起来并且用 Docker 在本地起服务验证。中间会接入 TaoToken 的统一 Key 和 API 通道让鉴权和限流共用同一套凭证来源避免 Key 散落在各个文件里。你跟着做最后能用 curl 分别触发 401 和 429看到完整的链路跑通。适合谁看已经写过 Next.js API 路由、对 MongoDB 有基本了解、想搞清楚中间件里鉴权限流怎么配合的开发者。不需要你之前用过 TaoToken但需要你本地有 Docker 和 Node.js 环境。核心检索词先明确Next.js 中间件鉴权限流、TypeScript 可复用 middleware、MongoDB 会话校验、TaoToken 统一 Key、Docker 本地验证。这几个词会贯穿全文你可以在每一步里找到对应的落点。先说结论鉴权和限流不是二选一而是有明确顺序的管道。请求进来先过限流基于 Key 计数再过鉴权基于 MongoDB 会话最后才到业务逻辑。Key 从哪来从 TaoToken 的统一通道拿这样前端、中间件、后端服务用的是同一套凭证不会出现「前端有 Key、中间件不认」的割裂。下面从环境准备开始一步步把这条链路搭起来。2. TaoToken 统一 Key 前置准备把凭证收口到一处在写中间件之前先把 Key 的来源理清楚。很多项目的问题不是代码写错而是 Key 管理混乱前端环境变量里一个、Next.js 服务端一个、MongoDB 连接串里又嵌一个最后排查 401 时根本不知道是哪个环节的 Key 失效了。TaoToken 在这里的作用是提供一个统一的 API 通道和 Key 管理入口让鉴权和限流都从同一个地方取凭证。你需要先拿到一个可用的 Key。打开 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册登录后进入控制台。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在里面找到 API Keys 页面路径是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。新建一个 Key复制出来后面会写进.env.local。这里要强调一点TaoToken 的 API 基础地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数配置的时候直接用这个。Key 的格式通常是一串以sk-开头的字符串具体以你控制台看到的为准。为什么要在中间件项目里用 TaoToken 的 Key因为鉴权和限流都需要一个「请求身份」的锚点。限流按 Key 计数鉴权用 Key 关联的会话去 MongoDB 查用户状态。如果 Key 不统一限流计的是 A Key鉴权查的是 B 会话两边对不上就会出现「明明登录了却被限流」或者「没登录却绕过了限流」的怪现象。接下来在项目根目录创建环境变量文件。Next.js 默认读取.env.local这个文件不要提交到 Git。内容如下# .env.local TAOTOKEN_API_BASEhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的实际Key MONGODB_URImongodb://localhost:27017/fullstack_demo NEXT_PUBLIC_APP_URLhttp://localhost:3000注意TAOTOKEN_API_KEY没有NEXT_PUBLIC_前缀这意味着它只在服务端可用不会被打包进浏览器代码。这是安全底线Key 不能暴露给前端。前端 React 组件如果需要调用受保护接口走的是同源请求由 Next.js 中间件在服务端完成 Key 注入和校验。如果你用的是 Claude Code 或者类似的编码工具来辅助开发可以在 TaoToken 的 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 了解长期编码场景的配置方式。不过本章的重点是中间件本身Key 拿到手就够了。还有一个细节TaoToken 的模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你在配置过程中对 Base URL 或 Key 的用法有疑问这两个页面能帮你确认参数格式。我实测下来文档里的 Base URL 和 Key 组合是最省事的不用自己猜。环境变量准备好之后先别急着写中间件。下一步是把 MongoDB 用 Docker 跑起来并且确认 Next.js 能连上。因为鉴权依赖 MongoDB 里的会话数据如果数据库没起来中间件里的会话校验会直接抛错你看到的可能不是 401 而是 500排查起来会绕远路。Docker 起 MongoDB 的命令很简单但要注意端口映射和数据卷。我用的是下面这个docker run -d \ --name fullstack-mongo \ -p 27017:27017 \ -v mongo-data:/data/db \ -e MONGO_INITDB_DATABASEfullstack_demo \ mongo:7跑起来之后用docker ps确认容器状态是 Up。然后可以进容器里用 mongosh 建一个测试集合或者直接在 Next.js 里用 Mongoose 连接。这里先不展开 Mongoose 模型下一节写中间件配置时会带上。Key 和数据库都就位后就可以进入核心部分写可复用的 TypeScript 中间件模块。3. 可复制配置TypeScript 中间件模块与 settings 片段这一节给出可以直接复制到项目里的配置和代码。目标是在 Next.js 的middleware.ts里实现两个能力基于 TaoToken Key 的限流以及基于 MongoDB 会话的鉴权。为了让代码可复用我把限流和鉴权拆成独立函数放在lib/middleware/目录下。先看目录结构这样你知道每个文件放哪project-root/ ├── middleware.ts ├── lib/ │ └── middleware/ │ ├── rate-limit.ts │ ├── auth.ts │ └── key.ts ├── models/ │ └── session.ts ├── .env.local └── package.jsonmiddleware.ts是 Next.js 的入口lib/middleware/下是具体逻辑models/session.ts是 MongoDB 会话模型。先写 Key 解析模块lib/middleware/key.ts// lib/middleware/key.ts export function resolveApiKey(req: Request): string | null { const headerKey req.headers.get(x-api-key); if (headerKey headerKey.startsWith(sk-)) { return headerKey; } const auth req.headers.get(authorization); if (auth auth.startsWith(Bearer sk-)) { return auth.slice(7); } return null; } export function getServerKey(): string { const key process.env.TAOTOKEN_API_KEY; if (!key) { throw new Error(TAOTOKEN_API_KEY is not set); } return key; }这个模块做两件事从请求头里解析客户端带来的 Key以及从服务端环境变量读取统一 Key。限流用客户端 Key 做计数维度鉴权时如果客户端没带 Key可以回退到服务端 Key 去 TaoToken 校验会话有效性。接着写限流模块lib/middleware/rate-limit.ts。这里用一个内存 Map 做演示生产环境应该换成 Redis但本地验证足够// lib/middleware/rate-limit.ts type Bucket { count: number; resetAt: number }; const buckets new Mapstring, Bucket(); const WINDOW_MS 60_000; const MAX_REQUESTS 5; export function checkRateLimit(key: string): { allowed: boolean; remaining: number; resetAt: number; } { const now Date.now(); const bucket buckets.get(key); if (!bucket || now bucket.resetAt) { const resetAt now WINDOW_MS; buckets.set(key, { count: 1, resetAt }); return { allowed: true, remaining: MAX_REQUESTS - 1, resetAt }; } if (bucket.count MAX_REQUESTS) { return { allowed: false, remaining: 0, resetAt: bucket.resetAt }; } bucket.count 1; return { allowed: true, remaining: MAX_REQUESTS - bucket.count, resetAt: bucket.resetAt, }; }参数说明WINDOW_MS是时间窗口MAX_REQUESTS是窗口内最大请求数。我设成 5 次每分钟方便你用 curl 快速触发 429。buckets以 Key 为维度存储计数同一个 Key 的请求共享计数。然后是鉴权模块lib/middleware/auth.ts它需要连 MongoDB 查会话// lib/middleware/auth.ts import mongoose from mongoose; import { SessionModel } from /models/session; let connected false; async function ensureDb() { if (connected) return; const uri process.env.MONGODB_URI; if (!uri) throw new Error(MONGODB_URI is not set); await mongoose.connect(uri); connected true; } export async function verifySession(apiKey: string): Promise{ valid: boolean; userId?: string; } { await ensureDb(); const session await SessionModel.findOne({ apiKey, expiresAt: { $gt: new Date() } }).lean(); if (!session) { return { valid: false }; } return { valid: true, userId: String(session.userId) }; }MongoDB 会话模型models/session.ts// models/session.ts import { Schema, model, models } from mongoose; const SessionSchema new Schema({ apiKey: { type: String, required: true, index: true }, userId: { type: Schema.Types.ObjectId, required: true }, expiresAt: { type: Date, required: true }, }); export const SessionModel models.Session || model(Session, SessionSchema);最后是入口middleware.ts把限流和鉴权串起来// middleware.ts import { NextResponse } from next/server; import type { NextRequest } from next/server; import { resolveApiKey, getServerKey } from /lib/middleware/key; import { checkRateLimit } from /lib/middleware/rate-limit; import { verifySession } from /lib/middleware/auth; export async function middleware(req: NextRequest) { const path req.nextUrl.pathname; if (path.startsWith(/api/public)) { return NextResponse.next(); } const clientKey resolveApiKey(req); const key clientKey ?? getServerKey(); const rate checkRateLimit(key); if (!rate.allowed) { return NextResponse.json( { error: Too Many Requests, resetAt: rate.resetAt }, { status: 429, headers: { X-RateLimit-Remaining: 0, X-RateLimit-Reset: String(rate.resetAt), }, } ); } const session await verifySession(key); if (!session.valid) { return NextResponse.json( { error: Unauthorized }, { status: 401 } ); } const res NextResponse.next(); res.headers.set(X-User-Id, session.userId ?? ); res.headers.set(X-RateLimit-Remaining, String(rate.remaining)); return res; } export const config { matcher: [/api/:path*], };这段配置的关键点限流在鉴权之前执行所以未登录请求也会被计数避免登录接口被无限刷。Key 优先取客户端带来的没有则用服务端统一 Key。鉴权通过后把userId写进响应头后续 API 路由可以直接读。如果你用的是 Claude Code 或者 Cline 这类工具配置里需要写全三件套Base URL 用https://taotoken.net/apiKey 用你控制台生成的Model ID 按你实际调用的模型填。这三者缺一不可否则会出现local proxy failed或者reading choices之类的报错。CC Switch 场景下也是同样的三件套逻辑Base URL、Key、Model ID 要对齐。配置写完后还需要一个种子脚本来往 MongoDB 里插一条测试会话否则鉴权永远返回 401。可以用下面这个脚本// scripts/seed-session.ts import mongoose from mongoose; import { SessionModel } from ../models/session; async function main() { await mongoose.connect(process.env.MONGODB_URI!); await SessionModel.deleteMany({}); await SessionModel.create({ apiKey: process.env.TAOTOKEN_API_KEY, userId: new mongoose.Types.ObjectId(), expiresAt: new Date(Date.now() 24 * 60 * 60 * 1000), }); console.log(session seeded); await mongoose.disconnect(); } main();用npx tsx scripts/seed-session.ts跑一下确保数据库里有一条有效会话。到这里配置部分就齐了。下一节用 curl 验证 401 和 429。4. 验证请求用 curl 触发 401 与 429 看真实结果配置写完了不验证等于没写。这一节用 curl 分别触发 401 和 429并且解释每个响应头代表什么。先确保 Next.js 开发服务在跑npm run dev服务默认在http://localhost:3000。先测一个正常请求带上正确的 Keycurl -i http://localhost:3000/api/protected \ -H x-api-key: sk-你的实际Key预期返回 200响应头里能看到X-User-Id和X-RateLimit-Remaining。X-RateLimit-Remaining会随着请求次数递减从 4 开始往下走。如果你看到的是 401说明 MongoDB 里没有匹配的会话回去检查种子脚本是否跑成功以及apiKey字段是否和请求头里的 Key 完全一致。现在测 401。故意带一个不存在的 Keycurl -i http://localhost:3000/api/protected \ -H x-api-key: sk-invalid-key-for-test预期返回HTTP/1.1 401 Unauthorized content-type: application/json {error:Unauthorized}注意这里限流是先执行的所以这个无效 Key 也会被计数。如果你连续发 5 次以上会先看到 429 而不是 401。这正好验证了「限流在鉴权之前」的设计。测 429 的时候用一个有效 Key 连续发 6 次请求for i in $(seq 1 6); do echo --- request $i --- curl -s -o /dev/null -w %{http_code}\n \ http://localhost:3000/api/protected \ -H x-api-key: sk-你的实际Key done预期输出前 5 次是 200第 6 次是 429。如果你想看 429 的响应体去掉-o /dev/nullcurl -i http://localhost:3000/api/protected \ -H x-api-key: sk-你的实际Key第 6 次会返回HTTP/1.1 429 Too Many Requests x-ratelimit-remaining: 0 x-ratelimit-reset: 1730000000000 {error:Too Many Requests,resetAt:1730000000000}x-ratelimit-reset是时间戳等它过去之后计数会重置。你可以用date -d 1730000000转成可读时间确认窗口是 60 秒。这里有个实测细节Next.js 的 middleware 在开发模式下每次热更新会重新加载模块内存里的buckets会被清空。所以如果你改了代码限流计数会归零需要重新发请求。生产环境用 Redis 就不会有这个问题。另外React 前端发请求时如果用的是fetch默认不会带x-api-key头。你需要在请求里显式加上或者让 Next.js 的 API 路由在服务端注入。我一般在前端封装一个apiFetch函数// lib/api-client.ts export async function apiFetch(path: string, init?: RequestInit) { const key process.env.NEXT_PUBLIC_TAOTOKEN_KEY; return fetch(path, { ...init, headers: { ...init?.headers, ...(key ? { x-api-key: key } : {}), }, }); }注意这里用了NEXT_PUBLIC_前缀意味着 Key 会暴露给浏览器。这在本地验证可以生产环境不建议。更安全的做法是前端只带会话 Cookie由中间件在服务端换成 TaoToken Key。这个取舍你要根据实际场景决定。验证通过后你已经有了一条完整的链路React 请求 → Next.js 中间件 → 限流计数 → MongoDB 会话校验 → 业务路由。下一节把常见的报错列出来方便你对照排查。5. 常见错排查401、429、local proxy failed、reading choices 对照这一节按真实报错来组织。你在本地跑这条链路时大概率会遇到下面几类问题我按错误信息分类给出原因和修法。第一类401 Unauthorized。这是最常见的。可能原因有三个。一是 MongoDB 里没有会话记录或者apiKey字段和请求头里的 Key 不一致。修法是重新跑种子脚本并且用mongosh进数据库确认docker exec -it fullstack-mongo mongosh fullstack_demo db.sessions.find().pretty()看apiKey字段的值是否和你 curl 里带的一致。二是会话过期了expiresAt小于当前时间。种子脚本里设的是 24 小时一般不会过期但如果你手动改过就要检查。三是MONGODB_URI没配对中间件连不上数据库verifySession抛错被吞掉后返回了valid: false。这种情况建议在auth.ts里把 catch 的错误打出来不要静默返回。第二类429 Too Many Requests。这个通常不是 bug而是限流生效了。但如果你觉得「我才发了一次就 429」检查MAX_REQUESTS是不是被改成了 1或者buckets的 Key 是不是每次请求都变了。Key 变化的原因可能是resolveApiKey没解析到请求头回退到了getServerKey()而服务端 Key 是固定的所以所有请求共享计数。这其实是预期行为但如果你想让每个客户端独立计数就要确保请求头里带了 Key。第三类local proxy failed。这个报错通常出现在你用编码工具或者 API 客户端配置 TaoToken 的时候。原因是 Base URL 或 Key 写错了。检查三件套Base URL 是不是https://taotoken.net/apiKey 是不是以sk-开头Model ID 是不是你实际调用的模型。三者缺一或者 Base URL 多写了路径都会导致代理失败。我踩过的坑是把 Base URL 写成了带/v1的地址结果一直连不上去掉/v1就正常了。第四类reading choices。这个报错一般出现在调用模型接口后解析响应时。原因是返回结构和你预期的字段不一致代码里去读choices但实际没有这个字段。排查方法是先把原始响应打印出来看实际返回的 JSON 结构。如果是 TaoToken 的接口确认你用的 Model ID 和文档里的一致。接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有各模型的参数说明对照一下。第五类OAuth 相关报错。如果你在配置 Claude Code 或者类似工具时看到 OAuth 失败检查是不是把 API Key 和 OAuth 流程混用了。TaoToken 的 Key 是直接放在请求头里的不需要走 OAuth 授权码流程。Claude Code 的配置入口在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 里面有具体的配置示例。Anthropic 兼容接口的说明在 https://taotoken.net/anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentanthropicutm_campaignrewrite 如果你用的是 Anthropic 风格的调用注意 Base URL 和请求头的差异。第六类Docker 容器起来了但连不上 MongoDB。检查端口映射是不是27017:27017以及 Next.js 里的MONGODB_URI是不是mongodb://localhost:27017/fullstack_demo。如果你在 Docker 容器里跑 Next.jslocalhost要换成容器名或者host.docker.internal。这个坑很隐蔽因为容器内外的网络视图不一样。第七类中间件不生效。Next.js 的middleware.ts必须放在项目根目录和pages或app同级。如果你放在src目录下要确认 Next.js 版本是否支持。另外config.matcher要匹配到你的 API 路径写错了中间件根本不会执行。可以在中间件里加一行console.log(middleware hit, path)确认。把这几类问题对照一遍基本能覆盖本地验证时 90% 的报错。剩下的就是具体业务逻辑的问题了。6. 把 Key 通道固定下来后续接入与长期编码的选择链路跑通之后下一步要考虑的是怎么把这套配置稳定下来。本地验证用的内存限流和手动种子会话到了真实项目里需要替换成更可靠的方案。限流换成 Redis会话换成真实的登录流程写入 MongoDBKey 从 TaoToken 控制台统一管理。这样鉴权和限流就不会因为服务重启而丢失状态。如果你后续要长期用这套组合做编码或者 Agent 开发可以了解 TaoToken 的 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它针对长期编码场景有更合适的配置方式。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 你可以直接在里面测试 Key 是否可用。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 需要轮换 Key 或者查看用量时从这里进。回到中间件本身我建议你把lib/middleware/下的三个模块保持独立不要把所有逻辑塞进middleware.ts。这样限流策略变了只改rate-limit.ts鉴权逻辑变了只改auth.tsKey 解析规则变了只改key.ts。React 前端那边封装一个统一的请求函数把 Key 注入和错误处理收口避免每个组件都写一遍 fetch。最后留一个实用技巧在middleware.ts里给响应头加上X-RateLimit-Remaining和X-RateLimit-Reset前端可以根据这两个值做倒计时提示用户体验会好很多。这个细节在本地验证时看不出差别但上线后能减少很多「为什么突然请求失败了」的困惑。
返回列表