假如你已经用 Next.js 搭好了一个应用,它跑在一个域名下,API 接口在另一个域名。登录没问题,仪表盘数据也正常填充,一切看起来井然有序。直到某天你无意间打开浏览器的 Network 面板,才发现那个访问令牌就躺在请求头里,以明文的方式,被浏览器自动发送出去。
从那一刻起,页面上的每一行 JavaScript 都能读到它。不仅仅是你的业务代码,上周刚加进来的分析脚本、那十二个你从未点开过的传递依赖项,甚至某个 XSS 漏洞趁虚而入时,都能拿到这根钥匙。可你从未真正做过这个决定——它只是“顺手”发生了,因为直接在组件里调用 API 是阻力最小的路径,而且一直没人提出异议。
把这个“顺手”的方式写成代码,大概长这样:
// 一个客户端组件直接请求你的 API'use client';export function Profile() {useEffect(() => {fetch('https://api.example.com/me', {headers: { authorization: `Bearer ${token}` },.then((r) => r.json()).then(setProfile);}, []);}这短短几行,实际上替你做了一连串你未必想要的决定。令牌现在在 JavaScript 里,原本 HttpOnly Cookie 能提供的保护就荡然无存了。请求是跨域的,于是你不得不应付 CORS 带来的整套麻烦:预检请求、一个需要持续维护的允许来源列表、Access-Control-Allow-Credentials 头,还有那种隐约的担忧——你可能已经把权限开得比预期更大了。因为请求是从浏览器直接发出,任何一个打开开发者工具的人都能看到 API 的基础地址、路由结构以及认证方式。相当于你把后端的一幅清晰地图,拱手交给每一位访客。
最隐蔽的地方在于,这一切都不会报错。演示环境跑得好好的,生产环境也毫无异常,代码评审也安然通过。它就是那样静悄悄地待着,成为一个负债项,直到某一天突然不再沉默。
正确的边界其实只需要画一根线:浏览器只与你的 Next.js 服务器对话,而 Next.js 服务器才去和 API 通信。这就是 Backend-for-Frontend 模式(BFF)的核心:
浏览器 -> Next.js (Server Actions / Route Handlers) -> APINext.js 的 App Router 本质上就是为这条路设计的。Server Actions 和 Route Handlers 都运行在服务端,那里可以读取会话 Cookie,令牌永远不需要离开服务器。浏览器手里只握着一个 HttpOnly 的会话 Cookie,除此之外再无其他。它不知道 API 的网址,也读不到令牌的内容。因为所有请求现在都变成同源的了,CORS 的烦恼就此消失。
这不是什么新奇的构想,它本来就是正确的做法。真正棘手的是,如果全部手工去搭建,代价会高到让人退缩。
一旦把 fetch 调用移到服务端,那些枯燥的样板代码就会立刻涌现出来。你必须做这么几件事:从进入的请求中读取会话 Cookie 并原样转发给 API;在登录时接收 API 返回的 Set-Cookie,再以 Next.js 源的名义重新下发;对 JSON 格式的请求体设置 Content-Type: application/json,但如果是 FormData 或者文件上传又绝不能这么干;碰到 429 状态码要去解析 Retry-After 头;还要考虑当 API 完全宕机时该怎样优雅降级,而不是把原始的传输错误直接泼到页面上。
这些工作一项项摊开,就把 BFF 变成了最容易在项目初期被跳过的部分。但一旦跳过去,那个看似无害的客户端调用,就会一直在生产环境里等着,直到某个不起眼的依赖、某次不经意的审查疏漏,或者一个足够确定的外部攻击,让它从沉默的负债项变成公开的漏洞。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.